re-init repo

This commit is contained in:
Eric85710 2026-04-19 21:36:53 +08:00
commit 05f75d9d6d
100 changed files with 4104 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 vendored Normal file
View file

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

1
.idea/.name Normal file
View file

@ -0,0 +1 @@
Login_V3

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>

6
.idea/compiler.xml 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>

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="2026-04-19T13:32:51.697307400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\ericc\.android\avd\Pixel_9.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

13
.idea/deviceManager.xml Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

18
.idea/gradle.xml Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<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 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>

9
.idea/misc.xml Normal file
View file

@ -0,0 +1,9 @@
<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>

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>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View file

@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View file

@ -0,0 +1,34 @@
kotlin version: 2.0.21
error message: kotlin.UninitializedPropertyAccessException: lateinit property cleanFilenames has not been initialized
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.getCleanFilenames(KotlinSymbolProcessingExtension.kt:128)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:234)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:195)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:106)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:170)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1555)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

91
app/build.gradle.kts Normal file
View file

@ -0,0 +1,91 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
// 1. 加入 Hilt 與 KSP 插件
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
android {
namespace = "com.example.login_v3"
compileSdk = 36
defaultConfig {
applicationId = "com.example.login_v3"
minSdk = 34
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = 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.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.volley)
implementation(libs.androidx.compose.foundation)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
implementation("androidx.compose.material:material-icons-extended:<compose_version>")
implementation("io.coil-kt:coil-compose:2.5.0")
implementation("io.coil-kt:coil-svg:2.5.0")
implementation("androidx.core:core-splashscreen:1.2.0")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.compose.material3:material3:1.2.0")
implementation(libs.androidx.compose.animation)
implementation(libs.androidx.navigation.compose)
//retrofit
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
//json stuff
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
//加入 Hilt 核心與編譯器
implementation(libs.hilt.android)
ksp(libs.hilt.compiler) // 使用 ksp 取代 kapt,效能更好
//加入 Compose 專用的 Hilt 導覽支援 (這對導覽注入非常重要)
implementation(libs.hilt.navigation.compose)
}

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.example.login_v3
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.example.login_v3", 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">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MyApplication"
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.MyApp.Splash"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MyApp.Splash">
<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,22 @@
package com.example.login_v3
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.example.login_v3.auth.HealthCheckScreen
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
installSplashScreen()
setContent {
Technologia()
}
}
}

View file

@ -0,0 +1,9 @@
package com.example.login_v3
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : Application() {
// 可選:全域初始化
}

View file

@ -0,0 +1,29 @@
package com.example.login_v3
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.rememberNavController
import com.example.login_v3.navigation.AppNavGraph
import com.example.login_v3.navigation.AppViewModel
import com.example.login_v3.navigation.MainViewModel
@Composable
fun Technologia() {
Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = Color.Transparent
) { padding ->
//在navController
AppNavGraph(
paddingValues = padding
)
}
}

View file

@ -0,0 +1,65 @@
package com.example.login_v3.auth
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.login_v3.data.health.HealthCheckViewModel
@Composable
fun HealthCheckScreen(
viewModel: HealthCheckViewModel = hiltViewModel()
) {
val status = viewModel.healthStatus
val detail = viewModel.detailMessage
//layout
Scaffold { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.padding(padding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//health
Text(text = "API Health", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(16.dp))
// 顯示狀態
Text(text = "Status: $status", style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(8.dp))
// 顯示詳細訊息
Text(text = detail, style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(20.dp))
// 如果正在檢查,顯示圓形進度
if (status == "Checking...") {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(12.dp))
}
Row {
Button(onClick = { viewModel.checkApiHealth() }) {
Text("Check Health")
}
Spacer(modifier = Modifier.width(12.dp))
OutlinedButton(onClick = {
// 若需要重置狀態,可在 ViewModel 加一個 reset 方法,或直接呼叫 repository 前先改狀態
}) {
Text("Reset")
}
}
}
}
}

View file

@ -0,0 +1,286 @@
package com.example.login_v3.auth
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AccountCircle
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Red
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavBackStackEntry
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.decode.SvgDecoder
import coil.request.ImageRequest
import com.example.login_v3.R
import com.example.login_v3.ui.theme.light_orange
import com.example.login_v3.ui.theme.main_orange
import kotlin.text.ifEmpty
@Composable
fun LoginScreen(
paddingValues: PaddingValues,
onLoginSuccess: () -> Unit
){
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
var emailError by remember { mutableStateOf("") }
var passwordError by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFDA7029),
Color(0xFF777777),
Color(0xFFB34800)
)
)
)
.padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
//Motion_logo
SvgImage(
resId = R.raw.tg_chat_v3,
modifier = Modifier.size(180.dp)
)
//title
Text(text = "Login",
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
//login Box
Box(
modifier = Modifier
.height(340.dp)
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, top = 20.dp),
contentAlignment = Alignment.Center
){
//glass effect
Box(
modifier = Modifier
.matchParentSize()
.clip(RoundedCornerShape(16.dp)) // 🔥 一定要有
.blur(
radius = 10.dp,
edgeTreatment = BlurredEdgeTreatment.Unbounded
)
.background(
Color.White.copy(alpha = 0.18f)
)
.border( // ⭐ 玻璃邊框
width = 1.dp,
color = Color.White.copy(alpha = 0.35f),
shape = RoundedCornerShape(16.dp)
)
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(20.dp))
TextField(
value = email,
onValueChange = { email = it},
label = {
Text(
emailError.ifEmpty { "Email" },
color = if (emailError.isNotEmpty()) Red else androidx.compose.ui.graphics.Color.Unspecified
)
},
leadingIcon = {
Icon(
Icons.Rounded.AccountCircle,
contentDescription = ""
)
},
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Transparent,
unfocusedIndicatorColor = Transparent
)
)
Spacer(modifier = Modifier.height(10.dp))
TextField(
value = password,
onValueChange = { password = it },
label = {
Text(
passwordError.ifEmpty { "Password" },
color = if (passwordError.isNotEmpty()) Red else androidx.compose.ui.graphics.Color.Unspecified
)
},
leadingIcon = {
Icon(Icons.Rounded.Lock, contentDescription = "")
},
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
val image = if (passwordVisible) {
Icons.Rounded.Visibility
} else {
Icons.Rounded.VisibilityOff
}
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, contentDescription = if (passwordVisible) "Hide password" else "Show password")
}
},
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Transparent,
unfocusedIndicatorColor = Transparent
)
)
Spacer(modifier = Modifier.height(20.dp))
Button(
shape = RoundedCornerShape(8.dp),
onClick = {
emailError = if(email.isBlank()) "Email is required" else ""
passwordError = if(password.isBlank()) "password is required" else ""
if (emailError.isEmpty() && passwordError.isEmpty()){
//login auth logic
}
},
colors = ButtonDefaults.buttonColors(
containerColor = main_orange, // 背景顏色
contentColor = Color.White // 文字顏色
)
) {
Text( text = "Login")
}
Spacer(modifier = Modifier.height(16.dp))
Text( text = "forget password?",
color = light_orange,
modifier = Modifier
.clickable{}
)
}
}
}
}
@Composable
fun SvgImage(
resId: Int,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val imageLoader = ImageLoader.Builder(context)
.components {
add(SvgDecoder.Factory())
}
.build()
val imageRequest = ImageRequest.Builder(context)
.data("android.resource://${context.packageName}/$resId")
.decoderFactory(SvgDecoder.Factory())
.build()
AsyncImage(
model = imageRequest,
contentDescription = null,
modifier = modifier,
imageLoader = imageLoader
)
}

