370 lines
14 KiB
Kotlin
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
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|