2026-06-20 13:28:08 +08:00

370 lines
14 KiB
Kotlin

package com.jihe.punchnet.screen
import android.app.Activity.MODE_PRIVATE
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.core.content.ContextCompat
import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
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.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.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
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.platform.LocalContext
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.jihe.punchnet.PunchnetService
import com.jihe.punchnet.PunchnetServiceArgument
import com.jihe.punchnet.R
import com.jihe.punchnet.RouteInfo
import com.jihe.punchnet.data.ButtonRepository
import com.jihe.punchnet.data.ButtonState
import com.jihe.punchnet.data.ButtonViewModel
import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel
import com.jihe.punchnet.helper.PreferenceName
import com.jihe.punchnet.helper.getPreferenceString
import com.jihe.punchnet.helper.setPreferenceString
import kotlin.math.exp
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material.icons.filled.Person
import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.background
data class HomeDropDownInfo(
@DrawableRes val icon: Int,
val name: String,
val callback: (()->Unit)? = null,
)
@Composable
fun HomeDropdownMenu(navController: androidx.navigation.NavHostController) {
var expanded by remember { mutableStateOf(false) }
val context = LocalContext.current
val homeItems = arrayOf(
HomeDropDownInfo(
R.drawable.tag, // TODO: Use settings icon if available, or just standard icon
"设置",
{
navController.navigate(Screen.SettingsScreen.route)
}
),
HomeDropDownInfo(
R.drawable.tag,
"退出登录",
{
// Clear token and go to LoginScreen
setPreferenceString(context, PreferenceName.PreferenceToken, "")
navController.navigate(Screen.LoginScreen.route) {
popUpTo(Screen.MainScreen.route) { inclusive = true }
}
}
)
)
Box() {
IconButton(
interactionSource = remember { MutableInteractionSource() },
modifier = Modifier.indication(
interactionSource = remember { MutableInteractionSource() },
indication = null,
),
onClick = {
expanded = true
}
) {
Icon(
Icons.Default.Settings,
contentDescription = "settings",
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = {expanded = !expanded}
) {
homeItems.forEach { item ->
DropdownMenuItem(
onClick = {
expanded = false
item.callback?.invoke()
},
text = {
Row (
verticalAlignment = Alignment.CenterVertically
){
Icon(
painter = painterResource(item.icon),
modifier = Modifier.size(24.dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.onBackground
)
Spacer(
modifier = Modifier.padding(start = 8.dp)
)
Text(text=item.name)
}
}
)
}
}
}
}
@Composable
fun HomeScreen(
buttonViewModel: ButtonViewModel,
serviceViewModel: ServiceViewModel,
routeViewModel: RouteViewModel,
navController: androidx.navigation.NavHostController,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val tkPref = getPreferenceString(context, PreferenceName.PreferenceToken)
val allroutes = routeViewModel.allRoutes.collectAsState(emptyList())
val vpnLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {result ->
when(result.resultCode) {
RESULT_OK -> {
// serviceViewModel.startService(context)
// serviceViewModel.setVPNPermission(true)
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
}
RESULT_CANCELED -> {
Toast.makeText(context, "vpn permission denied", Toast.LENGTH_SHORT).show()
}
}
}
val notificationPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
if (!isGranted) {
Toast.makeText(context, "未授予通知权限,连接可能会在后台被杀", Toast.LENGTH_SHORT).show()
}
val intent = VpnService.prepare(context.applicationContext)
if (intent != null) {
vpnLauncher.launch(intent)
} else {
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
}
}
val buttonState = buttonViewModel.buttonState.collectAsState()
val resourceList by com.jihe.punchnet.data.NodeRepository.resources.collectAsState()
CustomHeaderScreen(
"",
onBack = null,
onMenu = {
HomeDropdownMenu(navController)
}
) {
LazyColumn(
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
Spacer(modifier = Modifier.height(50.dp))
Image(
painter = painterResource(R.drawable.punchnet_log),
contentDescription = "logo",
modifier = Modifier.size(150.dp)
)
Text(
"Connecting the Infinite",
fontSize = 30.sp,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top=24.dp)
)
Text(
"Welcome to PunchNet",
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(top=8.dp)
)
Button(
onClick = {
if (buttonViewModel.buttonState.value == ButtonState.ButtonStarted) {
serviceViewModel.stopVpnService(context)
} else if (buttonViewModel.buttonState.value == ButtonState.ButtonStopped){
if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
} else {
val intent = VpnService.prepare(context.applicationContext)
if (intent != null) {
vpnLauncher.launch(intent)
} else {
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
}
}
}
},
enabled = buttonState.value.enabled,
shape = RoundedCornerShape(10.dp),
modifier = Modifier.padding(top=48.dp)
.width(120.dp)
.height(40.dp)
) {
Text(buttonState.value.text)
}
Spacer(modifier = Modifier.height(48.dp))
}
if (resourceList.isNotEmpty()) {
item {
Text(
text = "企业资源 (${resourceList.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp, start = 24.dp).fillMaxWidth()
)
}
items(resourceList) { resource ->
Box(modifier = Modifier.padding(horizontal = 24.dp)) {
ResourceItemRow(resource, navController)
}
}
}
}
}
}
@Composable
fun ResourceItemRow(resource: com.jihe.punchnet.api.ResourceItem, navController: androidx.navigation.NavHostController) {
androidx.compose.material3.Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable {
navController.navigate(Screen.WebViewScreen.createRoute(resource.url))
},
colors = androidx.compose.material3.CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.tag),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(resource.name, fontWeight = FontWeight.Bold, fontSize = 16.sp)
Text(resource.url, fontSize = 14.sp, color = MaterialTheme.colorScheme.primary)
}
Box(
modifier = Modifier
.size(12.dp)
.background(
color = if (resource.connectionStatus == "connected") Color(0xFF4CAF50) else Color.Gray,
shape = androidx.compose.foundation.shape.CircleShape
)
)
}
}
}
@Composable
fun NodeItemRow(node: com.jihe.punchnet.api.NodeItem) {
androidx.compose.material3.Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
colors = androidx.compose.material3.CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
// Icon
val isMobile = node.system?.contains("Android", true) == true || node.system?.contains("iOS", true) == true
Icon(
imageVector = if (isMobile) Icons.Default.Phone else Icons.Default.Person,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.width(16.dp))
// Texts
Column(modifier = Modifier.weight(1f)) {
Text(node.name, fontWeight = FontWeight.Bold, fontSize = 16.sp)
Text(node.ip, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
// Status dot
Box(
modifier = Modifier
.size(12.dp)
.background(
color = if (node.connectionStatus == "connected") Color(0xFF4CAF50) else Color.Gray,
shape = androidx.compose.foundation.shape.CircleShape
)
)
}
}
}