Initial Commit

This commit is contained in:
grtsinry43 2025-10-05 01:20:02 +08:00
commit 586425998d
Signed by: grtsinry43
GPG Key ID: F3305FB3A978C934
77 changed files with 2675 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

26
.idea/appInsightsSettings.xml generated Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

6
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

6
.idea/copilot.data.migration.agent.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.ask.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.edit.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

18
.idea/deploymentTargetSelector.xml generated Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-10-04T16:00:07.516752Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=15947d21" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

18
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -0,0 +1,61 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

10
.idea/migrations.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

10
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

77
app/build.gradle.kts Normal file
View File

@ -0,0 +1,77 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.qingshuige.tangyuan"
compileSdk = 36
defaultConfig {
applicationId = "com.qingshuige.tangyuan"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
isMinifyEnabled = false
isDebuggable = true
}
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.core)
implementation(libs.androidx.material.icons.extended)
implementation(libs.coil.compose)
// Network dependencies
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
implementation(libs.gson)
implementation(libs.androidx.security.crypto)
implementation(libs.androidx.datastore.preferences)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package com.qingshuige.tangyuan
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.qingshuige.tangyuan", appContext.packageName)
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".TangyuanApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Tangyuan">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Tangyuan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,44 @@
package com.qingshuige.tangyuan
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.qingshuige.tangyuan.navigation.Screen
import com.qingshuige.tangyuan.ui.components.PageLevel
import com.qingshuige.tangyuan.ui.components.TangyuanBottomAppBar
import com.qingshuige.tangyuan.ui.components.TangyuanTopBar
@Composable
fun App() {
var currentScreen by remember { mutableStateOf<Screen>(Screen.Talk) }
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TangyuanTopBar(
currentScreen = currentScreen,
avatarUrl = "https://dogeoss.grtsinry43.com/img/author.jpeg",
pageLevel = PageLevel.PRIMARY,
onAvatarClick = {/* 头像点击事件 */ },
onAnnouncementClick = {/* 公告点击事件 */ },
onPostClick = {/* 发表点击事件 */ }
)
},
bottomBar = {
TangyuanBottomAppBar(currentScreen) { selectedScreen ->
currentScreen = selectedScreen
}
}
) { innerPadding ->
Text(
text = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}

View File

@ -0,0 +1,33 @@
package com.qingshuige.tangyuan
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.qingshuige.tangyuan.navigation.Screen
import com.qingshuige.tangyuan.ui.theme.TangyuanTheme
import com.qingshuige.tangyuan.utils.PrefsManager
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
PrefsManager.init(this)
setContent {
TangyuanTheme {
App()
}
}
}
}

View File

@ -0,0 +1,36 @@
package com.qingshuige.tangyuan
import android.app.Application
import com.qingshuige.tangyuan.network.NetworkClient
import com.qingshuige.tangyuan.network.TokenManager
import com.qingshuige.tangyuan.utils.PrefsManager
import okhttp3.OkHttpClient
import retrofit2.Retrofit
class TangyuanApplication : Application() {
companion object {
lateinit var instance: TangyuanApplication
private set
}
// 全局实例
lateinit var tokenManager: TokenManager
lateinit var okHttpClient: OkHttpClient
lateinit var retrofit: Retrofit
override fun onCreate() {
super.onCreate()
instance = this
// 初始化 PrefsManager
PrefsManager.init(this)
// 初始化 TokenManager
tokenManager = TokenManager(this)
// 初始化网络客户端
okHttpClient = NetworkClient.createOkHttpClient(this)
retrofit = NetworkClient.createRetrofit(okHttpClient)
}
}

View File

