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 ) ) } } }