View file

@ -0,0 +1,30 @@
package com.example.login_v3.auth
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.login_v3.data.api.api_class.LoginResponse
import com.example.login_v3.data.repository.basic.ITecnologiaRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
private val repository: ITecnologiaRepository
) : ViewModel() {
// 使用 StateFlow 或 LiveData 來傳遞結果給 UI
private val _loginResult = MutableStateFlow<Result<LoginResponse>?>(null)
val loginResult = _loginResult.asStateFlow()
fun performLogin(username: String, password: String) {
viewModelScope.launch {
// 直接呼叫 repository,ViewModel 不關心資料是從網路還是資料庫來的
val result = repository.login(username, password)
_loginResult.value = result
}
}
}

View file

@ -0,0 +1,113 @@
package com.example.login_v3.auth
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.login_v3.ui.theme.light_orange
import com.example.login_v3.ui.theme.main_orange
@Composable
fun Login_or_Reg_page(
paddingValues: PaddingValues,
onLoginClick: () -> Unit,
onRegClick: () -> Unit
){
Column(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFDA7029),
Color(0xFF777777),
Color(0xFFB34800)
)
)
)
.padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(1f))
Text(
text = "Welcome",
fontSize = 42.sp, // 字體大小(像標題)
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.weight(1f))
//login and reg button
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
//login_button
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(start = 20.dp, end = 20.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Gray, // 背景顏色
contentColor = Color.White // 文字顏色
),
onClick = onLoginClick
) {
Text( text = "Login",
fontSize = 20.sp)
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(end = 20.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = light_orange, // 背景顏色
contentColor = Color.White // 文字顏色
),
onClick = onRegClick
) {
Text( text = "Register",
fontSize = 20.sp)
}
}
Spacer(modifier = Modifier.weight(0.02f))
}
}

View file

@ -0,0 +1,269 @@
package com.example.login_v3.auth
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AccountCircle
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Red
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.login_v3.data.health.HealthCheckViewModel
import com.example.login_v3.ui.theme.main_orange
import kotlin.text.ifEmpty
@Composable
fun Register_Screen(
paddingValues: PaddingValues,
){
var user_name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
var emailError by remember { mutableStateOf("") }
var user_name_Error by remember { mutableStateOf("") }
var passwordError by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFDA7029),
Color(0xFF777777),
Color(0xFFB34800)
)
)
)
.padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Register",
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(10.dp))
//Reg_box
Box(
modifier = Modifier
.height(360.dp)
.fillMaxWidth()
.padding(start = 34.dp, end = 34.dp, top = 20.dp),
contentAlignment = Alignment.Center
){
//glass effect
Box(
modifier = Modifier
.matchParentSize()
.clip(RoundedCornerShape(16.dp)) // 🔥 一定要有
.blur(
radius = 10.dp,
edgeTreatment = BlurredEdgeTreatment.Unbounded
)
.background(
Color.White.copy(alpha = 0.18f)
)
.border( // ⭐ 玻璃邊框
width = 1.dp,
color = Color.White.copy(alpha = 0.35f),
shape = RoundedCornerShape(16.dp)
)
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(20.dp))
//input_username
TextField(
value = user_name,
onValueChange = { user_name = it},
label = {
Text(
user_name_Error.ifEmpty { "username" },
color = if (user_name_Error.isNotEmpty()) Red else androidx.compose.ui.graphics.Color.Unspecified
)
},
leadingIcon = {
Icon(
Icons.Rounded.Person,
contentDescription = ""
)
},
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Transparent,
unfocusedIndicatorColor = Transparent
)
)
Spacer(modifier = Modifier.height(10.dp))
//input_email
TextField(
value = email,
onValueChange = { email = it},
label = {
Text(
emailError.ifEmpty { "Email" },
color = if (emailError.isNotEmpty()) Red else androidx.compose.ui.graphics.Color.Unspecified
)
},
leadingIcon = {
Icon(
Icons.Rounded.AccountCircle,
contentDescription = ""
)
},
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Transparent,
unfocusedIndicatorColor = Transparent
)
)
Spacer(modifier = Modifier.height(10.dp))
//input_password
TextField(
value = password,
onValueChange = { password = it },
label = {
Text(
passwordError.ifEmpty { "Password" },
color = if (passwordError.isNotEmpty()) Red else androidx.compose.ui.graphics.Color.Unspecified
)
},
leadingIcon = {
Icon(Icons.Rounded.Lock, contentDescription = "")
},
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
val image = if (passwordVisible) {
Icons.Rounded.Visibility
} else {
Icons.Rounded.VisibilityOff
}
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, contentDescription = if (passwordVisible) "Hide password" else "Show password")
}
},
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Transparent,
unfocusedIndicatorColor = Transparent
)
)
Spacer(modifier = Modifier.height(20.dp))
//Reg_button
Button(
shape = RoundedCornerShape(8.dp),
onClick = {
user_name_Error = if(user_name.isBlank()) "username is required" else ""
emailError = if(email.isBlank()) "Email is required" else ""
passwordError = if(password.isBlank()) "password is required" else ""
if (user_name_Error.isEmpty() && emailError.isEmpty() && passwordError.isEmpty()){
//login auth logic
}
},
colors = ButtonDefaults.buttonColors(
containerColor = main_orange, // 背景顏色
contentColor = Color.White // 文字顏色
)
) {
Text( text = "Login")
}
//input_block_end
}
}
}
}

View file