@ -0,0 +1,145 @@
package com.qingshuige.tangyuan.api
import com.google.gson.JsonObject
import com.qingshuige.tangyuan.model.Category
import com.qingshuige.tangyuan.model.Comment
import com.qingshuige.tangyuan.model.CreatPostMetadataDto
import com.qingshuige.tangyuan.model.CreateCommentDto
import com.qingshuige.tangyuan.model.CreateUserDto
import com.qingshuige.tangyuan.model.LoginDto
import com.qingshuige.tangyuan.model.NewNotification
import com.qingshuige.tangyuan.model.Notification
import com.qingshuige.tangyuan.model.PostBody
import com.qingshuige.tangyuan.model.PostMetadata
import com.qingshuige.tangyuan.model.User
import okhttp3.MultipartBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Url
// 用于 Retrofit
interface ApiInterface {
@GET("post/metadata/{id}")
fun getPostMetadata(@Path("id") id: Int): Call<PostMetadata>
@GET("post/body/{id}")
fun getPostBody(@Path("id") id: Int): Call<PostBody>
@GET("user/{id}")
fun getUser(@Path("id") id: Int): Call<User>
@GET("post/metadata/user/{id}")
fun getMetadatasByUserID(@Path("id") userId: Int): Call<List<PostMetadata>>
@Deprecated("Use phtPostMetadata instead")
@GET("post/metadata/random/{count}")
fun getRandomPostMetadata(@Path("count") count: Int): Call<List<PostMetadata>>
@POST("philotaxis/postmetadata/{sectionId}")
fun phtPostMetadata(
@Path("sectionId") sectionId: Int,
@Body exceptedIds: List<Int>
): Call<List<PostMetadata>>
@POST("post/metadata")
fun postPostMetadata(@Body metadata: CreatPostMetadataDto): Call<Map<String, Int>>
@POST("post/body")
fun postPostBody(@Body body: PostBody): Call<ResponseBody>
@GET("post/metadata/notice")
fun getNotice(): Call<PostMetadata>
@POST("user")
fun postUser(@Body user: CreateUserDto): Call<ResponseBody>
@DELETE("post/{id}")
fun deletePost(@Path("id") postId: Int): Call<ResponseBody>
@PUT("user/{id}")
fun putUser(@Path("id") id: Int, @Body userInfo: User): Call<ResponseBody>
@GET("comment/post/{postId}")
fun getCommentForPost(@Path("postId") postId: Int): Call<List<Comment>>
@GET("comment/{id}")
fun getComment(@Path("id") id: Int): Call<Comment>
@GET("comment/sub/{parentCommentId}")
fun getSubComment(@Path("parentCommentId") parentCommentId: Int): Call<List<Comment>>
@DELETE("comment/{id}")
fun deleteComment(@Path("id") commentId: Int): Call<ResponseBody>
@Multipart
@POST("image/uploadjpg")
fun postImage(@Part file: MultipartBody.Part): Call<Map<String, String>>
@POST("auth/login")
fun login(@Body loginDto: LoginDto): Call<Map<String, String>>
@Deprecated("Use getAllNotificationsByUserId instead")
@GET("notification/user/{userId}")
fun getAllUnreadNotificationsOf(@Path("userId") userId: Int): Call<List<Notification>>
@Deprecated("Use markNewNotificationAsRead instead")
@GET("notification/mark/{notificationId}")
fun markNotificationAsRead(@Path("notificationId") notificationId: Int): Call<ResponseBody>
@GET("newnotification/{userId}")
fun getAllNotificationsByUserId(@Path("userId") userId: Int): Call<List<NewNotification>>
@GET("newnotification/markasread/{id}")
fun markNewNotificationAsRead(@Path("id") id: Int): Call<ResponseBody>
@POST("comment")
fun postComment(@Body dto: CreateCommentDto): Call<Map<String, String>>
@GET("category/{id}")
fun getCategory(@Path("id") id: Int): Call<Category>
@GET("category/all")
fun getAllCategories(): Call<List<Category>>
@GET("category/count/{id}")
fun getPostCountOfCategory(@Path("id") id: Int): Call<Int>
@GET("category/weeklynewcount/{id}")
fun getWeeklyNewPostCountOfCategory(@Path("id") id: Int): Call<Int>
@GET("post/metadata/category/{categoryId}")
fun getAllMetadatasByCategoryId(@Path("categoryId") categoryId: Int): Call<List<PostMetadata>>
@GET("post/count/category/24h/{categoryId}")
fun get24hNewPostCountByCategoryId(@Path("categoryId") categoryId: Int): Call<Int>
@GET("post/count/category/7d/{categoryId}")
fun get7dNewPostCountByCategoryId(@Path("categoryId") categoryId: Int): Call<Int>
@GET("search/post/{keyword}")
fun searchPostByKeyword(@Path("keyword") keyword: String): Call<List<PostMetadata>>
@GET("search/user/{keyword}")
fun searchUserByKeyword(@Path("keyword") keyword: String): Call<List<User>>
@GET("search/comment/{keyword}")
fun searchCommentByKeyword(@Path("keyword") keyword: String): Call<List<Comment>>
/* 以下为非后端方法 */
@GET
fun getFromUrl(@Url url: String): Call<ResponseBody>
@FormUrlEncoded
@POST("https://api.pgyer.com/apiv2/app/check")
fun checkUpdate(@FieldMap params: Map<String, String>): Call<JsonObject>
}

View File

@ -0,0 +1,11 @@
package com.qingshuige.tangyuan.model
data class Category(
var categoryId: Int = 0,
var baseName: String? = null,
var baseDescription: String? = null
) {
override fun toString(): String {
return baseName!!
}
}

View File

@ -0,0 +1,13 @@
package com.qingshuige.tangyuan.model
import java.util.Date
data class Comment(
var commentId: Int = 0,
var parentCommentId: Int = 0,
var userId: Int = 0,
var postId: Int = 0,
var content: String? = null,
var imageGuid: String? = null,
var commentDateTime: Date? = null
)

View File

@ -0,0 +1,11 @@
package com.qingshuige.tangyuan.model
import java.util.Date
data class CreatPostMetadataDto(
var isVisible: Boolean = false,
var postDateTime: Date? = null,
var sectionId: Int = 0,
var categoryId: Int = 0,
var userId: Int = 0
)

View File

@ -0,0 +1,12 @@
package com.qingshuige.tangyuan.model
import java.util.Date
data class CreateCommentDto(
var commentDateTime: Date? = null,
var content: String? = null,
var imageGuid: String? = null,
var parentCommentId: Long = 0,
var postId: Long = 0,
var userId: Long = 0
)

View File

@ -0,0 +1,9 @@
package com.qingshuige.tangyuan.model
data class CreateUserDto(
var avatarGuid: String? = null,
var isoRegionName: String? = null,
var nickName: String? = null,
var password: String? = null,
var phoneNumber: String? = null
)

View File

@ -0,0 +1,6 @@
package com.qingshuige.tangyuan.model
data class LoginDto(
var password: String? = null,
var phoneNumber: String? = null
)

View File

@ -0,0 +1,13 @@
package com.qingshuige.tangyuan.model
import java.util.Date
data class NewNotification(
var notificationId: Int = 0,
var type: String? = null,
var targetUserId: Int = 0,
var sourceId: Int = 0,
var sourceType: String? = null,
var isRead: Boolean = false,
var createDate: Date? = null
)

View File

