feat(ui): 重新设计开放源代码许可页面,集成 M3 Expressive 设计与弹性动画
This commit is contained in:
parent
4f6dc2f36a
commit
7bf16dfe5a
@ -8,22 +8,91 @@ package plus.rua.project.ui
|
|||||||
*/
|
*/
|
||||||
data class LicenseItem(
|
data class LicenseItem(
|
||||||
val library: String,
|
val library: String,
|
||||||
val license: String
|
val license: String,
|
||||||
|
val description: String,
|
||||||
|
val category: String,
|
||||||
|
val url: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目使用的第三方库及其许可证列表。
|
* 项目使用的第三方库及其许可证列表。
|
||||||
*/
|
*/
|
||||||
val licenses = listOf(
|
val licenses = listOf(
|
||||||
LicenseItem("AndroidX Activity Compose", "Apache-2.0"),
|
LicenseItem(
|
||||||
LicenseItem("AndroidX CameraX", "Apache-2.0"),
|
library = "tyme4kt",
|
||||||
LicenseItem("AndroidX Lifecycle", "Apache-2.0"),
|
license = "MIT",
|
||||||
LicenseItem("AndroidX Media3 (ExoPlayer)", "Apache-2.0"),
|
description = "中国传统历法推演引擎,支持公历、农历、干支、节气及法定节假日推算。",
|
||||||
LicenseItem("AndroidX Room", "Apache-2.0"),
|
category = "农历与节气",
|
||||||
LicenseItem("Compose Material 3", "Apache-2.0"),
|
url = "https://github.com/6tail/tyme4kt"
|
||||||
LicenseItem("Kotlin", "Apache-2.0"),
|
),
|
||||||
LicenseItem("kotlinx-datetime", "Apache-2.0"),
|
LicenseItem(
|
||||||
LicenseItem("Sketch", "Apache-2.0"),
|
library = "kotlinx-datetime",
|
||||||
LicenseItem("tyme4kt", "MIT"),
|
license = "Apache-2.0",
|
||||||
LicenseItem("ZoomImage", "Apache-2.0"),
|
description = "Kotlin 官方 DateTime 日期时间计算库,取代传统 Java Calendar。",
|
||||||
|
category = "日期时间",
|
||||||
|
url = "https://github.com/Kotlin/kotlinx-datetime"
|
||||||
|
),
|
||||||
|
LicenseItem(
|
||||||
|
library = "Sketch",
|
||||||
|
license = "Apache-2.0",
|
||||||
|
description = "高效强大的图片加载与解码引擎,支持 WebP 动图扫描与帧动画播放。",
|
||||||
|
category = "图片处理",
|
||||||
|
url = "https://github.com/panpf/sketch"
|
||||||
|
),
|
||||||
|
LicenseItem(
|
||||||
|
library = "ZoomImage",
|
||||||
|
license = "Apache-2.0",
|
||||||
|
description = "日期记录器照片手势缩放、平移与双击查看手势缩放组件。",
|
||||||
|
category = "图片处理",
|
||||||
|
url = "https://github.com/panpf/zoomimage"
|
||||||
|
),
|
||||||
|
LicenseItem(
|
||||||
|
library = "AndroidX Room",
|
||||||
|
license = "Apache-2.0",
|
||||||
|
description = "日期记录器 (DateRecorder) 本地 SQLite 结构化数据持久化数据库。",
|
||||||
|
category = "数据存储",
|
||||||
|
url = "https://developer.android.com/training/data-storage/room"
|
||||||
|
),
|
||||||
|
LicenseItem(
|
||||||
|
library = "AndroidX CameraX",
|
||||||
|
license = "Apache-2.0",
|
||||||
|
description = "日期记录器内置相机拍照、镜头切换与图片捕捉与预览能力。",
|
||||||
|
category = "媒体与相机",
|
||||||
|
url = "https://developer.android.com/training/camerax"
|
||||||
|
),
|
||||||
|
LicenseItem(
|
||||||
|
library = "AndroidX Media3 (ExoPlayer)",
|
||||||
|
license = "Apache-2.0",
|
||||||
|
description = "小狗乐园彩蛋页面全屏视频循环播放的高性能媒体引擎。",
|
||||||
|
category = "媒体与相机",
|
||||||
|
url = "https://developer.android.com/media/media3"
|
||||||
|
),
|
||||||
|
LicenseItem(
|
||||||
|
library = "Compose Material 3",
|
||||||
|
license = "Apache-2.0",
|
||||||
|
description = "Material Design 3 现代化 UI 交互组件、动态配色与主题系统。",
|
||||||
|
category = "UI 框架",
|
||||||
|
url = "https://developer.android.com/jetpack/compose/design/material3"
|
||||||
|
),
|
||||||
|
LicenseItem(
|
||||||
|
library = "AndroidX Activity Compose",
|
||||||
|
license = "Apache-2.0",
|
||||||
|
description = "Jetpack Compose 与 Android Activity 生命周期及 Intent 桥接支持。",
|
||||||
|
category = "UI 框架",
|
||||||
|
url = "https://developer.android.com/jetpack/androidx/releases/activity"
|
||||||
|
),
|
||||||
|
LicenseItem(
|
||||||
|
library = "AndroidX Lifecycle",
|
||||||
|
license = "Apache-2.0",
|
||||||
|
description = "ViewModel 与 UI State 状态绑定及生命周期感知组合能力。",
|
||||||
|
category = "架构组件",
|
||||||
|
url = "https://developer.android.com/jetpack/androidx/releases/lifecycle"
|
||||||
|
),
|
||||||
|
LicenseItem(
|
||||||
|
library = "Kotlin",
|
||||||
|
license = "Apache-2.0",
|
||||||
|
description = "Kotlin 编程语言标准库与 Flow / Coroutine 异步协程框架。",
|
||||||
|
category = "核心语言",
|
||||||
|
url = "https://kotlinlang.org"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,27 +1,74 @@
|
|||||||
package plus.rua.project.ui
|
package plus.rua.project.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
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.fillMaxSize
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ChevronLeft
|
import androidx.compose.material.icons.filled.ChevronLeft
|
||||||
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
|
import androidx.compose.material.icons.outlined.Close
|
||||||
|
import androidx.compose.material.icons.outlined.Code
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material.icons.outlined.OpenInNew
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开放源代码许可页面,展示项目使用的第三方库及其许可证。
|
* 开放源代码许可页面,展示项目使用的第三方库及其许可证。
|
||||||
*
|
*
|
||||||
|
* 包含 header 汇总面板、分类与许可类型筛选 Chip、搜索栏以及支持弹性展开与弹性动画的列表。
|
||||||
|
*
|
||||||
* @param onBack 返回回调
|
* @param onBack 返回回调
|
||||||
* @param modifier 布局修饰符
|
* @param modifier 布局修饰符
|
||||||
*/
|
*/
|
||||||
@ -31,6 +78,27 @@ fun LicensesScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
var selectedFilter by remember { mutableStateOf("全部") }
|
||||||
|
|
||||||
|
val filterOptions = listOf("全部", "Apache-2.0", "MIT")
|
||||||
|
|
||||||
|
val filteredLicenses = remember(searchQuery, selectedFilter) {
|
||||||
|
licenses.filter { item ->
|
||||||
|
val matchesFilter = when (selectedFilter) {
|
||||||
|
"全部" -> true
|
||||||
|
else -> item.license.equals(selectedFilter, ignoreCase = true)
|
||||||
|
}
|
||||||
|
val matchesSearch = searchQuery.isBlank() ||
|
||||||
|
item.library.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
item.description.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
item.category.contains(searchQuery, ignoreCase = true)
|
||||||
|
|
||||||
|
matchesFilter && matchesSearch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@ -50,33 +118,386 @@ fun LicensesScreen(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding)
|
.padding(innerPadding),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(licenses) { item ->
|
item(key = "header_banner") {
|
||||||
Card(
|
LicenseHeroBanner(
|
||||||
colors = CardDefaults.cardColors(
|
totalCount = licenses.size,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
apacheCount = licenses.count { it.license == "Apache-2.0" },
|
||||||
),
|
mitCount = licenses.count { it.license == "MIT" },
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(key = "search_and_filters") {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
ListItem(
|
OutlinedTextField(
|
||||||
headlineContent = {
|
value = searchQuery,
|
||||||
Text(
|
onValueChange = { searchQuery = it },
|
||||||
text = item.library,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
style = MaterialTheme.typography.bodyLarge
|
placeholder = { Text("搜索开源库名称或用途...") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Search,
|
||||||
|
contentDescription = "搜索"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingIcon = {
|
||||||
|
if (searchQuery.isNotEmpty()) {
|
||||||
|
IconButton(onClick = { searchQuery = "" }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Close,
|
||||||
|
contentDescription = "清除搜索"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(filterOptions) { filter ->
|
||||||
|
val isSelected = selectedFilter == filter
|
||||||
|
FilterChip(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = { selectedFilter = filter },
|
||||||
|
label = { Text(filter) },
|
||||||
|
leadingIcon = if (isSelected) {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(FilterChipDefaults.IconSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredLicenses.isEmpty()) {
|
||||||
|
item(key = "empty_state") {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 32.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
text = item.license,
|
text = "未找到匹配的开源许可条目",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(
|
||||||
|
items = filteredLicenses,
|
||||||
|
key = { it.library }
|
||||||
|
) { item ->
|
||||||
|
LicenseItemCard(
|
||||||
|
item = item,
|
||||||
|
onOpenUrl = { url ->
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
Toast.makeText(context, "无法打开网页", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.animateItem(
|
||||||
|
placementSpec = spring(
|
||||||
|
stiffness = Spring.StiffnessLow,
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item(key = "bottom_space") {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LicenseHeroBanner(
|
||||||
|
totalCount: Int,
|
||||||
|
apacheCount: Int,
|
||||||
|
mitCount: Int,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Code,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "开源致谢",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "鸭鸭日历的诞生离不开以下优秀的开源组件与社区贡献者的无私奉献。",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.85f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
StatBadge(text = "$totalCount 个开源库")
|
||||||
|
StatBadge(text = "Apache-2.0 ($apacheCount)")
|
||||||
|
StatBadge(text = "MIT ($mitCount)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatBadge(text: String) {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.6f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LicenseItemCard(
|
||||||
|
item: LicenseItem,
|
||||||
|
onOpenUrl: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val rotationAngle by animateFloatAsState(
|
||||||
|
targetValue = if (expanded) 180f else 0f,
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
||||||
|
label = "expand_arrow_rotation"
|
||||||
|
)
|
||||||
|
|
||||||
|
val containerColor by animateColorAsState(
|
||||||
|
targetValue = if (expanded) {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
|
},
|
||||||
|
animationSpec = tween(durationMillis = 200),
|
||||||
|
label = "card_container_color"
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = containerColor),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { expanded = !expanded }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.animateContentSize(
|
||||||
|
animationSpec = spring(
|
||||||
|
stiffness = Spring.StiffnessLow,
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
CategoryBadge(category = item.category)
|
||||||
|
Text(
|
||||||
|
text = item.library,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = item.description,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = if (expanded) Int.MAX_VALUE else 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
LicenseBadge(license = item.license)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ExpandMore,
|
||||||
|
contentDescription = if (expanded) "折叠" else "展开",
|
||||||
|
modifier = Modifier.rotate(rotationAngle),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = expanded,
|
||||||
|
enter = fadeIn(animationSpec = tween(200)),
|
||||||
|
exit = fadeOut(animationSpec = tween(150))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(top = 12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
HorizontalDivider(
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "许可证声明 (${item.license})",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = when (item.license) {
|
||||||
|
"MIT" -> "授权人免费授予获得本软件和相关文档文件副本的任何人不受限制地处置本软件的权利,包括使用、复制、修改、合并、出版、发行、再许可和/或出售副本。"
|
||||||
|
else -> "根据 Apache License 2.0 许可授权。在遵循许可规定的前提下,允许自由使用、修改和分发代码,并保留版权与许可声明。"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.url != null) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { onOpenUrl(item.url) },
|
||||||
|
modifier = Modifier.align(Alignment.End),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.OpenInNew,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(text = "访问开源项目")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CategoryBadge(category: String) {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
shape = RoundedCornerShape(6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = category,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LicenseBadge(license: String) {
|
||||||
|
val containerColor = when (license) {
|
||||||
|
"MIT" -> MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
"Apache-2.0" -> MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
else -> MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
}
|
||||||
|
val contentColor = when (license) {
|
||||||
|
"MIT" -> MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
"Apache-2.0" -> MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
color = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = license,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -9,11 +9,16 @@ import kotlin.test.assertTrue
|
|||||||
class LicensesTest {
|
class LicensesTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `licenses list is non empty and contains valid library names and licenses`() {
|
fun `licenses list is non empty and contains valid fields`() {
|
||||||
assertTrue(licenses.isNotEmpty(), "许可列表不应为空")
|
assertTrue(licenses.isNotEmpty(), "许可列表不应为空")
|
||||||
licenses.forEach { item ->
|
licenses.forEach { item ->
|
||||||
assertTrue(item.library.isNotBlank(), "库名称不应为空")
|
assertTrue(item.library.isNotBlank(), "库名称不应为空")
|
||||||
assertTrue(item.license.isNotBlank(), "许可证名称不应为空")
|
assertTrue(item.license.isNotBlank(), "许可证名称不应为空")
|
||||||
|
assertTrue(item.description.isNotBlank(), "库描述不应为空")
|
||||||
|
assertTrue(item.category.isNotBlank(), "库分类不应为空")
|
||||||
|
if (item.url != null) {
|
||||||
|
assertTrue(item.url.startsWith("http://") || item.url.startsWith("https://"), "URL 应为有效 HTTP(S) 地址")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user