@ -0,0 +1,33 @@
package com.example.login_v3.data.api
import com.example.login_v3.data.api.api_class.ApiResponse
import com.example.login_v3.data.api.api_class.FriendRequest
import com.example.login_v3.data.api.api_class.LoginRequest
import com.example.login_v3.data.api.api_class.LoginResponse
import retrofit2.Response
import com.example.login_v3.data.api.api_class.User
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
interface TecnologiaApi {
@GET("api/health")
// 必須包裹在 Response<> 裡面,才能呼叫 .isSuccessful
suspend fun checkHealth(): Response<Unit>
@POST("api/auth/login")
suspend fun login(@Body body: LoginRequest): LoginResponse
@GET("api/me")
suspend fun getMe(@Header("Authorization") token: String): User
@POST("api/friends/request")
suspend fun sendFriendRequest(
@Header("Authorization") token: String,
@Body body: FriendRequest
): ApiResponse
// 其他 endpoint 一樣加
}

View file

@ -0,0 +1,26 @@
package com.example.login_v3.data.api
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
object RetrofitInstance {
private const val BASE_URL = "https://192.168.0.217:8443/" // 開發測試可能需改 HTTP
// 建立 Moshi instance,加入 Kotlin 支援
private val moshi: Moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
val api: TecnologiaApi by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
.create(TecnologiaApi::class.java)
}
}

View file

@ -0,0 +1,10 @@
package com.example.login_v3.data.api.api_class
data class FriendRequest(
val friendId: String
)
data class ApiResponse(
val success: Boolean,
val message: String
)

View file

@ -0,0 +1,11 @@
package com.example.login_v3.data.api.api_class
data class LoginRequest(
val username: String,
val password: String
)
data class LoginResponse(
val token: String,
val userId: String
)

View file

@ -0,0 +1,7 @@
package com.example.login_v3.data.api.api_class
data class User(
val id: String,
val username: String,
val email: String
)

View file

@ -0,0 +1,28 @@
package com.example.login_v3.data.di
import androidx.lifecycle.ViewModel
import com.example.login_v3.data.api.TecnologiaApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Inject
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideTecnologiaApi(): TecnologiaApi { // 補上函數宣告
return Retrofit.Builder()
.baseUrl("https://192.168.0.217:8443") // 記得最後面通常要有斜槓 /
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(TecnologiaApi::class.java) // 這裡要對應你的 Api 介面類別
}
}

View file

@ -0,0 +1,43 @@
package com.example.login_v3.data.health
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.login_v3.data.repository.basic.NetworkRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class HealthCheckViewModel @Inject constructor(
private val repository: NetworkRepository
) : ViewModel() {
// 使用 Compose 的 mutableState,這樣 UI 會自動根據這個值的改變而重繪
var healthStatus by mutableStateOf("Ready to Check")
private set
// 用來顯示更詳細的訊息(例如錯誤內容)
var detailMessage by mutableStateOf("")
private set
fun checkApiHealth() {
viewModelScope.launch {
healthStatus = "Checking..."
detailMessage = "Connecting to server..."
val result = repository.performHealthCheck()
result.onSuccess {
healthStatus = "Success"
detailMessage = "✅ Connection established! (200 OK)"
}.onFailure { exception ->
healthStatus = "Failed"
// 這裡會抓到連線逾時、404、或是找不到伺服器等錯誤
detailMessage = "❌ Error: ${exception.localizedMessage}"
}
}
}
}

View file