@ -0,0 +1,14 @@
package com.qingshuige.tangyuan.model
import java.util.Date
data class Notification(
var notificationId: Int = 0,
var targetUserId: Int = 0,
var targetPostId: Int = 0,
var targetCommentId: Int = 0,
var sourceCommentId: Int = 0,
var sourceUserId: Int = 0,
var isRead: Boolean = false,
var notificationDateTime: Date? = null
)

View File

@ -0,0 +1,9 @@
package com.qingshuige.tangyuan.model
data class PostBody(
var postId: Int = 0,
var textContent: String? = null,
var image1UUID: String? = null,
var image2UUID: String? = null,
var image3UUID: String? = null
)

View File

@ -0,0 +1,12 @@
package com.qingshuige.tangyuan.model
import java.util.Date
data class PostMetadata(
val postId: Int = 0,
val userId: Int = 0,
val postDateTime: Date? = null,
val sectionId: Int = 0,
val categoryId: Int = 0,
val isVisible: Boolean = false
)

View File

@ -0,0 +1,12 @@
package com.qingshuige.tangyuan.model
data class User(
var userId: Int = 0,
var nickName: String = "",
var phoneNumber: String = "",
var isoRegionName: String = "",
var email: String = "",
var bio: String = "",
var avatarGuid: String = "",
var password: String = ""
)

View File

@ -0,0 +1,8 @@
package com.qingshuige.tangyuan.navigation
sealed class Screen(val route: String, val title: String) {
object Talk : Screen("talk", "聊一聊")
object Topic : Screen("topic", "侃一侃")
object Message : Screen("message", "消息")
object User : Screen("settings", "我的")
}

View File

@ -0,0 +1,67 @@
package com.qingshuige.tangyuan.network
import com.google.gson.Gson
import com.qingshuige.tangyuan.model.LoginDto
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
class JwtAuthenticator(private val tm: TokenManager, private val baseUrl: String) :
Authenticator {
private val gson = Gson()
private val mediaType = "application/json; charset=utf-8".toMediaType()
@Throws(IOException::class)
override fun authenticate(route: Route?, response: Response): Request? {
// 创建登录请求体
val loginDto = LoginDto().apply {
phoneNumber = tm.phoneNumber
password = tm.password
}
val json = gson.toJson(loginDto)
val requestBody = json.toRequestBody(mediaType)
// 创建登录请求
val loginRequest = Request.Builder()
.url("${baseUrl}auth/login")
.post(requestBody)
.build()
// 创建新的 OkHttpClient 用于登录请求(避免递归)
val client = OkHttpClient.Builder()
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()
return try {
// 执行登录请求
val loginResponse = client.newCall(loginRequest).execute()
if (loginResponse.isSuccessful) {
val responseBody = loginResponse.body?.string()
val tokenResponse = gson.fromJson(responseBody, Map::class.java)
val newToken = tokenResponse?.values?.firstOrNull() as? String
if (newToken != null) {
// 更新 token
tm.token = newToken
// 重试原始请求,添加新的 access token
response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.header("X-Refresh-Attempt", "true")
.build()
} else {
null // 无法获取新 token放弃重试
}
} else {
null // 登录失败,放弃重试
}
} catch (e: Exception) {
null // 异常情况,放弃重试
}
}
}

View File

@ -0,0 +1,20 @@
package com.qingshuige.tangyuan.network
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
class JwtInterceptor(private val tm: TokenManager) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val token = tm.token
if (token != null) {
val modifiedRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.build()
return chain.proceed(modifiedRequest)
}
return chain.proceed(originalRequest)
}
}

View File

@ -0,0 +1,46 @@
package com.qingshuige.tangyuan.network
import android.content.Context
import com.google.gson.Gson
import com.qingshuige.tangyuan.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object NetworkClient {
private const val BASE_URL = "https://ty.qingshuige.ink/api/"
fun createOkHttpClient(context: Context): OkHttpClient {
val tokenManager = TokenManager(context)
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG)
HttpLoggingInterceptor.Level.BODY
else
HttpLoggingInterceptor.Level.NONE
}
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.addInterceptor(JwtInterceptor(tokenManager))
.authenticator(JwtAuthenticator(tokenManager, BASE_URL))
.build()
}
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit {
val gson = Gson()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
// 添加标准Gson转换为后备
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
}

View File

@ -0,0 +1,34 @@
package com.qingshuige.tangyuan.network
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
class TokenManager(context: Context? = null) {
private val prefs: SharedPreferences
init {
// 如果没有提供 context使用全局 Application context
val ctx = context ?: com.qingshuige.tangyuan.TangyuanApplication.instance
prefs = ctx.getSharedPreferences("tangyuan_token_prefs", Context.MODE_PRIVATE)
}
var token: String?
get() = prefs.getString("JwtToken", null)
set(token) {
prefs.edit { putString("JwtToken", token) }
}
val phoneNumber: String?
get() = prefs.getString("phoneNumber", null)
val password: String?
get() = prefs.getString("password", null)
fun setPhoneNumberAndPassword(phoneNumber: String?, password: String?) {
prefs.edit {
putString("phoneNumber", phoneNumber)
putString("password", password)
}
}
}

View File