@ -0,0 +1,66 @@
package com.example.login_v3.data.repository.basic
import com.example.login_v3.data.api.TecnologiaApi
import com.example.login_v3.data.api.api_class.ApiResponse
import com.example.login_v3.data.api.api_class.FriendRequest
import com.example.login_v3.data.api.api_class.LoginRequest
import com.example.login_v3.data.api.api_class.LoginResponse
import com.example.login_v3.data.api.api_class.User
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.lang.Exception
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class TecnologiaRepositoryImpl @Inject constructor(
private val api: TecnologiaApi // Hilt 會從 NetworkModule 找這個 API
) : ITecnologiaRepository {
override suspend fun login(username: String, password: String): Result<LoginResponse> {
return try {
val response = api.login(LoginRequest(username, password))
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun getMe(token: String): Result<User> {
return try {
val response = api.getMe("Bearer $token")
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun sendFriendRequest(token: String, friendId: String): Result<ApiResponse> {
return try {
val response = api.sendFriendRequest("Bearer $token", FriendRequest(friendId))
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindTecnologiaRepository(
repositoryImpl: TecnologiaRepositoryImpl // 這裡要對應實作類別的名稱
): ITecnologiaRepository
}
interface ITecnologiaRepository {
suspend fun login(username: String, password: String): Result<LoginResponse>
suspend fun getMe(token: String): Result<User>
suspend fun sendFriendRequest(token: String, friendId: String): Result<ApiResponse>
}

View file

@ -0,0 +1,21 @@
package com.example.login_v3.data.repository.basic
import com.example.login_v3.data.api.TecnologiaApi
import javax.inject.Inject
class NetworkRepository @Inject constructor(
private val apiService: TecnologiaApi
) {
suspend fun performHealthCheck(): Result<Boolean> {
return try {
val response = apiService.checkHealth()
if (response.isSuccessful) {
Result.success(true)
} else {
Result.failure(Exception("Error code: ${response.code()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}

View file

@ -0,0 +1,28 @@
package com.example.login_v3.home
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavBackStackEntry
@Composable
fun HomeScreen(onLogout: () -> Unit, paddingValues: PaddingValues) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(
onClick = onLogout
) {
Text( text = "Logout")
}
}
}

View file

@ -0,0 +1,36 @@
package com.example.login_v3.home
import android.net.Uri
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.login_v3.home.setting.setting_detail_Screen
import com.example.login_v3.home.setting.setting_list_Screen
import com.example.login_v3.navigation.BottomBarViewModel
import com.example.login_v3.navigation.ScreensViewModel
@Composable
fun Tg_MarketPlace(){
Box(
modifier = Modifier.fillMaxSize()
) {
Text(text = "marketplace")
}
}

View file

@ -0,0 +1,161 @@
package com.example.login_v3.home
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.runtime.getValue
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Tg_Message() {
val viewModel: MessageViewModel = viewModel()
val contacts by viewModel.contacts.collectAsState()
Scaffold(
containerColor = Color.Transparent,
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFFDA7029),
Color(0xFF777777),
Color(0xFFB34800)
)
)
),
topBar = {
TopAppBar(
title = { Text("Messages") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent, // ⭐ 改這裡
titleContentColor = Color.Black
),
modifier = Modifier
.background(
color = Color.White.copy(alpha = 0.2f), // ⭐ 調低透明度
shape = RoundedCornerShape(0.dp)
)
.border(
width = 0.5.dp,
color = Color.White.copy(alpha = 0.4f),
shape = RoundedCornerShape(0.dp)
)
)
},
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
) {
Spacer(modifier = Modifier.height(8.dp))
LazyColumn(
contentPadding = innerPadding
) {
items(contacts) { contact ->
Card(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.padding(6.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color.Transparent // ✅ 透明背景
)
) {
Box(
modifier = Modifier
.fillMaxSize()
) {
//glass effect
Box(
modifier = Modifier
.matchParentSize()
.blur(10.dp)
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Color.White.copy(alpha = 0.08f),
RoundedCornerShape(16.dp)
)
.border(
1.dp,
Color.White.copy(alpha = 0.3f),
RoundedCornerShape(16.dp)
)
)
// 前景層:文字 + 圖片,不會被模糊
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(12.dp),
) {
Image(
painter = painterResource(id = contact.avatarResId),
contentDescription = "${contact.name}'s avatar",
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.border(2.dp, Color.White, CircleShape)
)
Column(modifier = Modifier.padding(start = 12.dp)) {
Text(
text = contact.name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = "Online",
style = MaterialTheme.typography.bodySmall,
color = Color.Green
)
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,36 @@
// Message_ViewModel.kt
package com.example.login_v3.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.login_v3.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
data class Contact(
val name: String,
val avatarResId: Int // 或者用 String 存圖片 URL
)
class MessageViewModel : ViewModel() {
private val _contacts = MutableStateFlow<List<Contact>>(emptyList())
val contacts: StateFlow<List<Contact>> = _contacts
init {
viewModelScope.launch {
_contacts.value = listOf(
Contact("Alice", R.drawable.avatar_v1),
Contact("Bob", R.drawable.avatar_v1),
Contact("Charlie", R.drawable.avatar_v1),
Contact("David", R.drawable.avatar_v1),
Contact("nigga", R.drawable.avatar_v1),
Contact("gbl", R.drawable.avatar_v1),
Contact("威威夢夢", R.drawable.avatar_v1)
)
}
}
}

View file

@ -0,0 +1,203 @@
package com.example.login_v3.home
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Divider
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
@Composable
fun Tg_Server(
viewModel: ServerViewModel
){
val serverList by viewModel.serverList.collectAsState()
val currentServer by viewModel.currentServer.collectAsState()
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center),
verticalArrangement = Arrangement.Center
) {
//TOP_SPACER
Spacer(
modifier = Modifier.weight(0.6f)
)
Box(
modifier = Modifier
.weight(8f)
.fillMaxWidth()
){
//glass effect
Box(
modifier = Modifier
.matchParentSize()
.blur(10.dp)
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Color.Black.copy(alpha = 0.08f),
RoundedCornerShape(16.dp)
)
)
//row contain server list and function block
Row(
modifier = Modifier
.fillMaxSize()
) {
//server list
Column(
modifier = Modifier
.width(80.dp)
.fillMaxHeight()
) {
LazyColumn {
items(serverList) { server ->
Image(
painter = painterResource(id = server.server_icon),
contentDescription = server.name,
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.clickable {
viewModel.selectServer(server.id)
}
.padding(8.dp)
)
}
}
}
//server function block
Column(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
) {
Box(modifier = Modifier
.fillMaxSize()
){
//glass effect for server function_block
Box(
modifier = Modifier
.matchParentSize()
.background(
Color.White.copy(alpha = 0.08f),
RoundedCornerShape(16.dp)
)
.border(
1.dp,
Color.White.copy(alpha = 0.3f),
RoundedCornerShape(16.dp)
)
)
//column contain function block
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(10.dp))
//server name
Text(
text = if (currentServer != null) {
"目前伺服器:${currentServer!!.name}"
} else {
"尚未選擇伺服器"
},
fontSize = 20.sp, // 調整字體大小
fontWeight = FontWeight.Bold, // 設定字體加粗
color = Color(0xFFFFEDD9)
)
Divider(
color = Color(0xFFFFEDD9), // 線的顏色
thickness = 2.dp, // 線的粗細
modifier = Modifier.padding(vertical = 8.dp) // 與上下內容的間距
)
//gallery
Box(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.padding(horizontal = 28.dp, vertical = 16.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFFFEDD9), // 漸層起始色
Color(0xFFAB47BC) // 漸層結束色
),
start = Offset(0f, 0f),
end = Offset(1000f, 1000f) // 控制漸層方向
),
shape = RoundedCornerShape(8.dp)
)
.padding(24.dp)
) {}
}
}
}
}
}
//bottom spacer
Spacer(
modifier = Modifier.weight(1.4f)
)
}
}
}

View file

@ -0,0 +1,44 @@
package com.example.login_v3.home
import androidx.lifecycle.ViewModel
import com.example.login_v3.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
data class Server(
val id: String,
val name: String,
val server_icon: Int
)
class ServerViewModel : ViewModel() {
private val _serverList = MutableStateFlow<List<Server>>(emptyList())
val serverList: StateFlow<List<Server>> = _serverList
private val _currentServer = MutableStateFlow<Server?>(null)
val currentServer: StateFlow<Server?> = _currentServer
init {
// 測試用:初始化一些伺服器
_serverList.value = listOf(
Server("1", "伺服器A", R.drawable.avatar_v1),
Server("2", "伺服器B", R.drawable.avatar_v1),
Server("3", "伺服器C", R.drawable.avatar_v1)
)
}
fun addServer(server: Server) {
_serverList.value = _serverList.value + server
}
fun selectServer(serverId: String) {
_currentServer.value = _serverList.value.find { it.id == serverId }
}
fun removeServer(serverId: String) {
_serverList.value = _serverList.value.filterNot { it.id == serverId }
if (_currentServer.value?.id == serverId) {
_currentServer.value = null
}
}
}

View file

@ -0,0 +1,97 @@
package com.example.login_v3.home.setting
import android.net.Uri
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.login_v3.navigation.BottomBarViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Tg_Setting(
bottomBarViewModel: BottomBarViewModel
) {
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
SharedTransitionLayout() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "setting_list"
){
//setting_list_page
composable("setting_list") {
setting_list_Screen(
//scaling animation effect
onItemClick = { title, iconKey ->
val encodedTitle = Uri.encode(title)
navController.navigate("setting_detail/$encodedTitle/$iconKey")
},
animatedVisibilityScope = this
)
}
//setting_detail_page
composable(
route = "setting_detail/{title}/{iconKey}",
arguments = listOf(
navArgument("title") { type = NavType.StringType },
navArgument("iconKey") { type = NavType.StringType }
)
) { backStackEntry ->
val title = backStackEntry.arguments?.getString("title") ?: ""
val iconKey = backStackEntry.arguments?.getString("iconKey") ?: ""
setting_detail_Screen(title = title,
settingIcon = iconKey,
animatedVisibilityScope = this,
bottomBarViewModel = bottomBarViewModel
)
}
}
}
}
}
}

View file