@ -0,0 +1,86 @@
package com.qingshuige.tangyuan.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.material.icons.filled.ListAlt
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.qingshuige.tangyuan.navigation.Screen
// 定义一个数据类来存储底部导航项所需的所有信息
private data class BottomNavItem(
val screen: Screen,
val icon: ImageVector,
)
@Composable
fun TangyuanBottomAppBar(currentScreen: Screen, onScreenSelected: (Screen) -> Unit) {
// 定义底部导航栏的项目列表
val items = listOf(
BottomNavItem(Screen.Talk, Icons.Filled.ChatBubble),
BottomNavItem(Screen.Topic, Icons.Filled.ListAlt),
BottomNavItem(Screen.Message, Icons.Filled.Notifications),
BottomNavItem(Screen.User, Icons.Filled.Person)
)
val topBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
NavigationBar(
modifier = Modifier
.fillMaxWidth()
.height(84.dp)
// 绘制顶部的 1dp 分隔线
.drawBehind {
val strokeWidth = 1.dp.toPx()
drawLine(
color = topBorderColor,
start = Offset(0f, 0f),
end = Offset(size.width, 0f),
strokeWidth = strokeWidth
)
},
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {
// 循环渲染所有的导航项
items.forEach { item ->
val isSelected = currentScreen == item.screen
NavigationBarItem(
selected = isSelected,
onClick = { onScreenSelected(item.screen) },
icon = {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
item.icon,
contentDescription = item.screen.title
)
Text(
item.screen.title,
fontSize = 10.sp,
style = MaterialTheme.typography.labelSmall
)
}
},
label = null
)
}
}
}

View File

@ -0,0 +1,212 @@
package com.qingshuige.tangyuan.ui.components
/**
* // 一级页面使用示例
* TangyuanTopBar(
* currentScreen = Screen.Talk,
* pageLevel = PageLevel.PRIMARY,
* onAvatarClick = {/* 头像点击事件 */},
* onAnnouncementClick = {/* 公告点击事件 */},
* onPostClick = {/* 发表点击事件 */}
* )
*
* // 二级页面使用示例
* TangyuanTopBar(
* currentScreen = Screen.Talk,
* pageLevel = PageLevel.SECONDARY,
* onBackClick = {/* 返回点击事件 */},
* onAnnouncementClick = {/* 公告点击事件 */},
* onPostClick = {/* 发表点击事件 */},
* onActionClick = {/* 操作按钮点击事件 */}
* )
*
*/
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Campaign
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.qingshuige.tangyuan.navigation.Screen
// 定义页面层级类型
enum class PageLevel {
PRIMARY, // 一级页面
SECONDARY // 二级页面
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TangyuanTopBar(
currentScreen: Screen,
pageLevel: PageLevel,
avatarUrl: String? = null,
onBackClick: (() -> Unit)? = null,
onAvatarClick: (() -> Unit)? = null,
onAnnouncementClick: (() -> Unit)? = null,
onPostClick: (() -> Unit)? = null,
onActionClick: (() -> Unit)? = null
) {
TopAppBar(
title = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 左侧:头像或返回按钮
when {
// 二级页面显示返回按钮
pageLevel == PageLevel.SECONDARY -> {
IconButton(onClick = { onBackClick?.invoke() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "返回"
)
}
}
// 一级页面且不是我的页面显示头像
pageLevel == PageLevel.PRIMARY && currentScreen != Screen.User -> {
IconButton(
onClick = { onAvatarClick?.invoke() },
modifier = Modifier.size(40.dp)
) {
if (avatarUrl != null) {
AsyncImage(
model = avatarUrl,
contentDescription = "头像",
modifier = Modifier
.size(32.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
onError = {error ->
// 处理图片加载错误
error.result.throwable.printStackTrace()
},
fallback = painterResource(android.R.drawable.ic_menu_gallery),
error = painterResource(android.R.drawable.ic_menu_gallery)
)
} else {
Icon(
Icons.Filled.Person,
contentDescription = "头像",
modifier = Modifier
.size(32.dp)
.clip(CircleShape),
tint = MaterialTheme.colorScheme.primary
)
}
}
}
// 我的页面不显示左侧内容
else -> {
Box(modifier = Modifier.size(40.dp))
}
}
// 中间:页面标题
Text(
text = currentScreen.title,
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f).
padding(start = 24.dp),
textAlign = TextAlign.Start,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// 右侧:公告、发表、操作按钮
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 公告按钮(除我的页面一级页面都显示)
if (!(pageLevel == PageLevel.PRIMARY && currentScreen == Screen.User)) {
IconButton(onClick = { onAnnouncementClick?.invoke() }) {
Icon(
Icons.Filled.Campaign,
contentDescription = "公告",
tint = MaterialTheme.colorScheme.primary
)
}
}
// 发表按钮(除我的一级页面都显示)
if (!(pageLevel == PageLevel.PRIMARY && currentScreen == Screen.User)) {
IconButton(onClick = { onPostClick?.invoke() }) {
Icon(
Icons.Filled.Add,
contentDescription = "发表",
tint = MaterialTheme.colorScheme.primary
)
}
}
// 操作按钮(仅二级页面显示)
if (pageLevel == PageLevel.SECONDARY) {
IconButton(onClick = { onActionClick?.invoke() }) {
Icon(
Icons.Filled.MoreVert,
contentDescription = "更多操作",
tint = MaterialTheme.colorScheme.onSurface
)
}
}
// 如果没有任何右侧按钮,添加占位空间保持布局平衡
if (pageLevel == PageLevel.PRIMARY && currentScreen == Screen.User) {
Box(modifier = Modifier.size(40.dp))
}
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
titleContentColor = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.fillMaxWidth()
)
}
@Preview
@Composable
fun TangyuanTopBarPreview() {
TangyuanTopBar(
currentScreen = Screen.Talk,
pageLevel = PageLevel.PRIMARY,
avatarUrl = "https://dogeoss.grtsinry43.com/img/author.jpeg",
onAvatarClick = {},
onAnnouncementClick = {},
onPostClick = {}
)
}

View File

@ -0,0 +1,157 @@
package com.qingshuige.tangyuan.ui.theme
import androidx.compose.ui.graphics.Color
// 糖原社区颜色系统 - 现代简洁·文化雅致
object TangyuanColors {
// Light Theme Colors - 浅色主题(晴空蓝调)
val PrimaryLight = Color(0xFF2E7CF6) // 明亮天蓝 - 主品牌色
val OnPrimaryLight = Color(0xFFFFFFFF)
val PrimaryContainerLight = Color(0xFFE3F2FF) // 晴空浅蓝容器
val OnPrimaryContainerLight = Color(0xFF001D35)
val SecondaryLight = Color(0xFF5B9FFF) // 次要亮蓝
val OnSecondaryLight = Color(0xFFFFFFFF)
val SecondaryContainerLight = Color(0xFFEBF4FF)
val OnSecondaryContainerLight = Color(0xFF001B3D)
val TertiaryLight = Color(0xFF7B68EE) // 文化紫蓝(艺术感)
val OnTertiaryLight = Color(0xFFFFFFFF)
val TertiaryContainerLight = Color(0xFFEDE7FF)
val OnTertiaryContainerLight = Color(0xFF23036A)
val AccentLight = Color(0xFF00BCD4) // 青蓝强调色
val OnAccentLight = Color(0xFFFFFFFF)
val ErrorLight = Color(0xFFFF5252) // 温和红色
val OnErrorLight = Color(0xFFFFFFFF)
val ErrorContainerLight = Color(0xFFFFEBEE)
val OnErrorContainerLight = Color(0xFF410002)
val SuccessLight = Color(0xFF4CAF50) // 成功绿色
val WarningLight = Color(0xFFFF9800) // 警告橙色
val BackgroundLight = Color(0xFFFAFBFC) // 极简浅灰背景
val OnBackgroundLight = Color(0xFF1A1C1E)
val SurfaceLight = Color(0xFFFFFFFF) // 纯白卡片
val OnSurfaceLight = Color(0xFF1A1C1E)
val SurfaceVariantLight = Color(0xFFF5F7FA) // 分割线浅灰
val OnSurfaceVariantLight = Color(0xFF6B7280)
val OutlineLight = Color(0xFFE5E7EB) // 边框色
val ShadowLight = Color(0x0D000000) // 轻微阴影
// Dark Theme Colors - 深色主题(夜空蓝调)
val PrimaryDark = Color(0xFF5B9FFF) // 深色模式明亮蓝
val OnPrimaryDark = Color(0xFF001D35)
val PrimaryContainerDark = Color(0xFF0B4BA3)
val OnPrimaryContainerDark = Color(0xFFE3F2FF)
val SecondaryDark = Color(0xFF7CB5FF)
val OnSecondaryDark = Color(0xFF001B3D)
val SecondaryContainerDark = Color(0xFF0D3D6E)
val OnSecondaryContainerDark = Color(0xFFEBF4FF)
val TertiaryDark = Color(0xFF9B8AFF) // 深色模式文化紫
val OnTertiaryDark = Color(0xFF23036A)
val TertiaryContainerDark = Color(0xFF3E2D7A)
val OnTertiaryContainerDark = Color(0xFFEDE7FF)
val AccentDark = Color(0xFF26C6DA) // 深色强调色
val OnAccentDark = Color(0xFF00363D)
val ErrorDark = Color(0xFFFF6B6B)
val OnErrorDark = Color(0xFF410002)
val ErrorContainerDark = Color(0xFF8C1D18)
val OnErrorContainerDark = Color(0xFFFFEBEE)
val SuccessDark = Color(0xFF66BB6A)
val WarningDark = Color(0xFFFFB74D)
val BackgroundDark = Color(0xFF0F1419) // 深邃夜空背景
val OnBackgroundDark = Color(0xFFE4E6E9)
val SurfaceDark = Color(0xFF1A1F29) // 深色卡片
val OnSurfaceDark = Color(0xFFE4E6E9)
val SurfaceVariantDark = Color(0xFF141820)
val OnSurfaceVariantDark = Color(0xFF9CA3AF)
val OutlineDark = Color(0xFF2D3748) // 深色边框
val ShadowDark = Color(0x1A000000) // 更深阴影
}
// 语义化颜色扩展
object TangyuanSemanticColors {
// 互动状态色(浅色)
val InteractiveLightHover = Color(0xFFF0F7FF)
val InteractiveLightPressed = Color(0xFFD6EBFF)
val InteractiveLightDisabled = Color(0xFFE5E7EB)
// 互动状态色(深色)
val InteractiveDarkHover = Color(0xFF1E3A5F)
val InteractiveDarkPressed = Color(0xFF0D2847)
val InteractiveDarkDisabled = Color(0xFF374151)
// 信息层级色(浅色)
val TextPrimaryLight = Color(0xFF1A1C1E)
val TextSecondaryLight = Color(0xFF4B5563)
val TextTertiaryLight = Color(0xFF9CA3AF)
val TextDisabledLight = Color(0xFFD1D5DB)
// 信息层级色(深色)
val TextPrimaryDark = Color(0xFFE4E6E9)
val TextSecondaryDark = Color(0xFFB4B8BE)
val TextTertiaryDark = Color(0xFF6B7280)
val TextDisabledDark = Color(0xFF4B5563)
// 功能色
val LinkBlue = Color(0xFF2E7CF6)
val HighlightYellow = Color(0xFFFFF9E6)
val DividerLight = Color(0xFFF0F0F0)
val DividerDark = Color(0xFF2D3748)
}
// 渐变色方案(用于特殊场景)
object TangyuanGradients {
// 主题渐变(浅色)
val PrimaryGradientLight = listOf(
Color(0xFF2E7CF6),
Color(0xFF5B9FFF)
)
// 主题渐变(深色)
val PrimaryGradientDark = listOf(
Color(0xFF0B4BA3),
Color(0xFF2E7CF6)
)
// 文化艺术渐变
val CulturalGradient = listOf(
Color(0xFF7B68EE),
Color(0xFF5B9FFF)
)
// 高级灰渐变背景
val SurfaceGradientLight = listOf(
Color(0xFFFAFBFC),
Color(0xFFF5F7FA)
)
val SurfaceGradientDark = listOf(
Color(0xFF0F1419),
Color(0xFF1A1F29)
)
}
// 使用示例
/*
使用建议
1. 主要操作按钮使用 PrimaryLight/Dark
2. 卡片背景使用 SurfaceLight/Dark
3. 页面背景使用 BackgroundLight/Dark
4. 文化内容区域可使用 TertiaryLight/Dark CulturalGradient
5. 强调元素使用 AccentLight/Dark
6. 分割线使用 OutlineLight/Dark DividerLight/Dark
7. 悬停效果使用 InteractiveLightHover/DarkHover
*/

View File

@ -0,0 +1,88 @@
package com.qingshuige.tangyuan.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = TangyuanColors.PrimaryDark,
secondary = TangyuanColors.SecondaryDark,
tertiary = TangyuanColors.TertiaryDark,
onPrimary = TangyuanColors.OnPrimaryDark,
onSecondary = TangyuanColors.OnSecondaryDark,
onTertiary = TangyuanColors.OnTertiaryDark,
primaryContainer = TangyuanColors.PrimaryContainerDark,
secondaryContainer = TangyuanColors.SecondaryContainerDark,
tertiaryContainer = TangyuanColors.TertiaryContainerDark,
onPrimaryContainer = TangyuanColors.OnPrimaryContainerDark,
onSecondaryContainer = TangyuanColors.OnSecondaryContainerDark,
onTertiaryContainer = TangyuanColors.OnTertiaryContainerDark,
error = TangyuanColors.ErrorDark,
onError = TangyuanColors.OnErrorDark,
errorContainer = TangyuanColors.ErrorContainerDark,
onErrorContainer = TangyuanColors.OnErrorContainerDark,
background = TangyuanColors.BackgroundDark,
onBackground = TangyuanColors.OnBackgroundDark,
surface = TangyuanColors.SurfaceDark,
onSurface = TangyuanColors.OnSurfaceDark,
surfaceVariant = TangyuanColors.SurfaceVariantDark,
onSurfaceVariant = TangyuanColors.OnSurfaceVariantDark,
outline = TangyuanColors.OutlineDark,
)
private val LightColorScheme = lightColorScheme(
primary = TangyuanColors.PrimaryLight,
secondary = TangyuanColors.SecondaryLight,
tertiary = TangyuanColors.TertiaryLight,
onPrimary = TangyuanColors.OnPrimaryLight,
onSecondary = TangyuanColors.OnSecondaryLight,
onTertiary = TangyuanColors.OnTertiaryLight,
primaryContainer = TangyuanColors.PrimaryContainerLight,
secondaryContainer = TangyuanColors.SecondaryContainerLight,
tertiaryContainer = TangyuanColors.TertiaryContainerLight,
onPrimaryContainer = TangyuanColors.OnPrimaryContainerLight,
onSecondaryContainer = TangyuanColors.OnSecondaryContainerLight,
onTertiaryContainer = TangyuanColors.OnTertiaryContainerLight,
error = TangyuanColors.ErrorLight,
onError = TangyuanColors.OnErrorLight,
errorContainer = TangyuanColors.ErrorContainerLight,
onErrorContainer = TangyuanColors.OnErrorContainerLight,
background = TangyuanColors.BackgroundLight,
onBackground = TangyuanColors.OnBackgroundLight,
surface = TangyuanColors.SurfaceLight,
onSurface = TangyuanColors.OnSurfaceLight,
surfaceVariant = TangyuanColors.SurfaceVariantLight,
onSurfaceVariant = TangyuanColors.OnSurfaceVariantLight,
outline = TangyuanColors.OutlineLight,
)
@Composable
fun TangyuanTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
shapes = Shapes,
content = content
)
}