@ -0,0 +1,38 @@
package com.example.login_v3.home.setting
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Subscriptions
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
data class SettingItem(
val id: String,
val title: String,
val description: String? = null,
val setting_icon: ImageVector,
val iconKey: String
)
class SettingViewModel : ViewModel() {
private val _settings = MutableStateFlow<List<SettingItem>>(emptyList())
val settings: StateFlow<List<SettingItem>> = _settings
init {
_settings.value = listOf(
SettingItem("profile", "Profile", "Manage your account", Icons.Filled.Person, "profile"),
SettingItem("theme", "Theme", "Customize app appearance", Icons.Filled.Palette, "theme"),
SettingItem("devices", "Devices", "Manage connected devices", Icons.Filled.Devices, "devices"),
SettingItem("subscription", "Subscription", "View or update your plan", Icons.Filled.Subscriptions, "subscribetion")
)
}
}

View file

@ -0,0 +1,126 @@
package com.example.login_v3.home.setting
import android.net.Uri
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.Help
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Subscriptions
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.login_v3.home.setting.setting_detail_page.setting_devices_page
import com.example.login_v3.home.setting.setting_detail_page.setting_profile_page
import com.example.login_v3.home.setting.setting_detail_page.setting_subscription_page
import com.example.login_v3.home.setting.setting_detail_page.setting_theme_page
import com.example.login_v3.navigation.BottomBarViewModel
@Composable
fun SharedTransitionScope.setting_detail_Screen(
title: String,
settingIcon: String,
animatedVisibilityScope: AnimatedVisibilityScope,
bottomBarViewModel: BottomBarViewModel
){
//control bottom bar behavior
DisposableEffect(Unit) {
bottomBarViewModel.setVisible(false)
onDispose {
bottomBarViewModel.setVisible(true)
}
}
val setting_icon_decode: ImageVector = when (settingIcon) {
"profile" -> Icons.Default.Person
"theme" -> Icons.Default.Palette
"devices" -> Icons.Default.Devices
"subscribetion" -> Icons.Default.Subscriptions
else -> Icons.Default.Help
}
Column(modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
val iconState = rememberSharedContentState(key = "setting_icon_${settingIcon}")
val titleState = rememberSharedContentState(key = "setting_title_${settingIcon}_${Uri.encode(title)}")
Spacer(modifier = Modifier.height(46.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = setting_icon_decode,
contentDescription = title,
modifier = Modifier
.size(48.dp)
.sharedElement(
sharedContentState = iconState,
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400)
}
),
tint = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = title,
fontSize = 38.sp,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
)
}
//setting_detail_page_Navigation
val setting_detail_page_navController = rememberNavController()
val startRoute = when (settingIcon) {
"profile" -> "setting_Profile"
"theme" -> "setting_Theme"
"devices" -> "setting_Devices"
"subscribetion" -> "setting_Subscription"
else -> "setting_Profile"
}
//spacing between content and title
Spacer(modifier = Modifier.height(26.dp))
NavHost(
navController = setting_detail_page_navController,
startDestination = startRoute
) {
composable("setting_Profile") { setting_profile_page() }
composable("setting_Theme") { setting_theme_page() }
composable("setting_Devices") { setting_devices_page() }
composable("setting_Subscription") { setting_subscription_page() }
}
}
}

View file

@ -0,0 +1,172 @@
package com.example.login_v3.home.setting
import android.R
import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.draw.blur
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.example.login_v3.navigation.ScreensViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SharedTransitionScope.setting_list_Screen(
onItemClick: (String, String) -> Unit,
viewModel: SettingViewModel = viewModel(),
animatedVisibilityScope: AnimatedVisibilityScope, //scaling animation effect
){
var query by remember { mutableStateOf("") }
var active by remember { mutableStateOf(false) }
Column(modifier = Modifier
.fillMaxSize()
) {
val settings by viewModel.settings.collectAsState()
Spacer(modifier = Modifier.height(46.dp))
SearchBar(
query = query,
onQueryChange = { query = it },
onSearch = { /* 執行搜尋邏輯 */ },
active = active,
shape = RoundedCornerShape(12.dp),
onActiveChange = { active = it },
placeholder = { Text("搜尋設定") },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
) {
// 這裡是 SearchBar 展開後的內容
Text("搜尋結果: $query")
} // ← 注意這個大括號要結束 SearchBar
Spacer(modifier = Modifier.height(14.dp))
LazyColumn(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(2.dp), // 每個 item 之間 8.dp
contentPadding = PaddingValues(vertical = 2.dp)
) {
itemsIndexed(settings){ index, item ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
) {
Box(modifier = Modifier.matchParentSize().blur(10.dp))
Box(
modifier = Modifier
.matchParentSize()
.background(Color.White.copy(alpha = 0.08f), RoundedCornerShape(16.dp))
.border(1.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
)
Row(modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable{ onItemClick(item.title, item.iconKey)}
) {
val iconState = rememberSharedContentState(key = "setting_icon_${item.iconKey}")
val titleState = rememberSharedContentState(key = "setting_title_${item.iconKey}_${Uri.encode(item.title)}")
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = "${item.title}",
style = MaterialTheme.typography.titleMedium,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.sharedElement(
sharedContentState = titleState,
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 1000)
}
)
.padding(bottom = 4.dp)
)
item.description?.let {
Text(text = it,
style = MaterialTheme.typography.bodySmall,
color = Color.White
)
}
}
Spacer(modifier = Modifier.weight(1f))
Icon(item.setting_icon,
contentDescription = item.title,
modifier = Modifier
.sharedElement(
sharedContentState = iconState,
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400)
}
)
.padding(bottom = 4.dp)
.size(54.dp),
tint = Color.White
)
}
}
}
}
}
}

View file

@ -0,0 +1,12 @@
package com.example.login_v3.home.setting.setting_detail_page
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun setting_devices_page(){
Column() {
Text(text = "setting_devices_page")
}
}

View file