View File

@ -0,0 +1,407 @@
package com.qingshuige.tangyuan.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.material3.Shapes
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.qingshuige.tangyuan.R
// ====================================
// 字体家族定义
// ====================================
// 中英文混合字体(通用)
val TangyuanGeneralFontFamily = FontFamily(
Font(R.font.notosanssc_variablefont_wght, FontWeight.Light), // 300
Font(R.font.notosanssc_variablefont_wght, FontWeight.Normal), // 400
Font(R.font.notosanssc_variablefont_wght, FontWeight.Medium), // 500
Font(R.font.notosanssc_variablefont_wght, FontWeight.SemiBold), // 600
Font(R.font.notosanssc_variablefont_wght, FontWeight.Bold), // 700
)
// 英文字体Quicksand - 现代圆润)
val EnglishFontFamily = FontFamily(
Font(R.font.quicksand_variablefont_wght, FontWeight.Light),
Font(R.font.quicksand_variablefont_wght, FontWeight.Normal),
Font(R.font.quicksand_variablefont_wght, FontWeight.Medium),
Font(R.font.quicksand_variablefont_wght, FontWeight.SemiBold),
Font(R.font.quicksand_variablefont_wght, FontWeight.Bold)
)
// 中文字体(思源黑体 - 清晰易读)
val ChineseFontFamily = FontFamily(
Font(R.font.notosanssc_variablefont_wght, FontWeight.Light),
Font(R.font.notosanssc_variablefont_wght, FontWeight.Normal),
Font(R.font.notosanssc_variablefont_wght, FontWeight.Medium),
Font(R.font.notosanssc_variablefont_wght, FontWeight.SemiBold),
Font(R.font.notosanssc_variablefont_wght, FontWeight.Bold)
)
// 文学专用字体(思源宋体 - 传统韵味)
val LiteraryFontFamily = FontFamily(
Font(R.font.notoserifsc_variablefont_wght, FontWeight.Light),
Font(R.font.notoserifsc_variablefont_wght, FontWeight.Normal),
Font(R.font.notoserifsc_variablefont_wght, FontWeight.Medium),
Font(R.font.notoserifsc_variablefont_wght, FontWeight.SemiBold),
Font(R.font.notoserifsc_variablefont_wght, FontWeight.Bold)
)
// ====================================
// 糖原社区字体排版系统
// ====================================
val Typography = Typography(
// Display 系列 - 大标题(首页、专题页)
displayLarge = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
displayMedium = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Bold,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
),
displaySmall = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
),
// Headline 系列 - 标题(卡片标题、页面标题)
headlineLarge = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
headlineSmall = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
// Title 系列 - 小标题(列表标题、组件标题)
titleLarge = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
// Body 系列 - 正文(文章内容、描述文字)
bodyLarge = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
bodySmall = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
// Label 系列 - 标签(按钮文字、标签、辅助信息)
labelLarge = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
labelMedium = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)
// ====================================
// 糖原社区形状系统(圆角设计)
// ====================================
val Shapes = Shapes(
// 超小圆角 - 标签、徽章
extraSmall = RoundedCornerShape(4.dp),
// 小圆角 - 按钮、输入框
small = RoundedCornerShape(8.dp),
// 中圆角 - 卡片、对话框
medium = RoundedCornerShape(12.dp),
// 大圆角 - 底部表单、大卡片
large = RoundedCornerShape(16.dp),
// 超大圆角 - 浮动按钮、特殊组件
extraLarge = RoundedCornerShape(28.dp)
)
// ====================================
// 扩展圆角样式(用于特殊场景)
// ====================================
object TangyuanShapes {
// 完全圆角(头像、圆形按钮)
val Circle = RoundedCornerShape(50)
// 顶部圆角(底部表单)
val TopRounded = RoundedCornerShape(
topStart = 20.dp,
topEnd = 20.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp
)
// 底部圆角(顶部导航栏下拉)
val BottomRounded = RoundedCornerShape(
topStart = 0.dp,
topEnd = 0.dp,
bottomStart = 20.dp,
bottomEnd = 20.dp
)
// 左侧圆角(右侧滑出抽屉)
val LeftRounded = RoundedCornerShape(
topStart = 20.dp,
topEnd = 0.dp,
bottomStart = 20.dp,
bottomEnd = 0.dp
)
// 右侧圆角(左侧滑出抽屉)
val RightRounded = RoundedCornerShape(
topStart = 0.dp,
topEnd = 20.dp,
bottomStart = 0.dp,
bottomEnd = 20.dp
)
// 文化卡片(不对称圆角 - 艺术感)
val CulturalCard = RoundedCornerShape(
topStart = 16.dp,
topEnd = 4.dp,
bottomStart = 4.dp,
bottomEnd = 16.dp
)
// 聊天气泡 - 发送方(右侧)
val ChatBubbleSent = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = 16.dp,
bottomEnd = 4.dp
)
// 聊天气泡 - 接收方(左侧)
val ChatBubbleReceived = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = 4.dp,
bottomEnd = 16.dp
)
}
// ====================================
// 扩展字体样式(特殊场景)
// ====================================
object TangyuanTypography {
// 数字字体(统计数据、价格)
val numberLarge = TextStyle(
fontFamily = EnglishFontFamily,
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = (-0.5).sp
)
val numberMedium = TextStyle(
fontFamily = EnglishFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
)
val numberSmall = TextStyle(
fontFamily = EnglishFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.sp
)
// 引用文本(文章引用、诗词)
val quote = TextStyle(
fontFamily = ChineseFontFamily,
fontWeight = FontWeight.Normal,
fontSize = 18.sp,
lineHeight = 28.sp,
letterSpacing = 0.8.sp
)
// 代码文本(如果需要等宽字体,保留备用)
val code = TextStyle(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.sp
)
}
// ====================================
// 使用示例和最佳实践
// ====================================
/*
排版使用示例
1. 页面标题
Text(
text = "糖原社区",
style = MaterialTheme.typography.headlineLarge
)
2. 卡片标题
Text(
text = "今日推荐",
style = MaterialTheme.typography.titleLarge
)
3. 正文内容
Text(
text = "这里是文章内容...",
style = MaterialTheme.typography.bodyLarge
)
4. 次要信息
Text(
text = "2小时前",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
5. 按钮文字
Text(
text = "立即体验",
style = MaterialTheme.typography.labelLarge
)
6. 数字显示扩展样式
Text(
text = "1,234",
style = TangyuanTypography.numberLarge,
color = MaterialTheme.colorScheme.primary
)
7. 引用文字扩展样式
Text(
text = "人生若只如初见,何事秋风悲画扇。",
style = TangyuanTypography.quote,
color = MaterialTheme.colorScheme.tertiary
)
形状使用示例
1. 普通卡片
Card(
shape = MaterialTheme.shapes.medium
){...}
2. 按钮
Button(
shape = MaterialTheme.shapes.small
){...}
3. 底部弹窗
Surface(
shape = TangyuanShapes.TopRounded
){...}
4. 聊天气泡
Surface(
shape = TangyuanShapes.ChatBubbleSent,
color = MaterialTheme.colorScheme.primaryContainer
){...}
5. 文化内容卡片
Card(
shape = TangyuanShapes.CulturalCard,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer
)
){...}
设计原则
字体层级严格遵循 Material Design 3 规范
圆角系统4dp / 8dp / 12dp / 16dp / 28dp 五级渐进
行高比例保持 1.4-1.5 倍行高确保中文阅读舒适
字间距中文适当放宽英文数字适当收紧
混排优化中英文混合时自动选择最佳字体
可访问性
- 最小字号 11splabelSmall
- 正文字号不小于 14sp
- 重要信息不小于 16sp
- 行高确保触摸目标至少 48dp
*/