@ -0,0 +1,181 @@
package com.example.login_v3.home.setting.setting_detail_page
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Divider
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.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.login_v3.R
@Composable
fun setting_profile_page(){
Column(
modifier = Modifier.fillMaxSize()
) {
val avatarSize = 80.dp
val avatarOffset = avatarSize / 2 + 20.dp
Box() {
//glass effect
Box(modifier = Modifier.matchParentSize().blur(10.dp))
Box(
modifier = Modifier
.matchParentSize()
.background(Color.White.copy(alpha = 0.08f), RoundedCornerShape(16.dp))
.border(1.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
)
Column() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(272.dp)
){
Box(
modifier = Modifier
.padding(12.dp)
.clip(RoundedCornerShape(16.dp))
){
// 1. 封面圖 (背景)
Image(
painter = painterResource(id = R.drawable.thumbnail_v1),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
)
}
Row(
modifier = Modifier
.align(Alignment.BottomStart)
.offset(y = -10.dp)
,
verticalAlignment = Alignment.CenterVertically
){
Spacer(modifier = Modifier.width(36.dp))
//user avatar
Image(
painter = painterResource(id = R.drawable.avatar_v1), // 把 JPG 放在 res/drawable
contentDescription = "avatar",
modifier = Modifier
.size(avatarSize)
.clip(CircleShape)
,
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(20.dp))
Text(text = "Eric Chen",
fontSize = 36.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier
.padding(top = 14.dp)
)
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
//user detail
Box(){
//glass effect
Box(modifier = Modifier.matchParentSize().blur(10.dp))
Box(
modifier = Modifier
.matchParentSize()
.background(Color.White.copy(alpha = 0.08f), RoundedCornerShape(16.dp))
.border(1.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
)
//intro
Column(
modifier = Modifier
.padding(20.dp)
) {
// mutual friends
Text(text = "258 mutual friends",
fontSize = 20.sp,
)
Divider(
color = Color.White.copy(alpha = 0.4f),
thickness = 0.5.dp,
modifier = Modifier.padding(vertical = 8.dp)
)
//mutual server
Text(text = "25 servers",
fontSize = 20.sp,
)
Divider(
color = Color.White.copy(alpha = 0.4f),
thickness = 0.5.dp,
modifier = Modifier.padding(vertical = 8.dp)
)
//gender
Text(
text = "male",
fontSize = 22.sp
)
Divider(
color = Color.White.copy(alpha = 0.4f),
thickness = 0.5.dp,
modifier = Modifier.padding(vertical = 8.dp)
)
//location
Text(
text = "Taiwan",
fontSize = 22.sp
)
Divider(
color = Color.White.copy(alpha = 0.4f),
thickness = 0.5.dp,
modifier = Modifier.padding(vertical = 8.dp)
)
//intro
Text(
text = "Founder of JFF studios,Software Dev Guy,3D Artist",
fontSize = 16.sp
)
}
}
}
}

View file

@ -0,0 +1,12 @@
package com.example.login_v3.home.setting.setting_detail_page
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun setting_subscription_page(){
Column() {
Text(text = "setting_sub_page")
}
}

View file

@ -0,0 +1,12 @@
package com.example.login_v3.home.setting.setting_detail_page
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun setting_theme_page(){
Column() {
Text(text = "setting_Theme_page")
}
}

View file

@ -0,0 +1,41 @@
package com.example.login_v3.navigation
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class MainViewModel : ViewModel() {
var selectedTab by mutableStateOf(1)
private set
fun selectTab(index: Int) {
selectedTab = index
}
}
//init navigation
enum class AppScreen {
PreReg, Login, Register, Home, ScreensTab
}
class AppViewModel : ViewModel() {
private val _currentScreen = MutableStateFlow<AppScreen>(AppScreen.ScreensTab)
val currentScreen: StateFlow<AppScreen> = _currentScreen
fun goTo(screen: AppScreen) {
_currentScreen.value = screen
}
fun onLoginSuccess() {
_currentScreen.value = AppScreen.Home
}
fun onLogout() {
_currentScreen.value = AppScreen.PreReg
}
}

View file

@ -0,0 +1,141 @@
package com.example.login_v3.navigation
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.example.login_v3.home.MessageViewModel
import com.example.login_v3.home.Tg_MarketPlace
import com.example.login_v3.home.Tg_Message
import com.example.login_v3.home.Tg_Server
import com.example.login_v3.home.setting.Tg_Setting
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MainScreen_tab() {
val screensViewModel: ScreensViewModel = viewModel()
val bottomBarViewModel: BottomBarViewModel = viewModel()
//show bottom bar or not
val showBottomBar by bottomBarViewModel.showBottomBar.collectAsState()
val navHeight = 64.dp
val gap = 40.dp
val bottomBarHeight = navHeight + gap
Scaffold(
containerColor = Color.Transparent,
modifier = Modifier
.fillMaxSize()
.background(brush = Brush.verticalGradient(
colors = listOf(
Color(0xFFDA7029),
Color(0xFF777777),
Color(0xFFB34800)
)
)),
bottomBar = {
BottomBarAnimated(
visible = showBottomBar,
height = bottomBarHeight,
content = {
Column(modifier = Modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(navHeight)
) {
nav_Wheel_display_block(screensViewModel = screensViewModel)
}
Spacer(modifier = Modifier.height(gap)) // 真正的底下空白
}
}
)
}
) { innerPadding ->
Screens_NavGraph(
paddingValues = innerPadding,
screensViewModel = screensViewModel,
bottomBarViewModel = bottomBarViewModel
)
}
}
@Composable
fun BottomBarAnimated(
visible: Boolean,
height: Dp,
content: @Composable BoxScope.() -> Unit
) {
// 位移與透明度動畫
val offsetY by animateDpAsState(targetValue = if (visible) 0.dp else height)
val alpha by animateFloatAsState(targetValue = if (visible) 1f else 0f)
Box(
modifier = Modifier
.fillMaxWidth()
.height(height)
.offset(y = offsetY)
.graphicsLayer { this.alpha = alpha }
.background(Color.Transparent),
content = content
)
}
@Composable
fun Screens_NavGraph(
paddingValues: PaddingValues,
screensViewModel: ScreensViewModel,
bottomBarViewModel: BottomBarViewModel,
messageViewModel: MessageViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
//nav_wheel_selecting tab
val selectedScreen by screensViewModel.selected.collectAsState()
//message_contact_list_import
val contacts by messageViewModel.contacts.collectAsState()
when (selectedScreen) {
Screen.Message -> Tg_Message()
Screen.Server -> Tg_Server(viewModel = viewModel())
Screen.MarketPlace -> Tg_MarketPlace()
Screen.Setting -> Tg_Setting(
bottomBarViewModel = bottomBarViewModel
)
}
}

View file

@ -0,0 +1,10 @@
package com.example.login_v3.navigation
import androidx.compose.ui.graphics.vector.ImageVector
sealed class Screen(val route: String, val label: String? = null, val icon: ImageVector? = null) {
object Message : Screen("message", label = "Message")
object Server : Screen("server", label = "Server")
object MarketPlace : Screen("marketplace", label = "MarketPlace")
object Setting : Screen("setting", label = "Setting")
}

View file

@ -0,0 +1,25 @@
package com.example.login_v3.navigation
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class ScreensViewModel : ViewModel() {
private val _selected = MutableStateFlow<Screen>(Screen.Message)
val selected: StateFlow<Screen> = _selected
fun select(screen: Screen) {
_selected.value = screen
}
}
// BottomBarViewModel 放在同一個檔案,但仍是獨立的 ViewModel 類別
class BottomBarViewModel : ViewModel() {
private val _showBottomBar = MutableStateFlow(true)
val showBottomBar: StateFlow<Boolean> = _showBottomBar
fun setVisible(visible: Boolean) {
_showBottomBar.value = visible
}
}

View file

@ -0,0 +1,209 @@
package com.example.login_v3.navigation
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Dns
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.Message
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import kotlinx.coroutines.flow.filter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.material3.Icon
import androidx.navigation.NavHostController
//nav_wheel_icon數據
data class WheelItemData(
val icon: ImageVector,
val screen: Screen
)
@Composable
fun nav_Wheel_display_block(
screensViewModel: ScreensViewModel
) {
var isPressed by remember { mutableStateOf(false) }
//position_animation
val floatingOffset by animateDpAsState(
targetValue = if (isPressed) (-80).dp else 0.dp,
animationSpec = tween(durationMillis = 160, easing = FastOutSlowInEasing)
)
//scale_animation
val scale by animateFloatAsState(
targetValue = if (isPressed) 1.2f else 1f,
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing)
)
//different tab
val items = listOf(
WheelItemData(Icons.Filled.Message, Screen.Message),
WheelItemData(Icons.Filled.Dns, Screen.Server),
WheelItemData(Icons.Filled.Extension, Screen.MarketPlace),
WheelItemData(Icons.Filled.Settings, Screen.Setting)
)
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom
) {
Box(
modifier = Modifier.graphicsLayer {
translationY = floatingOffset.toPx()
scaleX = scale
scaleY = scale
}
) {
HorizontalWheelPicker(
items = items,
onValueChange = { item -> screensViewModel.select(item.screen) },
onInteractionChanged = { isPressed = it }
)
}
}
}
//wheel picker
@Composable
fun HorizontalWheelPicker(
items: List<WheelItemData>,
itemWidth: Dp = 80.dp,
onValueChange: (WheelItemData) -> Unit,
onInteractionChanged: (Boolean) -> Unit
) {
val listState = rememberLazyListState()
val flingBehavior = rememberSnapFlingBehavior(listState)
// 計算動態 horizontal padding(假設 LazyRow 佔滿螢幕寬度)
val screenWidthDp = androidx.compose.ui.platform.LocalConfiguration.current.screenWidthDp.dp
val horizontalPadding = (screenWidthDp - itemWidth) / 2
Box(modifier = Modifier
.fillMaxWidth()
.height(60.dp)
) {
LazyRow(
state = listState,
flingBehavior = flingBehavior,
contentPadding = PaddingValues(horizontal = horizontalPadding),
modifier = Modifier.fillMaxSize()
) {
itemsIndexed(items, key = { index, _ -> index }) { index, item ->
WheelItem(index = index, itemWidth = itemWidth, item = item)
}
}
// 中央指示線(不變)
Box(
modifier = Modifier
.align(Alignment.Center)
.width(itemWidth)
.fillMaxHeight()
) {
Box(
modifier = Modifier
.matchParentSize()
.clip(RoundedCornerShape(16.dp))
.blur(10.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded)
.background(Color.White.copy(alpha = 0.18f))
.border(1.dp, Color.White.copy(alpha = 0.35f), RoundedCornerShape(16.dp))
)
}
}
val isInteracting by remember { derivedStateOf { listState.isScrollInProgress } }
// 使用 layoutInfo.visibleItemsInfo 計算最接近中心的 item
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
.filter { it.isNotEmpty() }
.collect { visibleItems ->
val viewportCenter =
(listState.layoutInfo.viewportStartOffset + listState.layoutInfo.viewportEndOffset) / 2
val closest = visibleItems.minByOrNull { vi ->
kotlin.math.abs((vi.offset + vi.size / 2) - viewportCenter)
} ?: visibleItems.first()
val selectedIndex = closest.key as Int
onValueChange(items[selectedIndex])
}
}
// 通知互動狀態
LaunchedEffect(isInteracting) {
onInteractionChanged(isInteracting)
}
}
//wheel item
@Composable
fun WheelItem(
index: Int,
itemWidth: Dp,
item: WheelItemData
) {
Box(
modifier = Modifier
.width(itemWidth)
.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = item.icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(28.dp)
)
}
}