View File

@ -0,0 +1,142 @@
package com.qingshuige.tangyuan.utils
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "tangyuan_prefs")
object PrefsManager {
private lateinit var dataStore: DataStore<Preferences>
fun init(context: Context) {
dataStore = context.dataStore
}
suspend fun getString(key: String, defaultValue: String = ""): String {
val prefKey = stringPreferencesKey(key)
return dataStore.data.map { preferences ->
preferences[prefKey] ?: defaultValue
}.first()
}
suspend fun putString(key: String, value: String) {
val prefKey = stringPreferencesKey(key)
dataStore.edit { preferences ->
preferences[prefKey] = value
}
}
suspend fun getInt(key: String, defaultValue: Int = 0): Int {
val prefKey = intPreferencesKey(key)
return dataStore.data.map { preferences ->
preferences[prefKey] ?: defaultValue
}.first()
}
suspend fun putInt(key: String, value: Int) {
val prefKey = intPreferencesKey(key)
dataStore.edit { preferences ->
preferences[prefKey] = value
}
}
suspend fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
val prefKey = booleanPreferencesKey(key)
return dataStore.data.map { preferences ->
preferences[prefKey] ?: defaultValue
}.first()
}
suspend fun putBoolean(key: String, value: Boolean) {
val prefKey = booleanPreferencesKey(key)
dataStore.edit { preferences ->
preferences[prefKey] = value
}
}
suspend fun getLong(key: String, defaultValue: Long = 0L): Long {
val prefKey = longPreferencesKey(key)
return dataStore.data.map { preferences ->
preferences[prefKey] ?: defaultValue
}.first()
}
suspend fun putLong(key: String, value: Long) {
val prefKey = longPreferencesKey(key)
dataStore.edit { preferences ->
preferences[prefKey] = value
}
}
suspend fun remove(key: String) {
val stringKey = stringPreferencesKey(key)
val intKey = intPreferencesKey(key)
val booleanKey = booleanPreferencesKey(key)
val longKey = longPreferencesKey(key)
dataStore.edit { preferences ->
preferences.remove(stringKey)
preferences.remove(intKey)
preferences.remove(booleanKey)
preferences.remove(longKey)
}
}
suspend fun clear() {
dataStore.edit { preferences ->
preferences.clear()
}
}
// Flow 版本,用于在 Compose 中观察数据变化
fun getStringFlow(key: String, defaultValue: String = ""): Flow<String> {
val prefKey = stringPreferencesKey(key)
return dataStore.data.map { preferences ->
preferences[prefKey] ?: defaultValue
}
}
fun getIntFlow(key: String, defaultValue: Int = 0): Flow<Int> {
val prefKey = intPreferencesKey(key)
return dataStore.data.map { preferences ->
preferences[prefKey] ?: defaultValue
}
}
fun getBooleanFlow(key: String, defaultValue: Boolean = false): Flow<Boolean> {
val prefKey = booleanPreferencesKey(key)
return dataStore.data.map { preferences ->
preferences[prefKey] ?: defaultValue
}
}
fun getLongFlow(key: String, defaultValue: Long = 0L): Flow<Long> {
val prefKey = longPreferencesKey(key)
return dataStore.data.map { preferences ->
preferences[prefKey] ?: defaultValue
}
}
// 同步版本(不推荐,但为了兼容性保留)
fun getStringSync(key: String, defaultValue: String = ""): String = runBlocking {
getString(key, defaultValue)
}
fun getIntSync(key: String, defaultValue: Int = 0): Int = runBlocking {
getInt(key, defaultValue)
}
fun getBooleanSync(key: String, defaultValue: Boolean = false): Boolean = runBlocking {
getBoolean(key, defaultValue)
}
fun getLongSync(key: String, defaultValue: Long = 0L): Long = runBlocking {
getLong(key, defaultValue)
}
}

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Tangyuan</string>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Tangyuan" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,17 @@
package com.qingshuige.tangyuan
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

6
build.gradle.kts Normal file
View File

@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

23
gradle.properties Normal file
View File

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

49
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,49 @@
[versions]
agp = "8.12.3"
coilCompose = "2.7.0"
kotlin = "2.2.20"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0"
composeBom = "2025.09.01"
materialIconsCore = "1.7.8"
retrofit = "3.0.0"
okhttp = "4.12.0"
gson = "2.13.2"
securityCrypto = "1.1.0"
datastore = "1.1.7"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIconsCore" }
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsCore" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "securityCrypto" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Sat Oct 04 21:05:16 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View File

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

24
settings.gradle.kts Normal file
View File

@ -0,0 +1,24 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Tangyuan"
include(":app")