View file

@ -0,0 +1,56 @@
package com.example.login_v3.navigation
import android.R.attr.padding
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.login_v3.auth.LoginScreen
import com.example.login_v3.auth.LoginViewModel
import com.example.login_v3.auth.Login_or_Reg_page
import com.example.login_v3.auth.Register_Screen
import com.example.login_v3.home.HomeScreen
import com.example.login_v3.home.Tg_MarketPlace
@Composable
fun AppNavGraph(
paddingValues: PaddingValues,
appViewModel: AppViewModel = viewModel()
) {
val currentScreen by appViewModel.currentScreen.collectAsState()
when (currentScreen) {
AppScreen.PreReg -> Login_or_Reg_page(
paddingValues = paddingValues,
onLoginClick = { appViewModel.goTo(AppScreen.Login) },
onRegClick = { appViewModel.goTo(AppScreen.Register) }
)
AppScreen.Login -> LoginScreen(
paddingValues = paddingValues,
onLoginSuccess = { appViewModel.onLoginSuccess() }
)
AppScreen.Register -> {
//back to pre_reg
BackHandler {
appViewModel.goTo(AppScreen.PreReg)
}
Register_Screen(paddingValues = paddingValues)
}
AppScreen.Home -> HomeScreen(
paddingValues = paddingValues,
onLogout = { appViewModel.onLogout() }
)
AppScreen.ScreensTab -> MainScreen_tab()
}
}

View file

@ -0,0 +1,14 @@
package com.example.login_v3.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val main_orange = Color(0xFFFF6700)
val light_orange = Color(0xFFFFAD00)

View file

@ -0,0 +1,58 @@
package com.example.login_v3.ui.theme
import android.app.Activity
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 = main_orange,
secondary = light_orange,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = main_orange,
secondary = light_orange,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun Login_V3Theme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
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,
content = content
)
}

View file

@ -0,0 +1,34 @@
package com.example.login_v3.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

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.

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 MiB

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,48 @@
-----BEGIN CERTIFICATE-----
MIIDsTCCAzegAwIBAgISBk04PnNRbDK7k8y6K1rWIU5KMAoGCCqGSM49BAMDMDIx
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
ODAeFw0yNjA0MTcwODIyMjRaFw0yNjA3MTYwODIyMjNaMCAxHjAcBgNVBAMTFXRn
LnRlY2hub2xvZ2lhLXR3LmNvbTB2MBAGByqGSM49AgEGBSuBBAAiA2IABID+bF06
cwXMT3n0YXUylFIioGDgRjzZ0J2pgJFDsqBn+tM80wZwMmnhApI0rhCWI84aybFz
7U5YyzZ911w85Mi43ZCZ0hz8J3UjIP0rxqlocGCLUltF16+RdPsothbO4qOCAiAw
ggIcMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMB
Af8EAjAAMB0GA1UdDgQWBBT6AgbGomnDB1DXHrA6GP+PqSfCOTAfBgNVHSMEGDAW
gBSPDROi9i5+0VBsMxg4XVmOI3KRyjAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUH
MAKGFmh0dHA6Ly9lOC5pLmxlbmNyLm9yZy8wIAYDVR0RBBkwF4IVdGcudGVjaG5v
bG9naWEtdHcuY29tMBMGA1UdIAQMMAowCAYGZ4EMAQIBMC0GA1UdHwQmMCQwIqAg
oB6GHGh0dHA6Ly9lOC5jLmxlbmNyLm9yZy83NS5jcmwwggELBgorBgEEAdZ5AgQC
BIH8BIH5APcAdgDIo8R/x7OtuTVrAT9qehJt4zpOQ6XGRvmXrTl1mR3PmgAAAZ2a
vkWAAAAEAwBHMEUCIQDsHoBUCvx7jcRxWA962rCQC5Lm0nGpuUVmu3PPySCkjwIg
P64SbSkWuJVt0Pov7izzeUD2PdKt9tMGC3HYFHEU3iMAfQBGr4Y9Oz7ln6V33qgk
XTaw2e0ioiP0YXdBIpRS7pVQXwAAAZ2avkY5AAgAAAUABByQ6AQDAEYwRAIgPcu4
01rw3VkZ3HUK7yJuN+ITSyGoRJl174XPX8dztNUCIBg8FLvQksKWBTFKYVvOQEN2
joDgOh748gPgA3zedtStMAoGCCqGSM49BAMDA2gAMGUCMQC/qGFju5J53FGnn6c5
NxKOqSUwFs6r4k3tXxNa/xa0HjrlSm+1twq8cewj702WzMECMAKwit2DW3oe1Umy
8lnIfQiHFNZIB021+MdCDm1VvQXv1Jbx8PImXi+E9EPr7sValQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c
S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb
R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB
9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j
cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB
BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE
DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j
ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0
RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d
AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8
otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA
aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm
Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2
HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1
Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR
xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d
tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/
jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS
u1igv3OefnWjSQ==
-----END CERTIFICATE-----

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBpDCCAUqgAwIBAgIRAI/SRGy6bA6e4vvMIYZ1k0EwCgYIKoZIzj0EAwIwMDEu
MCwGA1UEAxMlQ2FkZHkgTG9jYWwgQXV0aG9yaXR5IC0gMjAyNiBFQ0MgUm9vdDAe
Fw0yNjA0MDYxMDI2MjlaFw0zNjAyMTMxMDI2MjlaMDAxLjAsBgNVBAMTJUNhZGR5
IExvY2FsIEF1dGhvcml0eSAtIDIwMjYgRUNDIFJvb3QwWTATBgcqhkjOPQIBBggq
hkjOPQMBBwNCAATtG4qugfT09Y6s2HqTrfx7ZVAnoms8z2J/0BgS+yvBTHMQr9bP
Dpl5eYMGEdzKKLuz7zFxAPt2FyPU/xf+R8/jo0UwQzAOBgNVHQ8BAf8EBAMCAQYw
EgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUM4CUTmtbdbVr77Owah55rj/W
IIswCgYIKoZIzj0EAwIDSAAwRQIhAOUB/yfaQjwV5bT8CqJmPFWXhr2Iv8p5DrsA
mlHPZjFgAiAnkIo4DPOoDqTNpfqPRHra01PZN4pXj/aT93GvHvDf6Q==
-----END CERTIFICATE-----

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 724 KiB

View file

@ -0,0 +1,13 @@
<?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>
<color name="main_orange">#FFFF6700</color>
<color name="light_orange">#FFFFAD00</color>
</resources>

View file

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

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.MyApp.Splash"
parent="Theme.Material3.DayNight.NoActionBar">
<!-- Splash 背景色 -->
<item name="android:windowSplashScreenBackground">
@color/black
</item>
<!-- Splash 中央 icon(Animated 或靜態都可以) -->
<item name="android:windowSplashScreenAnimatedIcon">
@drawable/tg_logo_zoom_v1
</item>
<!-- Splash 結束後切換到的主題(很重要) -->
<item name="postSplashScreenTheme">
@style/Theme.Login_V3
</item>
</style>
<style name="Theme.Login_V3" parent="Theme.Material3.DayNight.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,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">http://192.168.0.217:3000/</domain>
</domain-config>
</network-security-config>

View file

@ -0,0 +1,17 @@
package com.example.login_v3
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)
}
}

10
build.gradle.kts Normal file
View file

@ -0,0 +1,10 @@
// 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
// di
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
}

33
gradle.properties Normal file
View file

@ -0,0 +1,33 @@
# 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
#android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
#android.newDsl=false
android.dependency.excludeLibraryComponentsFromConstraints=true

View file

@ -0,0 +1,13 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e55dccbfe27cb97945148c61a39c89c5/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/dbd05c4936d573642f94cd149e1356c8/redirect
toolchainVendor=JETBRAINS
toolchainVersion=21

50
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,50 @@
[versions]
agp = "8.9.1"
kotlin = "2.0.21"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.3"
composeBom = "2024.09.00"
volley = "1.2.1"
foundation = "1.10.5"
composeAnimation = "1.10.5"
navigationCompose = "2.9.7"
# ... 其他維持原樣
hilt = "2.51.1"
# 新增一個 ksp 專用版本變數方便管理
ksp = "2.0.21-1.0.27"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
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-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "composeAnimation" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
volley = { group = "com.android.volley", name = "volley", version.ref = "volley" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.2.0" }
[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" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
# 這裡使用了上面定義的 ksp 版本
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

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

Binary file not shown.

View file

@ -0,0 +1,8 @@
#Fri Jan 30 22:01:00 CST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendored Normal file
View file

@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original 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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# 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 ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# 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
if ! command -v java >/dev/null 2>&1
then
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
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# 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"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View file

@ -0,0 +1,94 @@
@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
@rem SPDX-License-Identifier: Apache-2.0
@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=.
@rem This is normally unused
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% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 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!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

21
settings.gradle.kts Normal file
View file

@ -0,0 +1,21 @@
pluginManagement {
repositories {
google {}
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Login_V3"
include(":app")