merge: resolve VideoPlayerScreen conflict with remote design system

Adopt the remote LumaDesignSystem redesign as the base and port back player
optimizations: real-time network speed (SpeedMonitor + onBandwidthEstimate,
900ms refresh), LoadControl buffering (100MB/600s/70% refill trigger, fixes OOM),
cache progress in LumaSeekBar (new bufferedValue/bufferColor), long-press 2x
speed with a capsule indicator, and a top status bar (time/network/battery).
This commit is contained in:
beyond committed 2026-08-28 02:25:59 +08:00
commit 129a2fb028
185 files changed
+7582 -2216

No files matched your search

+4 -5
View File
@@ -7,6 +7,10 @@ android {
namespace = "com.example.myapplication"
compileSdk = 35
buildFeatures {
buildConfig = true
}
defaultConfig {
applicationId = "com.example.myapplication"
minSdk = 24
@@ -41,7 +45,6 @@ dependencies {
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
implementation("io.coil-kt:coil-compose:2.7.0")
implementation("androidx.palette:palette-ktx:1.0.0")
implementation("androidx.activity:activity-compose:1.13.0")
@@ -49,12 +52,8 @@ dependencies {
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.1")
implementation("top.yukonga.miuix.kmp:miuix-ui:0.9.4-rc01")
implementation("top.yukonga.miuix.kmp:miuix-blur:0.9.4-rc01")
implementation("dev.chrisbanes.haze:haze:1.5.3")
implementation("io.github.kyant0:backdrop:2.0.1")
implementation("io.github.kyant0:shapes:1.2.1")
implementation("top.yukonga.miuix.kmp:miuix-squircle:0.9.4-rc01")
implementation("top.yukonga.miuix.kmp:miuix-icons:0.9.4-rc01")
implementation("androidx.room:room-runtime:2.8.4")
implementation("androidx.room:room-ktx:2.8.4")
implementation("androidx.media3:media3-exoplayer:1.5.1")
@@ -8,6 +8,8 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.example.myapplication.data.IconLibraryRepository
import com.example.myapplication.data.ServerRepository
import com.example.myapplication.ui.AppLanguage
import com.example.myapplication.ui.AppTheme
import com.example.myapplication.ui.design.LumaGlassRuntime
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -20,12 +22,19 @@ class MainActivity : ComponentActivity() {
// Initialize on main thread (these are fast SharedPreferences/DB reads)
AppLanguage.init(this)
AppTheme.init(this)
IconLibraryRepository.init(this)
ServerRepository.init(this)
LumaGlassRuntime.startFrameMonitor()
dataReady = true
setContent {
MainApp()
}
}
override fun onDestroy() {
LumaGlassRuntime.stopFrameMonitor()
super.onDestroy()
}
}
@@ -1,5 +1,6 @@
package com.example.myapplication
import android.app.Activity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn
@@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -25,10 +27,14 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.core.view.WindowCompat
import com.example.myapplication.R as Res
import com.example.myapplication.data.EmbyRepository
import com.example.myapplication.data.EmbyServer
import com.example.myapplication.data.IconLibrary
@@ -41,8 +47,10 @@ import com.example.myapplication.navigation.rootTabIndex
import com.example.myapplication.ui.AppTheme
import com.example.myapplication.ui.LiquidAppBottomBar
import com.example.myapplication.ui.LocalGlassBackdrop
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.rememberLumaLayerBackdrop
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.theme.MiuixTheme
@@ -87,6 +95,15 @@ fun MainApp() {
if (!showStartScreen) {
MiuixTheme {
val view = LocalView.current
val isDark = AppTheme.isDark
SideEffect {
(view.context as? Activity)?.window?.let { window ->
val controller = WindowCompat.getInsetsController(window, view)
controller.isAppearanceLightStatusBars = !isDark
controller.isAppearanceLightNavigationBars = !isDark
}
}
val backStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = backStackEntry?.destination?.route
var lastRootTab by rememberSaveable { mutableIntStateOf(0) }
@@ -96,8 +113,19 @@ fun MainApp() {
}
val selectedTab = rootTab ?: lastRootTab
val showBottomBar = isRootRoute(currentRoute)
val glassBackdrop = rememberLayerBackdrop()
val bgColor = if (AppTheme.isDark) Color(0xFF0F1115) else Color(0xFFF2F4F7)
val glassBackdrop = rememberLumaLayerBackdrop()
val loginFailedMessage = stringResource(Res.string.error_login_failed)
val networkErrorMessage = stringResource(
Res.string.error_network,
stringResource(Res.string.error_unknown),
)
val backgroundBrush = Brush.linearGradient(
listOf(
LumaColors.background,
LumaColors.backgroundElevated,
LumaColors.background,
),
)
val selectServer: (String) -> Unit = { id ->
selectedServerId = id
@@ -112,10 +140,10 @@ fun MainApp() {
if (selectedServerId.isBlank()) selectedServerId = server.id
onResult(null)
} else {
onResult(authResult.errorMessage ?: "登录失败")
onResult(authResult.errorMessage ?: loginFailedMessage)
}
} catch (e: Exception) {
onResult("网络错误: " + (e.message ?: "未知"))
onResult(networkErrorMessage)
}
}
}
@@ -127,10 +155,10 @@ fun MainApp() {
ServerRepository.updateServer(server.copy(apiKey = authResult.accessToken, userId = authResult.userId))
onResult(null)
} else {
onResult(authResult.errorMessage ?: "登录失败")
onResult(authResult.errorMessage ?: loginFailedMessage)
}
} catch (e: Exception) {
onResult("网络错误: " + (e.message ?: "未知"))
onResult(networkErrorMessage)
}
}
}
@@ -145,8 +173,12 @@ fun MainApp() {
}
androidx.compose.runtime.CompositionLocalProvider(LocalGlassBackdrop provides glassBackdrop) {
Box(Modifier.fillMaxSize().background(bgColor)) {
Box(Modifier.fillMaxSize().layerBackdrop(glassBackdrop)) {
Box(Modifier.fillMaxSize().background(backgroundBrush)) {
Box(
Modifier
.fillMaxSize()
.then(if (showBottomBar && glassBackdrop != null) Modifier.layerBackdrop(glassBackdrop) else Modifier),
) {
androidx.compose.runtime.CompositionLocalProvider(LocalGlassBackdrop provides null) {
AppNavHost(
navController = navController,
@@ -125,10 +125,12 @@ data class EmbyMediaStream(
data class EmbyPlaybackInfoResponse(
@SerializedName("MediaSources") val mediaSources: List<EmbyMediaSource>? = null,
@SerializedName("PlaySessionId") val playSessionId: String? = null,
)
data class EmbyMediaSource(
@SerializedName("Id") val id: String,
@SerializedName("PlaySessionId") val playSessionId: String? = null,
@SerializedName("Path") val path: String? = null,
@SerializedName("Container") val container: String? = null,
@SerializedName("SupportsDirectPlay") val supportsDirectPlay: Boolean = false,
@@ -1,15 +1,21 @@
package com.example.myapplication.data
import android.net.Uri
import android.os.SystemClock
import android.util.Log
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import kotlin.math.abs
import java.util.UUID
import java.util.concurrent.TimeUnit
@@ -26,6 +32,13 @@ data class EmbyLibraryPage(
val totalCount: Int = 0,
)
data class EmbyPlaybackSession(
val streamUrl: String,
val mediaSourceId: String,
val playSessionId: String,
val playMethod: String,
)
object EmbyRepository {
private const val CLIENT_NAME = "Luma Player"
@@ -39,6 +52,49 @@ object EmbyRepository {
.writeTimeout(15, TimeUnit.SECONDS)
.build()
private sealed interface PlaybackCommand {
val server: EmbyServer
val item: EmbyItem
val positionMs: Long
val session: EmbyPlaybackSession?
data class Started(
override val server: EmbyServer,
override val item: EmbyItem,
override val positionMs: Long,
override val session: EmbyPlaybackSession?,
) : PlaybackCommand
data class Progress(
override val server: EmbyServer,
override val item: EmbyItem,
override val positionMs: Long,
val paused: Boolean,
val eventName: String,
override val session: EmbyPlaybackSession?,
) : PlaybackCommand
data class Stopped(
override val server: EmbyServer,
override val item: EmbyItem,
override val positionMs: Long,
override val session: EmbyPlaybackSession?,
) : PlaybackCommand
}
private data class PlaybackReportState(
var lastPositionMs: Long = -1L,
var lastSentAtMs: Long = 0L,
)
private val playbackQueue = Channel<PlaybackCommand>(Channel.UNLIMITED)
private val playbackReportState = mutableMapOf<String, PlaybackReportState>()
private val playbackWorker = CoroutineScope(SupervisorJob() + Dispatchers.IO).apply {
launch {
for (command in playbackQueue) processPlaybackCommand(command)
}
}
/**
* Authenticate with an Emby server using username + password.
*/
@@ -378,7 +434,7 @@ object EmbyRepository {
}
}
suspend fun resolvePlaybackUrl(server: EmbyServer, item: EmbyItem): String? = withContext(Dispatchers.IO) {
suspend fun resolvePlaybackSession(server: EmbyServer, item: EmbyItem): EmbyPlaybackSession? = withContext(Dispatchers.IO) {
try {
val infoUrl = "${server.fullUrl}/emby/Items/${item.id}/PlaybackInfo?UserId=${server.userId}"
val body = "{}".toRequestBody("application/json".toMediaType())
@@ -388,30 +444,37 @@ object EmbyRepository {
.post(body)
.build()
val mediaSource = client.newCall(request).execute().use { response ->
val playbackInfo = client.newCall(request).execute().use { response ->
if (!response.isSuccessful) null
else {
val responseBody = response.body?.string().orEmpty()
gson.fromJson(responseBody, EmbyPlaybackInfoResponse::class.java)
?.mediaSources?.firstOrNull()
}
}
val mediaSource = playbackInfo?.mediaSources?.firstOrNull()
val mediaSourceId = mediaSource?.id ?: item.id
val playSessionId = mediaSource?.playSessionId
?.takeUnless { it.isBlank() }
?: playbackInfo?.playSessionId?.takeUnless { it.isBlank() }
?: UUID.randomUUID().toString().replace("-", "")
val transcodingUrl = mediaSource?.transcodingUrl
if (!transcodingUrl.isNullOrBlank()) {
return@withContext authenticatedPlaybackUrl(server, transcodingUrl)
}
val directStreamUrl = mediaSource?.directStreamUrl
if (!directStreamUrl.isNullOrBlank()) {
return@withContext authenticatedPlaybackUrl(server, directStreamUrl)
val (candidateUrl, playMethod) = when {
!transcodingUrl.isNullOrBlank() -> transcodingUrl to "Transcode"
!directStreamUrl.isNullOrBlank() -> directStreamUrl to "DirectStream"
else -> {
"${server.fullUrl}/emby/Videos/${item.id}/stream" +
"?Static=true&MediaSourceId=$mediaSourceId&DeviceId=$deviceId&PlaySessionId=$playSessionId" to
"DirectStream"
}
}
val sourceId = mediaSource?.id ?: item.id
authenticatedPlaybackUrl(
server,
"${server.fullUrl}/emby/Videos/${item.id}/stream" +
"?Static=true&MediaSourceId=$sourceId&DeviceId=$deviceId",
EmbyPlaybackSession(
streamUrl = authenticatedPlaybackUrl(server, candidateUrl, playSessionId),
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
playMethod = playMethod,
)
} catch (e: Exception) {
Log.e("EmbyPlayback", "resolvePlaybackUrl failed", e)
@@ -419,21 +482,41 @@ object EmbyRepository {
}
}
suspend fun reportPlaybackStarted(server: EmbyServer, item: EmbyItem, positionMs: Long) {
suspend fun resolvePlaybackUrl(server: EmbyServer, item: EmbyItem): String? =
resolvePlaybackSession(server, item)?.streamUrl
suspend fun reportPlaybackStarted(
server: EmbyServer,
item: EmbyItem,
positionMs: Long,
session: EmbyPlaybackSession? = null,
) {
sendPlaybackCheckIn(
server = server,
item = item,
positionMs = positionMs,
paused = false,
endpoint = "/emby/Sessions/Playing",
session = session,
)
}
fun enqueuePlaybackStarted(
server: EmbyServer,
item: EmbyItem,
positionMs: Long,
session: EmbyPlaybackSession?,
) {
playbackQueue.trySend(PlaybackCommand.Started(server, item, positionMs, session))
}
suspend fun reportPlaybackProgress(
server: EmbyServer,
item: EmbyItem,
positionMs: Long,
paused: Boolean,
eventName: String = if (paused) "Pause" else "TimeUpdate",
session: EmbyPlaybackSession? = null,
) {
sendPlaybackCheckIn(
server = server,
@@ -441,20 +524,109 @@ object EmbyRepository {
positionMs = positionMs,
paused = paused,
endpoint = "/emby/Sessions/Playing/Progress",
eventName = if (paused) "Pause" else "TimeUpdate",
eventName = eventName,
session = session,
)
}
suspend fun reportPlaybackStopped(server: EmbyServer, item: EmbyItem, positionMs: Long) {
fun enqueuePlaybackProgress(
server: EmbyServer,
item: EmbyItem,
positionMs: Long,
paused: Boolean,
eventName: String,
session: EmbyPlaybackSession?,
) {
playbackQueue.trySend(
PlaybackCommand.Progress(server, item, positionMs, paused, eventName, session),
)
}
suspend fun reportPlaybackStopped(
server: EmbyServer,
item: EmbyItem,
positionMs: Long,
session: EmbyPlaybackSession? = null,
) {
sendPlaybackCheckIn(
server = server,
item = item,
positionMs = positionMs,
paused = true,
endpoint = "/emby/Sessions/Playing/Stopped",
session = session,
)
}
fun enqueuePlaybackStopped(
server: EmbyServer,
item: EmbyItem,
positionMs: Long,
session: EmbyPlaybackSession?,
) {
playbackQueue.trySend(PlaybackCommand.Stopped(server, item, positionMs, session))
}
private suspend fun processPlaybackCommand(command: PlaybackCommand) {
val key = playbackSessionKey(command.server, command.item, command.session)
when (command) {
is PlaybackCommand.Started -> {
sendPlaybackCheckIn(
server = command.server,
item = command.item,
positionMs = command.positionMs,
paused = false,
endpoint = "/emby/Sessions/Playing",
session = command.session,
)
playbackReportState[key] = PlaybackReportState(
lastPositionMs = command.positionMs.coerceAtLeast(0L),
lastSentAtMs = SystemClock.elapsedRealtime(),
)
}
is PlaybackCommand.Progress -> {
val now = SystemClock.elapsedRealtime()
val state = playbackReportState.getOrPut(key) { PlaybackReportState() }
val isUserInteraction = command.eventName != "TimeUpdate"
val positionChangedEnough = state.lastPositionMs < 0L ||
abs(command.positionMs - state.lastPositionMs) >= PLAYBACK_REPORT_INTERVAL_MS
val intervalElapsed = now - state.lastSentAtMs >= PLAYBACK_REPORT_INTERVAL_MS
if (!isUserInteraction && !positionChangedEnough && !intervalElapsed) return
sendPlaybackCheckIn(
server = command.server,
item = command.item,
positionMs = command.positionMs,
paused = command.paused,
endpoint = "/emby/Sessions/Playing/Progress",
eventName = command.eventName,
session = command.session,
)
state.lastPositionMs = command.positionMs.coerceAtLeast(0L)
state.lastSentAtMs = now
}
is PlaybackCommand.Stopped -> {
sendPlaybackCheckIn(
server = command.server,
item = command.item,
positionMs = command.positionMs,
paused = true,
endpoint = "/emby/Sessions/Playing/Stopped",
session = command.session,
)
playbackReportState.remove(key)
}
}
}
private fun playbackSessionKey(
server: EmbyServer,
item: EmbyItem,
session: EmbyPlaybackSession?,
): String = listOf(server.id, server.userId, item.id, session?.playSessionId.orEmpty()).joinToString("|")
private suspend fun sendPlaybackCheckIn(
server: EmbyServer,
item: EmbyItem,
@@ -462,6 +634,7 @@ object EmbyRepository {
paused: Boolean,
endpoint: String,
eventName: String? = null,
session: EmbyPlaybackSession? = null,
) = withContext(Dispatchers.IO) {
try {
val payload = linkedMapOf<String, Any>(
@@ -469,8 +642,15 @@ object EmbyRepository {
"PositionTicks" to positionMs.coerceAtLeast(0L) * 10_000L,
"IsPaused" to paused,
"CanSeek" to true,
"QueueableMediaTypes" to "Video",
"QueueableMediaTypes" to listOf("Video"),
"MediaSourceId" to (session?.mediaSourceId ?: item.id),
"PlaySessionId" to (session?.playSessionId ?: deviceId),
"IsMuted" to false,
"VolumeLevel" to 100,
"PlayMethod" to (session?.playMethod ?: "DirectStream"),
"PlaybackRate" to 1.0,
)
item.runTimeTicks?.let { payload["RunTimeTicks"] = it }
eventName?.let { payload["EventName"] = it }
val request = Request.Builder()
.url(server.fullUrl + endpoint)
@@ -483,14 +663,21 @@ object EmbyRepository {
}
}
private fun authenticatedPlaybackUrl(server: EmbyServer, candidate: String): String {
private fun authenticatedPlaybackUrl(
server: EmbyServer,
candidate: String,
playSessionId: String? = null,
): String {
val absoluteUrl = if (candidate.startsWith("/")) server.fullUrl + candidate else candidate
val uri = Uri.parse(absoluteUrl)
return if (uri.getQueryParameter("api_key").isNullOrBlank()) {
uri.buildUpon().appendQueryParameter("api_key", server.apiKey).build().toString()
} else {
absoluteUrl
val builder = uri.buildUpon()
if (uri.getQueryParameter("api_key").isNullOrBlank()) {
builder.appendQueryParameter("api_key", server.apiKey)
}
if (!playSessionId.isNullOrBlank() && uri.getQueryParameter("PlaySessionId").isNullOrBlank()) {
builder.appendQueryParameter("PlaySessionId", playSessionId)
}
return builder.build().toString()
}
fun buildBackdropUrl(serverUrl: String, item: EmbyItem, maxWidth: Int = 1200): String? {
@@ -546,4 +733,6 @@ object EmbyRepository {
val path = item.posterUrlPath ?: return null
return "$serverUrl$path"
}
private const val PLAYBACK_REPORT_INTERVAL_MS = 10_000L
}
@@ -1,38 +0,0 @@
package com.example.myapplication.model
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import com.example.myapplication.R as Res
data class MediaItem(
val title: String,
val subtitle: String,
val posterColors: List<Color>,
val rating: String = "",
)
@Composable
fun sampleMovies(): List<MediaItem> = listOf(
MediaItem(stringResource(Res.string.movie_interstellar), stringResource(Res.string.genre_sci_fi_adventure), listOf(Color(0xFF6A5BEF), Color(0xFF3A2BBF)), "9.4"),
MediaItem(stringResource(Res.string.movie_inception), stringResource(Res.string.genre_sci_fi_action), listOf(Color(0xFF42A5F5), Color(0xFF1B5E20)), "9.3"),
MediaItem(stringResource(Res.string.movie_matrix), stringResource(Res.string.genre_sci_fi_action), listOf(Color(0xFF2E7D32), Color(0xFF004D40)), "9.0"),
MediaItem(stringResource(Res.string.movie_titanic), stringResource(Res.string.genre_romance_disaster), listOf(Color(0xFFE53935), Color(0xFF6A1B9A)), "9.4"),
MediaItem(stringResource(Res.string.movie_avatar), stringResource(Res.string.genre_sci_fi_adventure), listOf(Color(0xFF00897B), Color(0xFF01579B)), "8.7"),
MediaItem(stringResource(Res.string.movie_dune), stringResource(Res.string.genre_sci_fi_adventure), listOf(Color(0xFFE65100), Color(0xFFBF360C)), "8.8"),
MediaItem(stringResource(Res.string.movie_tenet), stringResource(Res.string.genre_sci_fi_mystery), listOf(Color(0xFF37474F), Color(0xFF263238)), "8.6"),
MediaItem(stringResource(Res.string.movie_batman), stringResource(Res.string.genre_action_crime), listOf(Color(0xFF212121), Color(0xFF311B92)), "8.5"),
MediaItem(stringResource(Res.string.movie_blade_runner), stringResource(Res.string.genre_sci_fi_mystery), listOf(Color(0xFFEF6C00), Color(0xFF4E342E)), "8.4"),
MediaItem(stringResource(Res.string.movie_arrival), stringResource(Res.string.genre_sci_fi_drama), listOf(Color(0xFF5C6BC0), Color(0xFF1A237E)), "8.3"),
MediaItem(stringResource(Res.string.movie_martian), stringResource(Res.string.genre_sci_fi_adventure), listOf(Color(0xFFC62828), Color(0xFFE65100)), "8.4"),
MediaItem(stringResource(Res.string.movie_gravity), stringResource(Res.string.genre_sci_fi_thriller), listOf(Color(0xFF0D47A1), Color(0xFF000000)), "7.7"),
)
@Composable
fun sampleTvShows(): List<MediaItem> = listOf(
MediaItem(stringResource(Res.string.tv_black_mirror), stringResource(Res.string.genre_sci_fi_drama), listOf(Color(0xFF424242), Color(0xFF212121)), "9.0"),
MediaItem(stringResource(Res.string.tv_game_of_thrones), stringResource(Res.string.genre_fantasy_adventure), listOf(Color(0xFF1B5E20), Color(0xFFBF360C)), "9.3"),
MediaItem(stringResource(Res.string.tv_breaking_bad), stringResource(Res.string.genre_crime_drama), listOf(Color(0xFFF9A825), Color(0xFF1B5E20)), "9.5"),
MediaItem(stringResource(Res.string.tv_stranger_things), stringResource(Res.string.genre_sci_fi_horror), listOf(Color(0xFFB71C1C), Color(0xFF000000)), "8.7"),
MediaItem(stringResource(Res.string.tv_mandalorian), stringResource(Res.string.genre_sci_fi_adventure), listOf(Color(0xFF455A64), Color(0xFFE65100)), "8.8"),
)
@@ -5,28 +5,29 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.background
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.example.myapplication.data.EmbyItem
import com.example.myapplication.data.EmbyPlaybackSession
import com.example.myapplication.data.EmbyRepository
import com.example.myapplication.data.EmbyServer
import com.example.myapplication.data.IconItem
import com.example.myapplication.data.IconLibrary
import com.example.myapplication.R as Res
import com.example.myapplication.screens.AboutScreen
import com.example.myapplication.screens.AddServerScreen
import com.example.myapplication.screens.CacheManagementScreen
@@ -41,7 +42,8 @@ import com.example.myapplication.screens.SearchTopBar
import com.example.myapplication.screens.SettingsScreen
import com.example.myapplication.screens.SettingsTopBar
import com.example.myapplication.screens.VideoPlayerScreen
import kotlinx.coroutines.launch
import com.example.myapplication.ui.design.LumaLoading
import kotlinx.coroutines.CancellationException
import top.yukonga.miuix.kmp.basic.Text
@OptIn(ExperimentalAnimationApi::class)
@@ -243,10 +245,16 @@ private fun MediaDetailDestination(
LaunchedEffect(server?.id, itemId) {
loading = true
item = if (server == null || server.apiKey.isBlank() || itemId.isBlank()) {
item = try {
if (server == null || server.apiKey.isBlank() || itemId.isBlank()) {
null
} else {
EmbyRepository.getItem(server, itemId)
}
} catch (error: CancellationException) {
throw error
} catch (_: Exception) {
null
} else {
EmbyRepository.getItem(server, itemId)
}
loading = false
}
@@ -254,7 +262,7 @@ private fun MediaDetailDestination(
when {
item != null -> MediaDetailScreen(item = item!!, server = server, onClose = onClose, onPlay = onPlay)
loading -> DestinationLoading()
else -> DestinationMessage("详情加载失败", background = Color.Transparent)
else -> DestinationMessage(stringResource(Res.string.detail_load_failed), background = Color.Transparent)
}
}
@@ -267,30 +275,43 @@ private fun PlayerDestination(
seasonId: String?,
onClose: () -> Unit,
) {
val playbackScope = rememberCoroutineScope()
val playbackUrlFailedMessage = stringResource(Res.string.playback_url_failed)
var item by remember(server?.id, itemId) { mutableStateOf<EmbyItem?>(null) }
var itemLoading by remember(server?.id, itemId) { mutableStateOf(true) }
var episodes by remember(server?.id, seriesId, seasonId) { mutableStateOf<List<EmbyItem>>(emptyList()) }
var selectedItem by remember(server?.id, itemId) { mutableStateOf<EmbyItem?>(null) }
var playbackUrl by remember(server?.id, itemId) { mutableStateOf<String?>(null) }
var playbackSession by remember(server?.id, itemId) { mutableStateOf<EmbyPlaybackSession?>(null) }
var playbackLoading by remember(server?.id, itemId) { mutableStateOf(false) }
var playbackError by remember(server?.id, itemId) { mutableStateOf<String?>(null) }
LaunchedEffect(server?.id, itemId) {
itemLoading = true
item = if (server == null || server.apiKey.isBlank() || itemId.isBlank()) {
item = try {
if (server == null || server.apiKey.isBlank() || itemId.isBlank()) {
null
} else {
EmbyRepository.getItem(server, itemId)
}
} catch (error: CancellationException) {
throw error
} catch (_: Exception) {
null
} else {
EmbyRepository.getItem(server, itemId)
}
itemLoading = false
}
LaunchedEffect(server?.id, seriesId, seasonId) {
episodes = if (server == null || seriesId.isNullOrBlank() || seasonId.isNullOrBlank()) {
episodes = try {
if (server == null || seriesId.isNullOrBlank() || seasonId.isNullOrBlank()) {
emptyList()
} else {
EmbyRepository.getEpisodes(server, seriesId, seasonId)
}
} catch (error: CancellationException) {
throw error
} catch (_: Exception) {
emptyList()
} else {
EmbyRepository.getEpisodes(server, seriesId, seasonId)
}
}
@@ -299,9 +320,18 @@ private fun PlayerDestination(
if (server == null || server.apiKey.isBlank() || target == null) return@LaunchedEffect
playbackLoading = true
playbackError = null
playbackUrl = EmbyRepository.resolvePlaybackUrl(server, target)
try {
playbackSession = EmbyRepository.resolvePlaybackSession(server, target)
playbackUrl = playbackSession?.streamUrl
} catch (error: CancellationException) {
throw error
} catch (_: Exception) {
playbackSession = null
playbackUrl = null
playbackError = playbackUrlFailedMessage
}
playbackLoading = false
if (playbackUrl.isNullOrBlank()) playbackError = "获取播放地址失败"
if (playbackUrl.isNullOrBlank()) playbackError = playbackUrlFailedMessage
}
when {
@@ -324,28 +354,29 @@ private fun PlayerDestination(
onClose = onClose,
onPlaybackStarted = { positionMs ->
server?.let { currentServer ->
playbackScope.launch {
EmbyRepository.reportPlaybackStarted(currentServer, target, positionMs)
}
EmbyRepository.enqueuePlaybackStarted(currentServer, target, positionMs, playbackSession)
}
},
onPlaybackProgress = { positionMs, paused ->
onPlaybackProgress = { positionMs, paused, eventName ->
server?.let { currentServer ->
playbackScope.launch {
EmbyRepository.reportPlaybackProgress(currentServer, target, positionMs, paused)
}
EmbyRepository.enqueuePlaybackProgress(
currentServer,
target,
positionMs,
paused,
eventName,
playbackSession,
)
}
},
onPlaybackStopped = { positionMs ->
server?.let { currentServer ->
playbackScope.launch {
EmbyRepository.reportPlaybackStopped(currentServer, target, positionMs)
}
EmbyRepository.enqueuePlaybackStopped(currentServer, target, positionMs, playbackSession)
}
},
)
itemLoading -> DestinationLoading(background = Color.Black)
else -> DestinationMessage("播放资源加载失败", background = Color.Black)
else -> DestinationMessage(stringResource(Res.string.playback_resource_failed), background = Color.Black)
}
}
@@ -354,7 +385,7 @@ private fun DestinationLoading(background: Color = Color.Transparent) = Box(
Modifier.fillMaxSize().background(background),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(color = Color(0xFF3B82F6))
LumaLoading()
}
@Composable
@@ -8,20 +8,20 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.navigation.NavBackStackEntry
import com.example.myapplication.ui.design.LumaMotion
object NavigationMotionTokens {
const val detailPushDuration = 320
const val detailPopDuration = 280
const val tabDuration = 250
const val detailPushDuration = LumaMotion.page
const val detailPopDuration = LumaMotion.pagePop
const val tabDuration = LumaMotion.standard
const val detailParallax = 0.20f
const val tabOffset = 0.25f
val pushEasing = CubicBezierEasing(0.20f, 0.82f, 0.24f, 1f)
val popEasing = CubicBezierEasing(0.32f, 0f, 0.68f, 1f)
val tabEasing = CubicBezierEasing(0.22f, 0.78f, 0.28f, 1f)
val pushEasing = LumaMotion.enter
val popEasing = LumaMotion.exit
val tabEasing = LumaMotion.native
}
fun AnimatedContentTransitionScope<NavBackStackEntry>.appEnterTransition(): EnterTransition {
@@ -31,12 +31,6 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.Storage
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.runtime.Composable
@@ -60,7 +54,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.example.myapplication.R as Res
import com.example.myapplication.data.EmbyItem
@@ -73,6 +66,15 @@ import com.example.myapplication.ui.PosterImmersivePalette
import com.example.myapplication.ui.rememberPosterImmersiveBackground
import com.example.myapplication.ui.LiquidGlassTopBar
import com.example.myapplication.ui.bottomBarContentPadding
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaAsyncImage
import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaErrorState
import com.example.myapplication.ui.design.LumaSkeleton
import com.example.myapplication.ui.design.LumaMotion
import com.example.myapplication.ui.design.LumaShapes
import com.example.myapplication.ui.design.LumaSpacing
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
@@ -88,15 +90,10 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.TextStyle
import com.example.myapplication.ui.AppTheme
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
import androidx.compose.runtime.mutableIntStateOf
/* ---- Adaptive content colors for the immersive home background ---- */
@@ -104,6 +101,42 @@ val LocalHomeContentColor = staticCompositionLocalOf { Color.Unspecified }
val LocalHomeSecondaryColor = staticCompositionLocalOf { Color.Unspecified }
val LocalHomeAccentColor = staticCompositionLocalOf { Color.Unspecified }
private data class HomeSnapshot(
val libraries: List<EmbyMediaFolder>,
val resumeItems: List<EmbyItem>,
val latestItems: List<EmbyItem>,
)
private object HomeContentCache {
private val snapshots = mutableMapOf<String, HomeSnapshot>()
private val libraryPages = mutableMapOf<String, EmbyLibraryPage>()
@Synchronized
fun snapshot(key: String): HomeSnapshot? = snapshots[key]
@Synchronized
fun putSnapshot(key: String, value: HomeSnapshot) {
snapshots[key] = value
}
@Synchronized
fun libraryPage(key: String, libraryId: String): EmbyLibraryPage? = libraryPages["$key|$libraryId"]
@Synchronized
fun putLibraryPage(key: String, libraryId: String, value: EmbyLibraryPage) {
libraryPages["$key|$libraryId"] = value
}
@Synchronized
fun invalidate(key: String) {
snapshots.remove(key)
libraryPages.keys.removeAll { it.startsWith("$key|") }
}
}
private fun homeCacheKey(server: EmbyServer): String =
listOf(server.id, server.fullUrl, server.userId).joinToString("|")
@Composable
private fun homeContentColor(): Color {
val value = LocalHomeContentColor.current
@@ -142,10 +175,10 @@ fun HomeTopBar(modifier: Modifier = Modifier) {
.background(Brush.linearGradient(listOf(Color(0xFF6EA8FF), Color(0xFF9B6BFF)))),
contentAlignment = Alignment.Center,
) {
Icon(imageVector = Icons.Outlined.Person, contentDescription = null, tint = Color.White, modifier = Modifier.size(22.dp))
LumaIconView(LumaIcon.User, contentDescription = null, tint = Color.White, modifier = Modifier.size(22.dp))
}
Column(Modifier.padding(start = 10.dp)) {
Text(text = "媒体中心", color = GlassTokens.text, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
Text(text = stringResource(Res.string.home_media_center), color = GlassTokens.text, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
}
}
}
@@ -178,7 +211,7 @@ private fun HeroCarousel(
1f to immersiveBackground.pageTop.copy(alpha = 0.68f),
)
)
.padding(bottom = 10.dp)
.padding(bottom = LumaSpacing.sm)
) {
HorizontalPager(
state = pagerState,
@@ -220,7 +253,16 @@ private fun HeroCarousel(
Spacer(Modifier.width(10.dp))
}
Text(
text = listOfNotNull(current.type?.let { if (it == "Series") "剧集" else if (it == "Movie") "电影" else it }, current.genres.take(2).joinToString(" / ").takeIf { it.isNotBlank() }).joinToString(" · "),
text = listOfNotNull(
current.type?.let {
when (it) {
"Series" -> stringResource(Res.string.detail_type_series)
"Movie" -> stringResource(Res.string.detail_type_movie)
else -> it
}
},
current.genres.take(2).joinToString(" / ").takeIf { it.isNotBlank() },
).joinToString(" · "),
color = homeSecondaryColor(),
fontSize = 12.sp,
maxLines = 1,
@@ -240,7 +282,7 @@ private fun HeroCarousel(
}
}
}
Spacer(Modifier.height(10.dp))
Spacer(Modifier.height(LumaSpacing.sm))
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
@@ -286,7 +328,7 @@ private fun FeaturedItemBanner(
.background(Color.Black),
) {
if (imageUrl != null) {
AsyncImage(
LumaAsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).crossfade(true).build(),
contentDescription = item.name,
contentScale = ContentScale.Crop,
@@ -312,7 +354,7 @@ private fun FeaturedItemBanner(
Column(Modifier.align(Alignment.BottomStart).padding(start = 18.dp, end = 18.dp, bottom = 24.dp)) {
val logoUrl = item.getLogoUrl(serverUrl)
if (logoUrl != null) {
AsyncImage(
LumaAsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(logoUrl).crossfade(true).build(),
contentDescription = null,
contentScale = ContentScale.Fit,
@@ -348,12 +390,12 @@ private fun MediaPoster(
Column(modifier.width(115.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Box(
Modifier.fillMaxWidth().height(165.dp).clip(RoundedCornerShape(12.dp))
Modifier.fillMaxWidth().height(165.dp).clip(RoundedCornerShape(LumaShapes.small))
.clickable { onItemClick(item) }
.background(Brush.verticalGradient(fallbackColors)),
) {
if (posterUrl != null) {
AsyncImage(
LumaAsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(posterUrl).crossfade(true).build(),
contentDescription = item.name,
contentScale = ContentScale.Crop,
@@ -383,12 +425,12 @@ private fun ContinueWatchingPoster(item: EmbyItem, serverUrl: String, onItemClic
Column(Modifier.width(220.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Box(
Modifier.fillMaxWidth().aspectRatio(16f / 9f)
.clip(RoundedCornerShape(12.dp))
.clip(RoundedCornerShape(LumaShapes.small))
.clickable { onItemClick(item) }
.background(Brush.linearGradient(fallbackColors)),
) {
if (imageUrl != null) {
AsyncImage(
LumaAsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).crossfade(true).build(),
contentDescription = item.name,
contentScale = ContentScale.Crop,
@@ -399,8 +441,8 @@ private fun ContinueWatchingPoster(item: EmbyItem, serverUrl: String, onItemClic
Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
LumaIconView(
icon = LumaIcon.Play,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(36.dp),
@@ -420,13 +462,22 @@ private fun ContinueWatchingPoster(item: EmbyItem, serverUrl: String, onItemClic
@Composable
private fun LazyLibrarySection(folder: EmbyMediaFolder, activeServer: EmbyServer, onItemClick: (EmbyItem) -> Unit = {}) {
var page by remember(folder.id) { mutableStateOf<EmbyLibraryPage?>(null) }
val cacheKey = remember(activeServer.id, activeServer.fullUrl, activeServer.userId) {
homeCacheKey(activeServer)
}
var page by remember(cacheKey, folder.id) {
mutableStateOf(HomeContentCache.libraryPage(cacheKey, folder.id))
}
LaunchedEffect(folder.id) {
page = try {
EmbyRepository.getItemsForLibrary(activeServer, folder.id, limit = 8)
} catch (_: Exception) {
EmbyLibraryPage()
LaunchedEffect(cacheKey, folder.id) {
if (page == null) {
page = try {
EmbyRepository.getItemsForLibrary(activeServer, folder.id, limit = 8)
} catch (_: Exception) {
EmbyLibraryPage()
}.also { loaded ->
HomeContentCache.putLibraryPage(cacheKey, folder.id, loaded)
}
}
}
@@ -438,7 +489,7 @@ private fun LazyLibrarySection(folder: EmbyMediaFolder, activeServer: EmbyServer
repeat(3) {
Box(
Modifier.width(115.dp).height(165.dp)
.clip(RoundedCornerShape(12.dp))
.clip(RoundedCornerShape(LumaShapes.small))
.background(homeSecondaryColor().copy(alpha = 0.10f))
)
}
@@ -463,10 +514,10 @@ private fun SectionTitle(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(text = title, color = homeContentColor(), fontSize = 17.sp, fontWeight = FontWeight.ExtraBold)
Text(text = title, color = homeContentColor(), fontSize = 18.sp, fontWeight = FontWeight.ExtraBold)
if (count > 0) {
Text(
text = "更多 >",
text = stringResource(Res.string.more) + " >",
color = homeAccentColor(),
fontSize = 13.sp,
fontWeight = FontWeight.Bold
@@ -478,7 +529,7 @@ private fun SectionTitle(
@Composable
private fun MyMediaGrid(libraries: List<EmbyMediaFolder>, serverUrl: String, onSelect: (EmbyMediaFolder) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SectionTitle(title = "我的媒体")
SectionTitle(title = stringResource(Res.string.section_library))
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
items(libraries) { folder ->
Column(
@@ -489,11 +540,11 @@ private fun MyMediaGrid(libraries: List<EmbyMediaFolder>, serverUrl: String, onS
val folderImageUrl = "$serverUrl/emby/Items/${folder.id}/Images/Primary?maxWidth=300&quality=80"
Box(
Modifier.size(120.dp)
.clip(RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(LumaShapes.medium))
.background(homeSecondaryColor().copy(alpha = 0.1f))
.clickable { onSelect(folder) },
) {
AsyncImage(
LumaAsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(folderImageUrl).crossfade(true).build(),
contentDescription = folder.name,
contentScale = ContentScale.Crop,
@@ -520,34 +571,60 @@ fun HomeScreen(
selected: EmbyServer? = null,
onSelectServer: (String) -> Unit = {},
onOpenDetail: (String) -> Unit = {},
hazeState: HazeState? = null,
) {
val activeServer = remember(servers, selected) { selected ?: servers.firstOrNull { it.apiKey.isNotBlank() } }
val homeKey = activeServer?.let(::homeCacheKey)
var libraries by remember { mutableStateOf<List<EmbyMediaFolder>>(emptyList()) }
var resumeItems by remember { mutableStateOf<List<EmbyItem>>(emptyList()) }
var latestItems by remember { mutableStateOf<List<EmbyItem>>(emptyList()) }
var loading by remember { mutableStateOf(false) }
var loadError by remember { mutableStateOf(false) }
var reloadToken by remember { mutableIntStateOf(0) }
val heroItems = remember(latestItems) { latestItems.take(8) }
val heroPagerState = rememberPagerState(pageCount = { heroItems.size })
val activeHeroItem = heroItems.getOrNull(heroPagerState.currentPage)
val immersiveBackground = rememberPosterImmersiveBackground(heroImageUrl(activeHeroItem, activeServer?.fullUrl.orEmpty()))
var lastLoadedServerId by remember { mutableStateOf("") }
LaunchedEffect(homeKey, reloadToken) {
val server = activeServer
val cacheKey = homeKey
if (server == null || server.apiKey.isBlank() || cacheKey == null) return@LaunchedEffect
if (reloadToken == 0) {
HomeContentCache.snapshot(cacheKey)?.let { snapshot ->
libraries = snapshot.libraries
latestItems = snapshot.latestItems
resumeItems = snapshot.resumeItems
loadError = false
loading = false
return@LaunchedEffect
}
} else {
HomeContentCache.invalidate(cacheKey)
}
LaunchedEffect(activeServer?.id) {
if (activeServer == null || activeServer.apiKey.isBlank()) return@LaunchedEffect
if (activeServer.id == lastLoadedServerId) return@LaunchedEffect
loading = true
try {
val libsDeferred = async { EmbyRepository.getLibraries(activeServer) }
val latestDeferred = async { EmbyRepository.getLatestItems(activeServer, limit = 10) }
val resumeDeferred = async { EmbyRepository.getResumeItems(activeServer, limit = 10) }
val libsDeferred = async { EmbyRepository.getLibraries(server) }
val latestDeferred = async { EmbyRepository.getLatestItems(server, limit = 10) }
val resumeDeferred = async { EmbyRepository.getResumeItems(server, limit = 10) }
libraries = libsDeferred.await()
latestItems = latestDeferred.await()
resumeItems = resumeDeferred.await()
HomeContentCache.putSnapshot(
cacheKey,
HomeSnapshot(
libraries = libraries,
resumeItems = resumeItems,
latestItems = latestItems,
),
)
loadError = false
} catch (_: Exception) {
loadError = true
} finally {
loading = false
}
@@ -572,7 +649,7 @@ fun HomeScreen(
AnimatedContent(
targetState = loading,
transitionSpec = {
fadeIn(tween(500)) togetherWith fadeOut(tween(500))
fadeIn(tween(LumaMotion.standard)) togetherWith fadeOut(tween(LumaMotion.standard))
},
label = "HomeLoadingTransition"
) { isLoading ->
@@ -581,8 +658,34 @@ fun HomeScreen(
EmptyHomeState()
}
} else if (isLoading) {
Column(
Modifier
.fillMaxSize()
.padding(horizontal = LumaSpacing.md, vertical = LumaSpacing.xl),
verticalArrangement = Arrangement.spacedBy(LumaSpacing.md),
) {
LumaSkeleton(Modifier.fillMaxWidth().height(290.dp), RoundedCornerShape(LumaShapes.large))
LumaSkeleton(Modifier.fillMaxWidth(0.48f).height(22.dp), RoundedCornerShape(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
repeat(2) {
LumaSkeleton(Modifier.weight(1f).height(170.dp), RoundedCornerShape(LumaShapes.small))
}
}
LumaSkeleton(Modifier.fillMaxWidth(0.34f).height(22.dp), RoundedCornerShape(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
repeat(3) {
LumaSkeleton(Modifier.weight(1f).height(150.dp), RoundedCornerShape(LumaShapes.small))
}
}
}
} else if (loadError) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = adaptiveAccent)
LumaErrorState(
title = stringResource(Res.string.error_content_title),
description = stringResource(Res.string.error_content_sub),
retryLabel = stringResource(Res.string.retry),
onRetry = { reloadToken++ },
)
}
} else {
val server = activeServer
@@ -612,12 +715,24 @@ fun HomeScreen(
0.38f to Color.Transparent,
)
)
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
.padding(start = LumaSpacing.md, end = LumaSpacing.md, top = LumaSpacing.md),
verticalArrangement = Arrangement.spacedBy(LumaSpacing.xl),
) {
if (resumeItems.isEmpty() && latestItems.isEmpty() && libraries.isEmpty()) {
Box(
Modifier.fillMaxWidth().padding(vertical = LumaSpacing.xxl),
contentAlignment = Alignment.Center,
) {
LumaEmptyState(
icon = LumaIcon.Media,
title = stringResource(Res.string.empty_home_no_data),
description = stringResource(Res.string.empty_home_no_data_sub),
)
}
}
if (resumeItems.isNotEmpty()) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SectionTitle(title = "继续观看")
SectionTitle(title = stringResource(Res.string.continue_watching))
LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
items(resumeItems) { item ->
ContinueWatchingPoster(item, server.fullUrl) { onOpenDetail(it.id) }
@@ -642,7 +757,7 @@ fun HomeScreen(
}
HomeOverlayTopBar(
name = activeServer?.name ?: "未连接",
name = activeServer?.name ?: stringResource(Res.string.home_not_connected),
iconUrl = activeServer?.iconUrl ?: "",
modifier = Modifier.align(Alignment.TopCenter),
)
@@ -660,12 +775,12 @@ private fun EmptyHomeState() {
Modifier.size(64.dp).clip(CircleShape).background(homeSecondaryColor().copy(alpha = 0.16f)),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Filled.PlayArrow, contentDescription = null, tint = homeSecondaryColor(), modifier = Modifier.size(32.dp))
LumaIconView(LumaIcon.Media, contentDescription = null, tint = homeSecondaryColor(), modifier = Modifier.size(32.dp))
}
Spacer(Modifier.height(16.dp))
Text("没有可用的媒体库", color = homeSecondaryColor(), fontSize = 15.sp)
Text(stringResource(Res.string.empty_home_no_server), color = homeSecondaryColor(), fontSize = 15.sp)
Spacer(Modifier.height(6.dp))
Text("请在资源库中添加服务器", color = homeSecondaryColor().copy(alpha = 0.72f), fontSize = 12.sp)
Text(stringResource(Res.string.empty_home_no_server_sub), color = homeSecondaryColor().copy(alpha = 0.72f), fontSize = 12.sp)
}
}
@@ -677,13 +792,13 @@ private fun HomeOverlayTopBar(name: String, iconUrl: String, modifier: Modifier
verticalAlignment = Alignment.CenterVertically
) {
Box(
Modifier.size(36.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.2f)),
Modifier.size(40.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.16f)),
contentAlignment = Alignment.Center
) {
if (iconUrl.isNotEmpty()) {
AsyncImage(model = iconUrl, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
LumaAsyncImage(model = iconUrl, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
} else {
Icon(Icons.Outlined.Storage, null, tint = Color.White)
LumaIconView(LumaIcon.Server, tint = Color.White, modifier = Modifier.size(22.dp))
}
}
Spacer(Modifier.width(12.dp))
File diff suppressed because it is too large. Load diff
@@ -21,18 +21,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Icon
import androidx.compose.ui.window.Dialog
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -58,18 +53,32 @@ import com.example.myapplication.ui.PosterImmersivePalette
import com.example.myapplication.ui.rememberPosterImmersiveBackground
import com.example.myapplication.ui.LiquidGlassButton
import com.example.myapplication.ui.GlassTokens
import com.example.myapplication.ui.GlassDialog
import com.example.myapplication.ui.LocalGlassBackdrop
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaLoading
import com.example.myapplication.ui.design.LumaAsyncImage
import com.example.myapplication.ui.design.LumaGlassLevel
import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.rememberLumaLayerBackdrop
import com.example.myapplication.ui.design.lumaGlass
import com.example.myapplication.ui.design.LumaSpacing
import com.kyant.backdrop.backdrops.layerBackdrop
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.example.myapplication.data.EmbyItem
import com.example.myapplication.data.EmbyRepository
import com.example.myapplication.data.EmbyServer
import com.example.myapplication.R as Res
import com.kyant.backdrop.Backdrop
import top.yukonga.miuix.kmp.basic.Text
import java.util.Locale
@@ -87,6 +96,8 @@ fun MediaDetailScreen(
onPlay: (itemId: String, seriesId: String?, seasonId: String?) -> Unit = { _, _, _ -> },
) {
val context = LocalContext.current
val detailLoadFailed = stringResource(Res.string.detail_load_failed)
val episodeNotReadyMessage = stringResource(Res.string.detail_episode_not_ready)
var detail by remember(item.id) { mutableStateOf(item) }
var loadError by remember(item.id) { mutableStateOf<String?>(null) }
var refreshing by remember(item.id) { mutableStateOf(false) }
@@ -108,7 +119,7 @@ fun MediaDetailScreen(
refreshing = true
val fullItem = EmbyRepository.getItem(server, item.id)
if (fullItem == null) {
loadError = "详情加载失败"
loadError = detailLoadFailed
} else {
detail = fullItem
loadError = null
@@ -143,6 +154,7 @@ fun MediaDetailScreen(
val detailSurfaceColor = if (lightContent) Color.Black.copy(alpha = 0.055f) else Color.White.copy(alpha = 0.06f)
val detailBorderColor = if (lightContent) Color.Black.copy(alpha = 0.10f) else Color.White.copy(alpha = 0.10f)
val detailAccentColor = if (lightContent) Color(0xFF1D4ED8) else Color(0xFFBFDBFE)
val detailBackdrop = rememberLumaLayerBackdrop()
CompositionLocalProvider(
LocalDetailText provides detailTextColor,
@@ -151,17 +163,23 @@ fun MediaDetailScreen(
LocalDetailBorder provides detailBorderColor,
LocalDetailAccent provides detailAccentColor,
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
0f to immersive.pageTop,
0.53f to immersive.hero,
1f to immersive.pageBottom,
Box(Modifier.fillMaxSize()) {
Box(
Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
0f to immersive.pageTop,
0.53f to immersive.hero,
1f to immersive.pageBottom,
)
)
),
) {
.then(
if (detailBackdrop != null) Modifier.layerBackdrop(detailBackdrop) else Modifier,
),
)
CompositionLocalProvider(LocalGlassBackdrop provides detailBackdrop) {
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
@@ -189,7 +207,7 @@ fun MediaDetailScreen(
}
DetailPlayButton(resumePositionMs = playTarget?.playbackPositionMs ?: detail.playbackPositionMs) {
if (playTarget == null) {
Toast.makeText(context, "剧集尚未加载完成", Toast.LENGTH_SHORT).show()
Toast.makeText(context, episodeNotReadyMessage, Toast.LENGTH_SHORT).show()
} else {
onPlay(
playTarget.id,
@@ -202,7 +220,7 @@ fun MediaDetailScreen(
if (detail.overview.isNullOrBlank().not()) {
Spacer(Modifier.height(20.dp))
DetailSectionCard(title = "简介") {
DetailSectionCard(title = stringResource(Res.string.detail_overview)) {
Text(
text = detail.overview.orEmpty(),
color = LocalDetailText.current.copy(alpha = 0.86f),
@@ -232,7 +250,7 @@ fun MediaDetailScreen(
if (seasons.isNotEmpty()) {
Spacer(Modifier.height(20.dp))
Text(text = "剧集", color = LocalDetailText.current, fontSize = 17.sp, fontWeight = FontWeight.Bold)
Text(text = stringResource(Res.string.detail_episodes), color = LocalDetailText.current, fontSize = 17.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(10.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -268,11 +286,13 @@ fun MediaDetailScreen(
}
Text(
text = "查看全部",
text = stringResource(Res.string.detail_view_all),
color = LocalDetailAccent.current,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.clickable(enabled = episodes.isNotEmpty()) { showAllEpisodes = true },
modifier = Modifier
.heightIn(min = 48.dp)
.clickable(enabled = episodes.isNotEmpty()) { showAllEpisodes = true },
)
}
@@ -286,7 +306,10 @@ fun MediaDetailScreen(
.background(LocalDetailSurface.current),
contentAlignment = Alignment.Center,
) {
Text(text = "正在加载剧集...", color = LocalDetailSecondary.current.copy(alpha = 0.82f), fontSize = 13.sp)
LumaLoading(
label = stringResource(Res.string.detail_loading_episodes),
modifier = Modifier.padding(vertical = LumaSpacing.xs),
)
}
} else {
Spacer(Modifier.height(12.dp))
@@ -313,25 +336,9 @@ fun MediaDetailScreen(
}
}
if (showAllEpisodes) {
EpisodeListDialog(
title = seasons.firstOrNull { it.id == selectedSeasonId }?.name ?: "全部剧集",
episodes = episodes,
selectedEpisodeId = selectedEpisodeId,
serverUrl = server?.fullUrl.orEmpty(),
fallbackImageUrl = seriesFallbackImageUrl,
background = immersive,
onDismiss = { showAllEpisodes = false },
onSelect = { episode ->
selectedEpisodeId = episode.id
showAllEpisodes = false
},
)
}
if (detail.people.isNotEmpty()) {
Spacer(Modifier.height(22.dp))
Text(text = "演职人员", color = LocalDetailText.current, fontSize = 17.sp, fontWeight = FontWeight.Bold)
Text(text = stringResource(Res.string.detail_cast), color = LocalDetailText.current, fontSize = 17.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(12.dp))
LazyRow(horizontalArrangement = Arrangement.spacedBy(13.dp)) {
items(detail.people.take(18).size) { index ->
@@ -348,27 +355,50 @@ fun MediaDetailScreen(
Spacer(Modifier.height(38.dp))
}
}
// Pinned above the scrolling content so it stays visible while browsing episodes.
Box(
Modifier
.align(Alignment.TopStart)
.statusBarsPadding()
.padding(start = 14.dp, top = 10.dp)
.size(39.dp)
.clip(CircleShape)
.background(Color.Black.copy(alpha = 0.32f))
.border(1.dp, Color.White.copy(alpha = 0.16f), CircleShape)
.size(48.dp)
.lumaGlass(
backdrop = LocalGlassBackdrop.current,
radius = 24.dp,
level = LumaGlassLevel.Floating,
tint = Color.Black.copy(alpha = 0.30f),
borderColor = Color.White.copy(alpha = 0.16f),
shadowElevation = 4.dp,
)
.clickable(onClick = onClose),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "返回",
LumaIconView(
icon = LumaIcon.Back,
contentDescription = stringResource(Res.string.back),
tint = Color.White,
modifier = Modifier.size(19.dp),
)
}
}
if (showAllEpisodes) {
EpisodeListDialog(
title = seasons.firstOrNull { it.id == selectedSeasonId }?.name ?: stringResource(Res.string.detail_all_episodes),
episodes = episodes,
selectedEpisodeId = selectedEpisodeId,
serverUrl = server?.fullUrl.orEmpty(),
fallbackImageUrl = seriesFallbackImageUrl,
background = immersive,
backdrop = detailBackdrop,
onDismiss = { showAllEpisodes = false },
onSelect = { episode ->
selectedEpisodeId = episode.id
showAllEpisodes = false
},
)
}
}
}
}
}
@@ -394,7 +424,7 @@ private fun DetailHero(
// Keep the detail cover at the same height as the home hero.
Box(Modifier.fillMaxWidth().aspectRatio(0.84f)) {
if (backdropUrl != null || posterUrl != null) {
AsyncImage(
LumaAsyncImage(
model = backdropUrl ?: posterUrl,
contentDescription = item.name,
contentScale = ContentScale.Crop,
@@ -487,10 +517,10 @@ private fun DetailHeadline(detail: EmbyItem, refreshing: Boolean, errorText: Str
Spacer(Modifier.height(9.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
if (detail.communityRating != null && detail.communityRating!! > 0) {
Icon(imageVector = Icons.Filled.Star, contentDescription = null, tint = Color(0xFFFFD54F), modifier = Modifier.size(14.dp))
detail.communityRating?.takeIf { it > 0.0 }?.let { rating ->
LumaIconView(LumaIcon.Star, contentDescription = null, tint = Color(0xFFFFD54F), modifier = Modifier.size(14.dp))
Spacer(Modifier.width(3.dp))
Text(text = String.format("%.1f", detail.communityRating), color = LocalDetailText.current, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
Text(text = String.format("%.1f", rating), color = LocalDetailText.current, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
Spacer(Modifier.width(9.dp))
}
if (detail.productionYear != null) {
@@ -505,9 +535,9 @@ private fun DetailHeadline(detail: EmbyItem, refreshing: Boolean, errorText: Str
Spacer(Modifier.height(6.dp))
Text(
text = when {
detail.type == "Series" -> "剧集"
detail.type == "Movie" -> "电影"
else -> detail.type ?: "媒体"
detail.type == "Series" -> stringResource(Res.string.detail_type_series)
detail.type == "Movie" -> stringResource(Res.string.detail_type_movie)
else -> detail.type ?: stringResource(Res.string.detail_type_media)
},
color = Color(0xFFBFDBFE),
fontSize = 11.sp,
@@ -516,7 +546,7 @@ private fun DetailHeadline(detail: EmbyItem, refreshing: Boolean, errorText: Str
if (refreshing) {
Spacer(Modifier.height(7.dp))
Text(text = "同步详情中...", color = LocalDetailSecondary.current.copy(alpha = 0.75f), fontSize = 11.sp)
Text(text = stringResource(Res.string.detail_loading), color = LocalDetailSecondary.current.copy(alpha = 0.75f), fontSize = 11.sp)
}
errorText?.let {
Spacer(Modifier.height(7.dp))
@@ -528,19 +558,23 @@ private fun DetailHeadline(detail: EmbyItem, refreshing: Boolean, errorText: Str
private fun DetailPlayButton(resumePositionMs: Long, onClick: () -> Unit) {
LiquidGlassButton(
onClick = onClick,
modifier = Modifier.width(188.dp).height(47.dp),
modifier = Modifier.width(188.dp).height(48.dp),
cornerRadius = 999.dp,
fill = GlassTokens.glassFillStrong,
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
LumaIconView(
icon = LumaIcon.Play,
contentDescription = null,
tint = GlassTokens.accent,
modifier = Modifier.size(21.dp)
modifier = Modifier.size(21.dp),
)
Spacer(Modifier.width(7.dp))
Text(
text = if (resumePositionMs > 0L) "继续 ${formatPlaybackTime(resumePositionMs)}" else "立即播放",
text = if (resumePositionMs > 0L) {
stringResource(Res.string.detail_continue_format, formatPlaybackTime(resumePositionMs))
} else {
stringResource(Res.string.detail_play_now)
},
color = GlassTokens.text,
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold
@@ -561,9 +595,14 @@ private fun DetailSectionCard(title: String, content: @Composable () -> Unit) {
Column(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(18.dp))
.background(LocalDetailSurface.current)
.border(1.dp, LocalDetailBorder.current, RoundedCornerShape(18.dp))
.lumaGlass(
backdrop = LocalGlassBackdrop.current,
radius = 18.dp,
level = LumaGlassLevel.Clear,
tint = LocalDetailSurface.current,
borderColor = LocalDetailBorder.current,
shadowElevation = 2.dp,
)
.padding(16.dp),
) {
Text(text = title, color = LocalDetailText.current, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
@@ -580,6 +619,7 @@ private fun EpisodeListDialog(
serverUrl: String,
fallbackImageUrl: String?,
background: PosterImmersivePalette,
backdrop: Backdrop?,
onDismiss: () -> Unit,
onSelect: (EmbyItem) -> Unit,
) {
@@ -589,20 +629,19 @@ private fun EpisodeListDialog(
Color.Black.copy(alpha = 0.10f)
}
Dialog(onDismissRequest = onDismiss) {
GlassDialog(backdrop = backdrop, onDismissRequest = onDismiss, modifier = Modifier.widthIn(max = 360.dp)) {
Column(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(24.dp))
.background(
Brush.verticalGradient(
0f to background.pageTop,
0.53f to background.hero,
1f to background.pageBottom,
)
.lumaGlass(
backdrop = null,
radius = 18.dp,
level = LumaGlassLevel.Strong,
tint = background.hero.copy(alpha = if (background.isDark) 0.20f else 0.12f),
borderColor = dialogBorder,
shadowElevation = 8.dp,
)
.border(1.dp, dialogBorder, RoundedCornerShape(24.dp))
.padding(16.dp),
.padding(4.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
@@ -613,11 +652,13 @@ private fun EpisodeListDialog(
modifier = Modifier.weight(1f),
)
Text(
text = "关闭",
text = stringResource(Res.string.detail_close),
color = LocalDetailSecondary.current,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.clickable(onClick = onDismiss),
modifier = Modifier
.heightIn(min = 48.dp)
.clickable(onClick = onDismiss),
)
}
Spacer(Modifier.height(12.dp))
@@ -653,10 +694,10 @@ private fun CompactEpisodeRow(
val rawName = item.name.trim()
val nameAlreadyNumbered = rawName.contains("第") && rawName.contains("集")
val title = when {
rawName.isNotBlank() && !nameAlreadyNumbered && item.indexNumber != null -> "第 ${item.indexNumber} 集 · $rawName"
rawName.isNotBlank() && !nameAlreadyNumbered && item.indexNumber != null -> stringResource(Res.string.detail_episode_format, item.indexNumber, rawName)
rawName.isNotBlank() -> rawName
item.indexNumber != null -> "第 ${item.indexNumber} 集"
else -> "剧集"
item.indexNumber != null -> stringResource(Res.string.detail_episode_number, item.indexNumber)
else -> stringResource(Res.string.detail_episode_short)
}
val rowBackground = if (selected) {
LocalDetailAccent.current.copy(alpha = 0.13f)
@@ -672,9 +713,14 @@ private fun CompactEpisodeRow(
Row(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(15.dp))
.background(rowBackground)
.border(1.dp, rowBorder, RoundedCornerShape(15.dp))
.lumaGlass(
backdrop = LocalGlassBackdrop.current,
radius = 15.dp,
level = LumaGlassLevel.Interactive,
tint = rowBackground,
borderColor = rowBorder,
shadowElevation = 0.dp,
)
.clickable(onClick = onClick)
.padding(7.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -686,7 +732,7 @@ private fun CompactEpisodeRow(
.background(Color(0xFF152036)),
) {
if (imageUrl != null) {
AsyncImage(
LumaAsyncImage(
model = imageUrl,
contentDescription = item.name,
contentScale = ContentScale.Crop,
@@ -695,7 +741,7 @@ private fun CompactEpisodeRow(
} else {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = item.indexNumber?.toString() ?: "TV",
text = item.indexNumber?.toString() ?: stringResource(Res.string.detail_tv_short),
color = Color.White.copy(alpha = 0.70f),
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
@@ -711,29 +757,16 @@ private fun CompactEpisodeRow(
) {
Text(
text = title,
color = if (selected) Color.Unspecified else LocalDetailText.current,
color = if (selected) LocalDetailAccent.current else LocalDetailText.current,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = if (selected) {
androidx.compose.ui.text.TextStyle(
brush = Brush.linearGradient(
listOf(
Color(0xFFFF8FA3),
Color(0xFF7BE495),
Color(0xFF66C7FF),
Color(0xFF9E8CFF),
)
)
)
} else {
LocalTextStyle.current
},
style = LocalTextStyle.current,
)
Spacer(Modifier.height(3.dp))
Text(
text = item.overview?.takeIf { it.isNotBlank() } ?: "暂无简介",
text = item.overview?.takeIf { it.isNotBlank() } ?: stringResource(Res.string.detail_no_overview),
color = LocalDetailSecondary.current.copy(alpha = if (selected) 0.90f else 0.76f),
fontSize = 10.sp,
lineHeight = 13.sp,
@@ -755,26 +788,16 @@ private fun EpisodeCard(
val imageUrl = EmbyRepository.buildEpisodeImageUrl(serverUrl, item, maxWidth = 560)
?: fallbackImageUrl
val rainbowColors = listOf(
Color(0xFFE06A5F),
Color(0xFFDE9455),
Color(0xFFCDB254),
Color(0xFF61A47C),
Color(0xFF58A3AB),
Color(0xFF6079CB),
Color(0xFF9A72C7),
Color(0xFFBC6A92),
)
val rainbowBrush = Brush.linearGradient(rainbowColors)
val normalBorderBrush = SolidColor(LocalDetailBorder.current.copy(alpha = 0.48f))
val selectedBorderBrush = SolidColor(LocalDetailAccent.current.copy(alpha = 0.84f))
val rawName = item.name.trim()
val nameAlreadyNumbered = rawName.contains("第") && rawName.contains("集")
val title = when {
rawName.isNotBlank() && !nameAlreadyNumbered && item.indexNumber != null -> "第 ${item.indexNumber} 集 · $rawName"
rawName.isNotBlank() && !nameAlreadyNumbered && item.indexNumber != null -> stringResource(Res.string.detail_episode_format, item.indexNumber, rawName)
rawName.isNotBlank() -> rawName
item.indexNumber != null -> "第 ${item.indexNumber} 集"
else -> "剧集"
item.indexNumber != null -> stringResource(Res.string.detail_episode_number, item.indexNumber)
else -> stringResource(Res.string.detail_episode_short)
}
val interactionSource = remember { MutableInteractionSource() }
@@ -802,7 +825,7 @@ private fun EpisodeCard(
val radius = (21.dp - strokeWidth / 2f).toPx()
drawRoundRect(
brush = if (selected) {
rainbowBrush
selectedBorderBrush
} else {
normalBorderBrush
},
@@ -814,7 +837,7 @@ private fun EpisodeCard(
},
) {
if (imageUrl != null) {
AsyncImage(
LumaAsyncImage(
model = imageUrl,
contentDescription = item.name,
contentScale = ContentScale.Crop,
@@ -828,7 +851,7 @@ private fun EpisodeCard(
contentAlignment = Alignment.Center,
) {
Text(
text = item.indexNumber?.toString() ?: "TV",
text = item.indexNumber?.toString() ?: stringResource(Res.string.detail_tv_short),
color = Color.White.copy(alpha = 0.72f),
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
@@ -841,22 +864,16 @@ private fun EpisodeCard(
Spacer(Modifier.height(9.dp))
Text(
text = title,
color = if (selected) Color.Unspecified else LocalDetailText.current,
color = if (selected) LocalDetailAccent.current else LocalDetailText.current,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 18.sp,
style = if (selected) {
androidx.compose.ui.text.TextStyle(
brush = Brush.linearGradient(rainbowColors.map { it.copy(alpha = 0.92f) })
)
} else {
LocalTextStyle.current
},
style = LocalTextStyle.current,
)
Spacer(Modifier.height(4.dp))
Text(
text = item.overview?.takeIf { it.isNotBlank() } ?: "暂无简介",
text = item.overview?.takeIf { it.isNotBlank() } ?: stringResource(Res.string.detail_no_overview),
color = LocalDetailSecondary.current.copy(alpha = if (selected) 0.94f else 0.82f),
fontSize = 11.sp,
lineHeight = 16.sp,
@@ -878,7 +895,7 @@ private fun PersonCard(person: com.example.myapplication.data.EmbyPerson, server
contentAlignment = Alignment.Center,
) {
if (imageUrl != null) {
AsyncImage(model = imageUrl, contentDescription = person.name, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
LumaAsyncImage(model = imageUrl, contentDescription = person.name, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
} else {
Text(text = person.name.take(1), color = LocalDetailText.current.copy(alpha = 0.72f), fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
@@ -897,19 +914,19 @@ private fun DetailInfoCard(item: EmbyItem) {
val videoStream = item.mediaStreams.firstOrNull { it.type == "Video" }
val audioStreams = item.mediaStreams.filter { it.type == "Audio" }
DetailSectionCard(title = "详细信息") {
InfoRow(label = "类型", value = item.type ?: "未知")
DetailSectionCard(title = stringResource(Res.string.detail_information)) {
InfoRow(label = stringResource(Res.string.detail_type), value = item.type ?: stringResource(Res.string.detail_unknown))
item.originalTitle?.takeIf { it.isNotBlank() && it != item.name }?.let {
InfoRow(label = "原名", value = it)
InfoRow(label = stringResource(Res.string.detail_original_title), value = it)
}
studios.takeIf { it.isNotBlank() }?.let { InfoRow(label = "制作方", value = it) }
studios.takeIf { it.isNotBlank() }?.let { InfoRow(label = stringResource(Res.string.detail_studio), value = it) }
videoStream?.let {
val quality = listOfNotNull(it.width, it.height).joinToString("×").ifBlank { null }
val value = listOfNotNull(quality, it.codec?.uppercase()).joinToString(" · ")
if (value.isNotBlank()) InfoRow(label = "画质", value = value)
if (value.isNotBlank()) InfoRow(label = stringResource(Res.string.detail_quality), value = value)
}
if (audioStreams.isNotEmpty()) {
InfoRow(label = "音轨", value = audioStreams.count().toString() + " 条")
InfoRow(label = stringResource(Res.string.detail_audio_track), value = stringResource(Res.string.detail_audio_track_count, audioStreams.count()))
}
}
}
@@ -928,9 +945,20 @@ private fun InfoRow(label: String, value: String) {
}
}
@Composable
private fun formatRuntime(ticks: Long?): String? {
if (ticks == null || ticks <= 0) return null
val minutes = ticks / 600_000_000L
if (minutes <= 0) return null
return if (minutes < 60) "${minutes}分钟" else "${minutes / 60}小时${if (minutes % 60 == 0L) "" else " ${minutes % 60}分"}"
return if (minutes < 60) {
stringResource(Res.string.detail_runtime_minutes, minutes.toInt())
} else {
val hours = (minutes / 60).toInt()
val remainingMinutes = (minutes % 60).toInt()
if (remainingMinutes == 0) {
stringResource(Res.string.detail_runtime_hours, hours)
} else {
stringResource(Res.string.detail_runtime_hours_minutes, hours, remainingMinutes)
}
}
}
@@ -28,11 +28,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CalendarMonth
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -52,7 +47,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.example.myapplication.R as Res
import com.example.myapplication.data.EmbyItem
@@ -62,6 +56,13 @@ import com.example.myapplication.ui.GlassTokens
import com.example.myapplication.ui.LiquidGlassTopBar
import com.example.myapplication.ui.SegmentedControl
import com.example.myapplication.ui.bottomBarContentPadding
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaErrorState
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaAsyncImage
import com.example.myapplication.ui.design.LumaSkeleton
import com.example.myapplication.ui.design.LumaShapes
import top.yukonga.miuix.kmp.basic.Text
@Composable
@@ -96,12 +97,22 @@ fun RecordsScreen(
) {
var favorites by remember { mutableStateOf<List<EmbyItem>>(emptyList()) }
var loading by remember { mutableStateOf(false) }
var loadError by remember { mutableStateOf(false) }
var reloadToken by remember { mutableStateOf(0) }
LaunchedEffect(activeServer?.id) {
LaunchedEffect(activeServer?.id, reloadToken) {
if (activeServer != null && activeServer.apiKey.isNotBlank()) {
loading = true
favorites = EmbyRepository.getFavoriteItems(activeServer)
try {
favorites = EmbyRepository.getFavoriteItems(activeServer)
loadError = false
} catch (_: Exception) {
favorites = emptyList()
loadError = true
}
loading = false
} else {
loadError = false
}
}
@@ -114,23 +125,40 @@ fun RecordsScreen(
) { selectedSegment ->
if (selectedSegment == 1) {
RecordsEmptyState(
icon = Icons.Outlined.CalendarMonth,
icon = LumaIcon.Calendar,
title = stringResource(Res.string.empty_calendar),
subtitle = stringResource(Res.string.empty_calendar_sub),
)
} else if (loading) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 150.dp),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
contentPadding = PaddingValues(top = 16.dp, bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(6) {
LumaSkeleton(Modifier.fillMaxWidth().aspectRatio(0.68f), RoundedCornerShape(LumaShapes.small))
}
}
} else if (loadError) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = GlassTokens.accent)
LumaErrorState(
title = stringResource(Res.string.error_content_title),
description = stringResource(Res.string.error_content_sub),
retryLabel = stringResource(Res.string.retry),
onRetry = { reloadToken++ },
)
}
} else if (favorites.isEmpty()) {
RecordsEmptyState(
icon = Icons.Outlined.FavoriteBorder,
icon = LumaIcon.Favorite,
title = stringResource(Res.string.empty_favorites),
subtitle = stringResource(Res.string.empty_favorites_sub),
)
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
columns = GridCells.Adaptive(minSize = 150.dp),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
contentPadding = PaddingValues(top = 16.dp, bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()),
verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -152,7 +180,7 @@ fun RecordsScreen(
@Composable
private fun RecordsEmptyState(
icon: ImageVector,
icon: LumaIcon,
title: String,
subtitle: String,
) {
@@ -162,23 +190,7 @@ private fun RecordsEmptyState(
.padding(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding()),
contentAlignment = Alignment.Center,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
Modifier
.size(68.dp)
.clip(CircleShape)
.background(GlassTokens.glassFill)
.border(1.dp, GlassTokens.glassBorder, CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(icon, contentDescription = null, tint = GlassTokens.subtext2, modifier = Modifier.size(30.dp))
}
Text(title, color = GlassTokens.text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
Text(subtitle, color = GlassTokens.subtext, fontSize = 12.sp)
}
LumaEmptyState(icon = icon, title = title, description = subtitle)
}
}
@@ -193,11 +205,11 @@ private fun FavoriteMediaCard(item: EmbyItem, serverUrl: String, onClick: () ->
Modifier
.fillMaxWidth()
.aspectRatio(0.7f)
.clip(RoundedCornerShape(12.dp))
.background(Color.DarkGray)
.clip(RoundedCornerShape(LumaShapes.small))
.background(LumaColors.backgroundElevated)
) {
if (imageUrl != null) {
AsyncImage(
LumaAsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).crossfade(true).build(),
contentDescription = item.name,
contentScale = ContentScale.Crop,
@@ -19,10 +19,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -41,7 +37,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.example.myapplication.R as Res
import com.example.myapplication.data.EmbyItem
@@ -51,6 +46,11 @@ import com.example.myapplication.ui.GlassSearchField
import com.example.myapplication.ui.GlassTokens
import com.example.myapplication.ui.LiquidGlassTopBar
import com.example.myapplication.ui.bottomBarContentPadding
import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaErrorState
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaAsyncImage
import com.example.myapplication.ui.design.LumaSkeleton
import kotlinx.coroutines.delay
import top.yukonga.miuix.kmp.basic.Text
@@ -85,18 +85,27 @@ fun SearchScreen(
) {
var results by remember(activeServer?.id) { mutableStateOf<List<EmbyItem>>(emptyList()) }
var loading by remember(activeServer?.id) { mutableStateOf(false) }
var loadError by remember(activeServer?.id) { mutableStateOf(false) }
var reloadToken by remember { mutableStateOf(0) }
LaunchedEffect(query, activeServer?.id, activeServer?.apiKey) {
LaunchedEffect(query, activeServer?.id, activeServer?.apiKey, reloadToken) {
val server = activeServer
val term = query.trim()
if (server == null || server.apiKey.isBlank() || term.isBlank()) {
results = emptyList()
loading = false
loadError = false
return@LaunchedEffect
}
delay(280)
loading = true
results = EmbyRepository.searchItems(server, term)
try {
results = EmbyRepository.searchItems(server, term)
loadError = false
} catch (_: Exception) {
results = emptyList()
loadError = true
}
loading = false
}
Box(Modifier.fillMaxSize()) {
@@ -108,25 +117,44 @@ fun SearchScreen(
) {
when {
activeServer == null -> SearchEmptyState(
title = "暂无可搜索的媒体库",
subtitle = "请先在资源库中连接 Emby 服务器",
title = stringResource(Res.string.search_no_server),
subtitle = stringResource(Res.string.search_no_server_sub),
)
query.isBlank() -> SearchEmptyState(
title = stringResource(Res.string.search_title),
subtitle = stringResource(Res.string.search_empty_input),
)
loading -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = GlassTokens.accent)
loading -> LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 150.dp),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
top = 16.dp,
bottom = bottomBarContentPadding(86.dp).calculateBottomPadding(),
),
verticalArrangement = Arrangement.spacedBy(14.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(6) {
LumaSkeleton(Modifier.fillMaxWidth().aspectRatio(0.68f), RoundedCornerShape(16.dp))
}
}
loadError -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
LumaErrorState(
title = stringResource(Res.string.error_content_title),
description = stringResource(Res.string.error_content_sub),
retryLabel = stringResource(Res.string.retry),
onRetry = { reloadToken++ },
)
}
results.isEmpty() -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = stringResource(Res.string.search_no_result_prefix) + query.trim() + stringResource(Res.string.search_no_result_suffix),
color = GlassTokens.subtext,
fontSize = 14.sp,
LumaEmptyState(
icon = LumaIcon.Search,
title = stringResource(Res.string.search_no_result_title),
description = stringResource(Res.string.search_no_result_prefix) + query.trim() + stringResource(Res.string.search_no_result_suffix),
)
}
else -> LazyVerticalGrid(
columns = GridCells.Fixed(2),
columns = GridCells.Adaptive(minSize = 150.dp),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
top = 16.dp,
@@ -156,22 +184,7 @@ private fun SearchEmptyState(title: String, subtitle: String) {
.padding(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding()),
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(
Modifier
.size(68.dp)
.clip(CircleShape)
.background(GlassTokens.glassFill)
.border(1.dp, GlassTokens.glassBorder, CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Outlined.Search, contentDescription = null, tint = GlassTokens.subtext2, modifier = Modifier.size(32.dp))
}
Spacer(Modifier.height(16.dp))
Text(title, color = GlassTokens.text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
Spacer(Modifier.height(6.dp))
Text(subtitle, color = GlassTokens.subtext, fontSize = 12.sp)
}
LumaEmptyState(icon = LumaIcon.Search, title = title, description = subtitle)
}
}
@@ -193,7 +206,7 @@ private fun SearchMediaCard(item: EmbyItem, serverUrl: String, onClick: () -> Un
.background(Brush.verticalGradient(fallback)),
) {
if (imageUrl != null) {
AsyncImage(
LumaAsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).crossfade(true).build(),
contentDescription = item.name,
contentScale = ContentScale.Crop,
@@ -3,9 +3,11 @@ package com.example.myapplication.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -18,27 +20,40 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
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.myapplication.R
import com.example.myapplication.BuildConfig
import com.example.myapplication.data.IconLibraryRepository
import com.example.myapplication.ui.GlassConfirmDialog
import com.example.myapplication.ui.GlassTokens
import com.example.myapplication.ui.LocalGlassBackdrop
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.rememberLumaLayerBackdrop
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaGlassLevel
import com.example.myapplication.ui.design.lumaGlass
import com.example.myapplication.ui.design.LumaShapes
import com.example.myapplication.ui.design.LumaSpacing
import com.example.myapplication.ui.design.LumaTypography
import top.yukonga.miuix.kmp.basic.Text
import com.kyant.backdrop.backdrops.layerBackdrop
@Composable
private fun PageTopBar(title: String, onBack: () -> Unit) {
@@ -48,14 +63,12 @@ private fun PageTopBar(title: String, onBack: () -> Unit) {
) {
Box(
Modifier
.size(36.dp)
.clip(CircleShape)
.background(GlassTokens.glassFill)
.border(0.5.dp, GlassTokens.glassBorder, CircleShape)
.clickable(onClick = onBack),
.size(48.dp)
.lumaGlass(radius = 24.dp, level = LumaGlassLevel.Floating)
.clickable(indication = null, interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }, onClick = onBack),
contentAlignment = Alignment.Center,
) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回", tint = GlassTokens.text, modifier = Modifier.size(20.dp))
LumaIconView(LumaIcon.Back, contentDescription = stringResource(R.string.back), tint = LumaColors.text, modifier = Modifier.size(20.dp))
}
Spacer(Modifier.size(12.dp))
Text(title, color = GlassTokens.text, fontSize = 20.sp, fontWeight = FontWeight.Bold)
@@ -65,39 +78,236 @@ private fun PageTopBar(title: String, onBack: () -> Unit) {
@Composable
fun CacheManagementScreen(onBack: () -> Unit) {
var cacheSize by remember { mutableLongStateOf(IconLibraryRepository.getIconCacheSize()) }
Column(Modifier.fillMaxSize()) {
PageTopBar(title = "缓存管理", onBack = onBack)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
item {
CacheInfoCard(
title = "图标数据缓存",
value = formatCacheSize(cacheSize),
icon = Icons.Outlined.Info,
)
}
item {
Box(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(18.dp))
.background(GlassTokens.glassFill)
.border(0.5.dp, GlassTokens.glassBorder, RoundedCornerShape(18.dp))
.clickable {
IconLibraryRepository.clearAllIconCache()
cacheSize = 0L
}
.padding(horizontal = 16.dp, vertical = 15.dp),
contentAlignment = Alignment.CenterStart,
var showClearCacheConfirm by remember { mutableStateOf(false) }
val cacheBackdrop = rememberLumaLayerBackdrop()
Box(Modifier.fillMaxSize()) {
Box(
Modifier
.fillMaxSize()
.background(LumaColors.background)
.then(
if (cacheBackdrop != null) Modifier.layerBackdrop(cacheBackdrop) else Modifier,
),
)
CompositionLocalProvider(LocalGlassBackdrop provides cacheBackdrop) {
Column(Modifier.fillMaxSize()) {
PageTopBar(title = stringResource(R.string.cache_detail_title), onBack = onBack)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Outlined.Delete, contentDescription = null, tint = Color(0xFFE5484D), modifier = Modifier.size(21.dp))
Spacer(Modifier.size(12.dp))
Text("删除缓存", color = Color(0xFFE5484D), fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
item {
CacheInfoCard(
title = stringResource(R.string.icon_cache),
value = formatCacheSize(cacheSize),
icon = LumaIcon.Info,
)
}
item {
Box(
Modifier
.fillMaxWidth()
.lumaGlass(radius = LumaShapes.medium, level = LumaGlassLevel.Regular)
.clickable { showClearCacheConfirm = true }
.padding(horizontal = 16.dp, vertical = 15.dp),
contentAlignment = Alignment.CenterStart,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
LumaIconView(LumaIcon.Delete, contentDescription = null, tint = LumaColors.danger, modifier = Modifier.size(21.dp))
Spacer(Modifier.size(12.dp))
Text(stringResource(R.string.delete_cache), color = LumaColors.danger, fontSize = LumaTypography.body, fontWeight = FontWeight.SemiBold)
}
}
}
}
}
}
if (showClearCacheConfirm) {
GlassConfirmDialog(
title = stringResource(R.string.confirm_delete_title),
message = stringResource(R.string.confirm_clear_cache_message),
confirmText = stringResource(R.string.confirm_clear),
dismissText = stringResource(R.string.confirm_cancel),
backdrop = cacheBackdrop,
onDismissRequest = { showClearCacheConfirm = false },
onConfirm = {
IconLibraryRepository.clearAllIconCache()
cacheSize = 0L
showClearCacheConfirm = false
},
)
}
}
}
@Composable
private fun CacheInfoCard(title: String, value: String, icon: LumaIcon) {
Row(
Modifier
.fillMaxWidth()
.lumaGlass(radius = LumaShapes.medium, level = LumaGlassLevel.Regular)
.padding(horizontal = 16.dp, vertical = 15.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
Modifier.size(40.dp).clip(RoundedCornerShape(LumaShapes.small)).background(LumaColors.accentSoft),
contentAlignment = Alignment.Center,
) {
LumaIconView(icon, contentDescription = null, tint = LumaColors.accent, modifier = Modifier.size(21.dp))
}
Spacer(Modifier.size(12.dp))
Column(Modifier.weight(1f)) {
Text(title, color = GlassTokens.text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
Spacer(Modifier.height(3.dp))
Text(stringResource(R.string.cache_total_size), color = GlassTokens.subtext, fontSize = LumaTypography.caption)
}
Text(value, color = GlassTokens.subtext, fontSize = 13.sp)
}
}
@Composable
fun AboutScreen(onBack: () -> Unit) {
val aboutBackdrop = rememberLumaLayerBackdrop()
Box(Modifier.fillMaxSize()) {
Box(
Modifier
.fillMaxSize()
.background(LumaColors.background)
.then(
if (aboutBackdrop != null) Modifier.layerBackdrop(aboutBackdrop) else Modifier,
),
)
CompositionLocalProvider(LocalGlassBackdrop provides aboutBackdrop) {
Column(Modifier.fillMaxSize()) {
PageTopBar(title = stringResource(R.string.about), onBack = onBack)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = LumaSpacing.md, vertical = LumaSpacing.sm),
verticalArrangement = Arrangement.spacedBy(LumaSpacing.md),
) {
item {
Column(
Modifier.fillMaxWidth().padding(vertical = LumaSpacing.sm),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
Modifier
.size(88.dp)
.clip(RoundedCornerShape(LumaShapes.large))
.background(LumaColors.accentSoft)
.border(0.7.dp, LumaColors.glassBorder, RoundedCornerShape(LumaShapes.large)),
contentAlignment = Alignment.Center,
) {
Image(
// Adaptive mipmap XML cannot be loaded by Compose painterResource on API 35.
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = stringResource(R.string.about_app_name),
modifier = Modifier.size(56.dp),
)
}
Spacer(Modifier.height(LumaSpacing.md))
Text(stringResource(R.string.about_app_name), color = GlassTokens.text, fontSize = 24.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(LumaSpacing.xs))
Text(
stringResource(R.string.about_version_format, BuildConfig.VERSION_NAME),
color = GlassTokens.subtext,
fontSize = LumaTypography.secondary,
)
Spacer(Modifier.height(LumaSpacing.md))
Text(
stringResource(R.string.app_desc),
color = GlassTokens.subtext,
fontSize = LumaTypography.body,
)
}
}
item {
AboutSectionCard(title = stringResource(R.string.about_technology_title)) {
AboutInfoRow(
LumaIcon.Media,
stringResource(R.string.about_technology_compose),
stringResource(R.string.about_technology_compose_sub),
)
AboutInfoRow(
LumaIcon.Play,
stringResource(R.string.about_technology_media3),
stringResource(R.string.about_technology_media3_sub),
)
AboutInfoRow(
LumaIcon.Folder,
stringResource(R.string.about_technology_navigation),
stringResource(R.string.about_technology_navigation_sub),
)
AboutInfoRow(
LumaIcon.Library,
stringResource(R.string.about_technology_backdrop),
stringResource(R.string.about_technology_backdrop_sub),
)
}
}
item {
AboutSectionCard(title = stringResource(R.string.about_licenses_title)) {
AboutInfoRow(
LumaIcon.Media,
stringResource(R.string.about_license_compose),
stringResource(R.string.about_license_apache),
)
AboutInfoRow(
LumaIcon.Play,
stringResource(R.string.about_license_media3),
stringResource(R.string.about_license_apache),
)
AboutInfoRow(
LumaIcon.Folder,
stringResource(R.string.about_license_navigation),
stringResource(R.string.about_license_apache),
)
AboutInfoRow(
LumaIcon.Image,
stringResource(R.string.about_license_coil),
stringResource(R.string.about_license_apache),
)
AboutInfoRow(
LumaIcon.Network,
stringResource(R.string.about_license_retrofit),
stringResource(R.string.about_license_apache),
)
AboutInfoRow(
LumaIcon.Network,
stringResource(R.string.about_license_okhttp),
stringResource(R.string.about_license_apache),
)
AboutInfoRow(
LumaIcon.Database,
stringResource(R.string.about_license_room),
stringResource(R.string.about_license_apache),
)
AboutInfoRow(
LumaIcon.Library,
stringResource(R.string.about_license_backdrop),
stringResource(R.string.about_license_apache),
)
AboutInfoRow(
LumaIcon.Settings,
stringResource(R.string.about_license_miuix),
stringResource(R.string.about_license_apache),
)
}
}
item {
AboutSectionCard(title = stringResource(R.string.about_credits_title)) {
AboutInfoRow(
LumaIcon.Library,
stringResource(R.string.about_aurora_title),
stringResource(R.string.about_aurora_subtitle),
)
AboutInfoRow(
LumaIcon.Info,
stringResource(R.string.about_open_source_title),
stringResource(R.string.about_open_source_subtitle),
)
}
}
}
}
@@ -106,57 +316,40 @@ fun CacheManagementScreen(onBack: () -> Unit) {
}
@Composable
private fun CacheInfoCard(title: String, value: String, icon: androidx.compose.ui.graphics.vector.ImageVector) {
Row(
private fun AboutSectionCard(title: String, content: @Composable ColumnScope.() -> Unit) {
Column(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(18.dp))
.background(GlassTokens.glassFill)
.border(0.5.dp, GlassTokens.glassBorder, RoundedCornerShape(18.dp))
.padding(horizontal = 16.dp, vertical = 15.dp),
verticalAlignment = Alignment.CenterVertically,
.lumaGlass(radius = LumaShapes.large, level = LumaGlassLevel.Regular)
.padding(vertical = LumaSpacing.xs),
) {
Box(
Modifier.size(40.dp).clip(RoundedCornerShape(12.dp)).background(GlassTokens.accent.copy(alpha = 0.14f)),
contentAlignment = Alignment.Center,
) {
Icon(icon, contentDescription = null, tint = GlassTokens.accent, modifier = Modifier.size(21.dp))
}
Spacer(Modifier.size(12.dp))
Column(Modifier.weight(1f)) {
Text(title, color = GlassTokens.text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
Spacer(Modifier.height(3.dp))
Text("缓存数据总大小", color = GlassTokens.subtext, fontSize = 12.sp)
}
Text(value, color = GlassTokens.subtext, fontSize = 13.sp)
Text(
title,
color = GlassTokens.subtext,
fontSize = LumaTypography.secondary,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(horizontal = LumaSpacing.md, vertical = LumaSpacing.xs),
)
content()
}
}
@Composable
fun AboutScreen(onBack: () -> Unit) {
Column(Modifier.fillMaxSize()) {
PageTopBar(title = "关于", onBack = onBack)
Column(
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
private fun AboutInfoRow(icon: LumaIcon, title: String, subtitle: String) {
Row(
Modifier.fillMaxWidth().padding(horizontal = LumaSpacing.md, vertical = LumaSpacing.sm),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
Modifier.size(38.dp).clip(RoundedCornerShape(LumaShapes.small)).background(LumaColors.accentSoft),
contentAlignment = Alignment.Center,
) {
Box(
Modifier.size(72.dp).clip(RoundedCornerShape(22.dp)).background(GlassTokens.accent.copy(alpha = 0.16f)),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Outlined.Info, contentDescription = null, tint = GlassTokens.accent, modifier = Modifier.size(36.dp))
}
Spacer(Modifier.height(14.dp))
Text("LumaPlayer", color = GlassTokens.text, fontSize = 21.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(6.dp))
Text("版本 1.0", color = GlassTokens.subtext, fontSize = 13.sp)
Spacer(Modifier.height(22.dp))
Box(
Modifier.fillMaxWidth().clip(RoundedCornerShape(18.dp)).background(GlassTokens.glassFill)
.border(0.5.dp, GlassTokens.glassBorder, RoundedCornerShape(18.dp)).padding(16.dp),
) {
Text("一款简洁优雅的 Emby 媒体库管理工具", color = GlassTokens.text, fontSize = 14.sp)
}
LumaIconView(icon, contentDescription = null, tint = GlassTokens.accent, modifier = Modifier.size(20.dp))
}
Spacer(Modifier.size(LumaSpacing.sm))
Column(Modifier.weight(1f)) {
Text(title, color = GlassTokens.text, fontSize = LumaTypography.body, fontWeight = FontWeight.SemiBold)
Text(subtitle, color = GlassTokens.subtext, fontSize = LumaTypography.caption)
}
}
}
@@ -1,11 +1,12 @@
package com.example.myapplication.screens
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.animateColorAsState
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.interaction.collectIsPressedAsState
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -16,6 +17,7 @@ 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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
@@ -25,14 +27,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ChevronRight
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Language
import androidx.compose.material.icons.outlined.PlayCircle
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -47,7 +43,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
@@ -64,16 +59,28 @@ import androidx.compose.ui.zIndex
import com.example.myapplication.R
import com.example.myapplication.data.EmbyServer
import com.example.myapplication.ui.AppLanguage
import com.example.myapplication.ui.AppTheme
import com.example.myapplication.ui.GlassTokens
import com.example.myapplication.ui.LiquidGlassTopBar
import com.example.myapplication.ui.LiquidGlassPopupOverlay
import com.example.myapplication.ui.LocalGlassBackdrop
import com.example.myapplication.ui.bottomBarContentPadding
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.rememberLumaLayerBackdrop
import com.example.myapplication.ui.design.LumaMotion
import com.example.myapplication.ui.design.LumaShapes
import com.example.myapplication.ui.design.LumaSpacing
import com.example.myapplication.ui.design.LumaTypography
import com.example.myapplication.ui.design.lumaGlass
import com.example.myapplication.ui.design.lumaFallbackGlass
import com.kyant.backdrop.Backdrop
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import com.kyant.backdrop.drawBackdrop
import com.kyant.backdrop.effects.blur
import com.kyant.backdrop.effects.colorControls
import com.kyant.backdrop.effects.lens
import com.kyant.backdrop.effects.vibrancy
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.shadow.Shadow
@@ -87,7 +94,7 @@ import top.yukonga.miuix.kmp.basic.Text
fun SettingsTopBar(modifier: Modifier = Modifier) {
LiquidGlassTopBar(modifier) {
Box(Modifier.fillMaxWidth().padding(horizontal = 18.dp, vertical = 14.dp)) {
Text(text = "设置", color = GlassTokens.text, fontSize = 22.sp, fontWeight = FontWeight.Bold)
Text(text = stringResource(R.string.settings_title), color = GlassTokens.text, fontSize = LumaTypography.titleLarge, fontWeight = FontWeight.Bold)
}
}
}
@@ -101,28 +108,50 @@ fun SettingsScreen(
var showLanguageDialog by remember { mutableStateOf(false) }
var languageMenuMounted by remember { mutableStateOf(false) }
var languageEntryBounds by remember { mutableStateOf<Rect?>(null) }
var themeMenuVisible by remember { mutableStateOf(false) }
var themeMenuMounted by remember { mutableStateOf(false) }
var themeEntryBounds by remember { mutableStateOf<Rect?>(null) }
var settingsRootOrigin by remember { mutableStateOf(Offset.Zero) }
val context = LocalContext.current
val languageScope = rememberCoroutineScope()
val languageBackdrop = rememberLayerBackdrop()
val languageBackdrop = rememberLumaLayerBackdrop()
Box(
Modifier
.fillMaxSize()
.onGloballyPositioned { settingsRootOrigin = it.positionInRoot() },
) {
Column(
Box(
Modifier
.fillMaxSize()
.layerBackdrop(languageBackdrop)
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
.background(LumaColors.background)
.then(
if (languageBackdrop != null) Modifier.layerBackdrop(languageBackdrop) else Modifier,
),
)
CompositionLocalProvider(LocalGlassBackdrop provides languageBackdrop) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
SettingSectionCard {
SettingRowItem(
icon = Icons.Outlined.Language,
title = "语言",
icon = LumaIcon.Image,
title = stringResource(R.string.settings_theme),
subtitle = themeModeLabel(),
onTrailingIconPositioned = { themeEntryBounds = it.boundsInRoot() },
onClick = {
themeMenuMounted = true
themeMenuVisible = true
},
)
DividerLine()
SettingRowItem(
icon = LumaIcon.Globe,
title = stringResource(R.string.settings_language),
subtitle = AppLanguage.displayName,
onTrailingIconPositioned = { languageEntryBounds = it.boundsInRoot() },
onClick = {
@@ -132,47 +161,63 @@ fun SettingsScreen(
)
DividerLine()
SettingRowItem(
icon = Icons.Outlined.PlayCircle,
title = "播放设置",
subtitle = "自适应比特率",
icon = LumaIcon.Play,
title = stringResource(R.string.settings_playback_row),
subtitle = stringResource(R.string.settings_playback_row_sub),
onClick = {},
)
}
SettingSectionCard {
SettingRowItem(
icon = Icons.Outlined.Folder,
title = "清除缓存",
subtitle = "管理应用缓存",
icon = LumaIcon.Folder,
title = stringResource(R.string.settings_cache),
subtitle = stringResource(R.string.settings_cache_sub),
onClick = { onOpenCacheDetail() },
)
DividerLine()
SettingRowItem(
icon = Icons.Outlined.Info,
title = "关于",
subtitle = "版本 1.0",
icon = LumaIcon.Info,
title = stringResource(R.string.about),
subtitle = stringResource(R.string.about_sub),
onClick = { onOpenAbout() },
)
}
}
val anchor = languageEntryBounds
if (languageMenuMounted && anchor != null) {
LanguageMenuOverlay(
anchorBounds = anchor,
rootOrigin = settingsRootOrigin,
backdrop = languageBackdrop,
visible = showLanguageDialog,
onDismiss = { showLanguageDialog = false },
onClosed = { languageMenuMounted = false },
onLanguageSelected = { language ->
showLanguageDialog = false
languageScope.launch {
delay(230)
AppLanguage.apply(context, language)
}
},
)
val anchor = languageEntryBounds
if (languageMenuMounted && anchor != null) {
LanguageMenuOverlay(
anchorBounds = anchor,
rootOrigin = settingsRootOrigin,
backdrop = languageBackdrop,
visible = showLanguageDialog,
onDismiss = { showLanguageDialog = false },
onClosed = { languageMenuMounted = false },
onLanguageSelected = { language ->
showLanguageDialog = false
languageScope.launch {
delay(230)
AppLanguage.apply(context, language)
}
},
)
}
val themeAnchor = themeEntryBounds
if (themeMenuMounted && themeAnchor != null) {
ThemeMenuOverlay(
anchorBounds = themeAnchor,
rootOrigin = settingsRootOrigin,
backdrop = languageBackdrop,
visible = themeMenuVisible,
onDismiss = { themeMenuVisible = false },
onClosed = { themeMenuMounted = false },
onThemeSelected = { mode ->
themeMenuVisible = false
AppTheme.setMode(context, mode)
},
)
}
}
}
}
@@ -182,9 +227,7 @@ private fun SettingSectionCard(content: @Composable ColumnScope.() -> Unit) {
Column(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(20.dp))
.background(Color.White)
.border(0.5.dp, Color.LightGray, RoundedCornerShape(20.dp))
.lumaGlass(radius = LumaShapes.large, level = com.example.myapplication.ui.design.LumaGlassLevel.Regular)
.padding(vertical = 4.dp),
) {
content()
@@ -193,40 +236,48 @@ private fun SettingSectionCard(content: @Composable ColumnScope.() -> Unit) {
@Composable
private fun SettingRowItem(
icon: ImageVector,
icon: LumaIcon,
title: String,
subtitle: String,
modifier: Modifier = Modifier,
onTrailingIconPositioned: ((LayoutCoordinates) -> Unit)? = null,
onClick: () -> Unit,
) {
val interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
val pressed by interactionSource.collectIsPressedAsState()
val pressColor by animateColorAsState(
targetValue = if (pressed) LumaColors.glassHighlight else Color.Transparent,
animationSpec = tween(LumaMotion.fast),
label = "settingRowPress",
)
Row(
modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 14.dp),
.background(pressColor)
.clickable(indication = null, interactionSource = interactionSource) { onClick() }
.padding(horizontal = LumaSpacing.md, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
Modifier
.size(38.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color(0xFFF3F4F6)),
.clip(RoundedCornerShape(LumaShapes.small))
.background(LumaColors.accentSoft),
contentAlignment = Alignment.Center
) {
Icon(icon, null, tint = Color(0xFF374151), modifier = Modifier.size(20.dp))
LumaIconView(icon, tint = LumaColors.accent, contentDescription = null, modifier = Modifier.size(20.dp))
}
Spacer(Modifier.width(14.dp))
Column(Modifier.weight(1f)) {
Text(title, color = Color(0xFF1F2937), fontSize = 15.sp, fontWeight = FontWeight.Bold)
Text(title, color = LumaColors.text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
if (subtitle.isNotEmpty()) {
Text(subtitle, color = Color(0xFF6B7280), fontSize = 12.sp)
Text(subtitle, color = LumaColors.textSecondary, fontSize = 12.sp)
}
}
Icon(
Icons.Outlined.ChevronRight,
null,
tint = Color.LightGray,
LumaIconView(
icon = LumaIcon.ChevronRight,
contentDescription = null,
tint = LumaColors.textTertiary,
modifier = Modifier
.size(20.dp)
.then(
@@ -242,7 +293,80 @@ private fun SettingRowItem(
@Composable
private fun DividerLine() {
Box(Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 16.dp).background(Color(0xFFE5E7EB)))
Box(Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = LumaSpacing.md).background(LumaColors.divider))
}
@Composable
private fun themeModeLabel(): String = when (AppTheme.themeMode) {
1 -> stringResource(R.string.theme_dark)
2 -> stringResource(R.string.theme_system)
else -> stringResource(R.string.theme_light)
}
@Composable
private fun ThemeMenuOverlay(
anchorBounds: Rect,
rootOrigin: Offset,
backdrop: Backdrop?,
visible: Boolean,
onDismiss: () -> Unit,
onClosed: () -> Unit,
onThemeSelected: (Int) -> Unit,
) {
LiquidGlassPopupOverlay(
anchorBounds = anchorBounds,
rootOrigin = rootOrigin,
backdrop = backdrop,
visible = visible,
onDismissRequest = onDismiss,
onClosed = onClosed,
width = 190.dp,
height = 184.dp,
) {
ThemeOption(
icon = LumaIcon.Image,
label = stringResource(R.string.theme_light),
selected = AppTheme.themeMode == 0,
onClick = { onThemeSelected(0) },
)
ThemeOption(
icon = LumaIcon.Eye,
label = stringResource(R.string.theme_dark),
selected = AppTheme.themeMode == 1,
onClick = { onThemeSelected(1) },
)
ThemeOption(
icon = LumaIcon.Cloud,
label = stringResource(R.string.theme_system),
selected = AppTheme.themeMode == 2,
onClick = { onThemeSelected(2) },
)
}
}
@Composable
private fun ThemeOption(
icon: LumaIcon,
label: String,
selected: Boolean,
onClick: () -> Unit,
) {
Row(
Modifier
.fillMaxWidth()
.heightIn(min = 48.dp)
.clip(RoundedCornerShape(16.dp))
.clickable(indication = null, interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }, onClick = onClick)
.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
LumaIconView(icon, contentDescription = null, tint = if (selected) LumaColors.accent else LumaColors.textSecondary, modifier = Modifier.size(19.dp))
Spacer(Modifier.width(10.dp))
Text(label, color = LumaColors.text, fontSize = 14.sp, fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, modifier = Modifier.weight(1f))
if (selected) {
LumaIconView(LumaIcon.Check, contentDescription = stringResource(R.string.selected), tint = LumaColors.accent, modifier = Modifier.size(18.dp))
}
}
}
@Composable
@@ -264,12 +388,12 @@ private fun LanguageMenuOverlay(
hasOpened = true
progress.animateTo(
targetValue = 1f,
animationSpec = tween(180, easing = CubicBezierEasing(0.18f, 0.82f, 0.28f, 1f)),
animationSpec = tween(LumaMotion.menu, easing = LumaMotion.enter),
)
} else if (hasOpened) {
progress.animateTo(
targetValue = 0f,
animationSpec = tween(300, easing = CubicBezierEasing(0.35f, 0f, 0.72f, 0.18f)),
animationSpec = tween(LumaMotion.sheet, easing = LumaMotion.exit),
)
onClosed()
}
@@ -301,12 +425,8 @@ private fun LanguageMenuOverlay(
)
val currentMenuRect by rememberUpdatedState(menuRect)
val shape = RoundedCornerShape(cornerRadius)
val surface = if (GlassTokens.isDark) {
Color.White.copy(alpha = 0.08f)
} else {
Color.Black.copy(alpha = 0.10f)
}
val border = if (GlassTokens.isDark) Color.White.copy(alpha = 0.24f) else Color.White.copy(alpha = 0.62f)
val surface = LumaColors.floatingTint
val border = LumaColors.glassBorder
Box(
Modifier
@@ -331,25 +451,24 @@ private fun LanguageMenuOverlay(
.width(menuWidth)
.height(menuHeight)
.then(
if (backdrop != null) {
if (backdrop != null && LumaGlassRuntime.useNativeBackdrop) {
Modifier.drawBackdrop(
backdrop = backdrop,
shape = { RoundedRectangle(cornerRadius) },
effects = {
colorControls(
brightness = if (GlassTokens.isDark) 0f else 0.2f,
saturation = 1.5f,
brightness = if (GlassTokens.isDark) 0f else 0.05f,
saturation = 1.10f,
)
vibrancy()
blur(if (GlassTokens.isDark) 8.dp.toPx() else 16.dp.toPx())
lens(24.dp.toPx(), 48.dp.toPx(), depthEffect = true)
blur(if (GlassTokens.isDark) 10.dp.toPx() else 14.dp.toPx())
},
highlight = { Highlight.Plain },
shadow = { Shadow(alpha = 0.16f) },
onDrawSurface = { drawRect(surface) },
)
} else {
Modifier.background(surface)
Modifier.lumaFallbackGlass(shape, border = false)
},
)
.border(0.7.dp, border, shape)
@@ -387,6 +506,7 @@ private fun LanguageOption(
Row(
Modifier
.fillMaxWidth()
.heightIn(min = 48.dp)
.clip(RoundedCornerShape(18.dp))
.clickable(onClick = onClick)
.padding(horizontal = 10.dp, vertical = 8.dp),
@@ -397,11 +517,11 @@ private fun LanguageOption(
.size(20.dp)
.clip(CircleShape)
.background(if (selected) GlassTokens.accent else Color.Transparent)
.border(1.dp, if (selected) GlassTokens.accent else GlassTokens.subtext2, CircleShape),
.border(1.dp, if (selected) GlassTokens.accent else LumaColors.textTertiary, CircleShape),
contentAlignment = Alignment.Center,
) {
if (selected) {
Text("✓", color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.Bold)
LumaIconView(LumaIcon.Check, tint = Color.White, modifier = Modifier.size(14.dp), contentDescription = stringResource(R.string.selected))
}
}
Spacer(Modifier.width(10.dp))
File diff suppressed because it is too large. Load diff
@@ -12,13 +12,11 @@ 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.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -28,7 +26,13 @@ import com.example.myapplication.R as Res
import com.example.myapplication.ui.liquidglass.LiquidBottomTab
import com.example.myapplication.ui.liquidglass.LiquidBottomTabs
import com.example.myapplication.ui.liquidglass.LocalLiquidBottomTabsGhostLayer
import com.example.myapplication.ui.liquidglass.LocalLiquidBottomTabsPillValue
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaAnimatedIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaSpacing
import com.example.myapplication.ui.design.lumaFallbackGlass
import com.kyant.backdrop.Backdrop
import com.kyant.backdrop.drawBackdrop
import com.kyant.backdrop.effects.blur
@@ -41,89 +45,102 @@ import top.yukonga.miuix.kmp.basic.Text
fun LiquidAppBottomBar(
selectedTab: Int,
onSelect: (Int) -> Unit,
backdrop: Backdrop,
backdrop: Backdrop?,
modifier: Modifier = Modifier,
) {
val isDark = GlassTokens.isDark
val inactive = if (isDark) Color(0xFFF3F4F6) else Color(0xFF111111)
val inactive = LumaColors.text
Row(
modifier = modifier
.navigationBarsPadding()
.padding(start = 20.dp, end = 20.dp, bottom = 12.dp),
.padding(start = LumaSpacing.lg, end = LumaSpacing.lg, bottom = LumaSpacing.sm),
verticalAlignment = Alignment.CenterVertically,
) {
Box(modifier = Modifier.weight(1f)) {
LiquidBottomTabs(
selectedTabIndex = { if (selectedTab < 4) selectedTab else -1 },
selectedTabIndex = { selectedTab.coerceIn(0, 3) },
onTabSelected = onSelect,
backdrop = backdrop,
tabsCount = 4,
showSelection = { selectedTab < 4 },
) {
val tabsCount = mainTabs.size
for (index in 0 until tabsCount) {
val tab = mainTabs[index]
LiquidBottomTab(onClick = { onSelect(index) }) {
val isGhostLayer = LocalLiquidBottomTabsGhostLayer.current
val pillValue = LocalLiquidBottomTabsPillValue.current
val distance = pillValue - index.toFloat()
val pillDistance = if (distance < 0f) -distance else distance
val normalAlpha = if (isGhostLayer || pillDistance > 0.58f || selectedTab == 4) 1f else 0f
Icon(
imageVector = if (selectedTab == index) tab.selectedIcon else tab.unselectedIcon,
LumaAnimatedIcon(
icon = tab.icon,
selected = selectedTab == index,
contentDescription = stringResource(tab.labelRes),
tint = if (isGhostLayer) Color.White else inactive,
modifier = Modifier.size(21.dp).alpha(normalAlpha),
tint = if (isGhostLayer) Color.White else if (selectedTab == index) LumaColors.accent else inactive,
modifier = Modifier.size(36.dp),
)
Spacer(Modifier.height(2.dp))
Spacer(Modifier.height(3.dp))
Text(
text = stringResource(tab.labelRes),
color = if (isGhostLayer) Color.White else inactive,
color = if (isGhostLayer) Color.White else if (selectedTab == index) LumaColors.accent else inactive,
fontSize = 10.sp,
fontWeight = FontWeight.Medium,
maxLines = 1,
modifier = Modifier.alpha(normalAlpha),
)
}
}
}
}
Spacer(Modifier.width(12.dp))
Spacer(Modifier.width(LumaSpacing.sm))
val searchSelected = selectedTab == 4
val safeBackdrop = backdrop?.takeIf { LumaGlassRuntime.useNativeBackdrop }
Box(
modifier = Modifier
.size(width = 64.dp, height = 64.dp)
.drawBackdrop(
backdrop = backdrop,
shape = { Capsule() },
effects = {
vibrancy()
blur(2f.dp.toPx())
lens(12f.dp.toPx(), 24f.dp.toPx())
},
onDrawSurface = {
if (searchSelected) {
drawRect(Color.White.copy(alpha = 0.18f))
}
.clip(CircleShape)
.then(
if (safeBackdrop != null) {
Modifier.drawBackdrop(
backdrop = safeBackdrop,
shape = { Capsule() },
effects = {
vibrancy()
blur(2f.dp.toPx())
lens(12f.dp.toPx(), 16f.dp.toPx())
},
onDrawSurface = {
if (searchSelected) {
drawRect(LumaColors.glassHighlight.copy(alpha = 0.40f))
}
},
)
} else {
Modifier.lumaFallbackGlass(CircleShape, selected = searchSelected)
},
)
.border(1.dp, if (searchSelected) Color.LightGray else Color.White.copy(alpha = 0.3f), CircleShape)
.clickable { onSelect(4) },
.then(
if (safeBackdrop != null) {
Modifier.border(
0.6.dp,
if (searchSelected) LumaColors.glassBorder else LumaColors.glassBorder.copy(alpha = 0.72f),
CircleShape,
)
} else {
Modifier
},
)
.clickable(indication = null, interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }) { onSelect(4) },
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = Icons.Filled.Search,
LumaIconView(
icon = LumaIcon.Search,
contentDescription = null,
tint = if (searchSelected) Color(0xFF1F2937) else inactive,
modifier = Modifier.size(24.dp),
tint = if (searchSelected) LumaColors.accent else inactive,
modifier = Modifier.size(41.dp),
)
Text(
text = stringResource(Res.string.search),
color = if (searchSelected) Color(0xFF1F2937) else inactive,
color = if (searchSelected) LumaColors.accent else inactive,
fontSize = 10.sp,
fontWeight = FontWeight.ExtraBold,
)
@@ -1,11 +1,15 @@
package com.example.myapplication.ui
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
object AppTheme {
private const val preferencesName = "app_settings"
private const val themeKey = "theme"
// 0 = Light, 1 = Dark, 2 = Follow System
var themeMode by mutableIntStateOf(0)
var systemIsDark by mutableStateOf(false)
@@ -16,4 +20,20 @@ object AppTheme {
1 -> true
else -> systemIsDark
}
}
fun init(context: Context) {
themeMode = context
.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
.getInt(themeKey, 0)
.coerceIn(0, 2)
}
fun setMode(context: Context, mode: Int) {
themeMode = mode.coerceIn(0, 2)
context
.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
.edit()
.putInt(themeKey, themeMode)
.apply()
}
}
@@ -1,20 +1,14 @@
package com.example.myapplication.ui
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect
import com.kyant.backdrop.Backdrop
import com.kyant.backdrop.drawBackdrop
import com.kyant.backdrop.effects.blur
import com.kyant.backdrop.effects.colorControls
import com.kyant.backdrop.effects.lens
import com.kyant.backdrop.effects.vibrancy
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.shadow.Shadow
@@ -56,6 +50,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawBehind
@@ -68,7 +63,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.text.font.FontWeight
@@ -79,6 +73,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Dp
@@ -88,41 +83,33 @@ import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlin.math.roundToInt
import androidx.compose.ui.zIndex
import androidx.compose.material.icons.outlined.ChevronRight
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.material3.Icon
import top.yukonga.miuix.kmp.basic.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Apps
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.outlined.GridView
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Storage
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaGlassLevel
import com.example.myapplication.ui.design.LumaMotion
import com.example.myapplication.ui.design.LumaShapes
import com.example.myapplication.ui.design.LumaTypography
import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.lumaGlass
import com.example.myapplication.ui.design.lumaFallbackGlass
internal data class NavTab(
val selectedIcon: ImageVector,
val unselectedIcon: ImageVector,
val icon: LumaIcon,
val labelRes: Int,
)
internal val mainTabs = listOf(
NavTab(Icons.Filled.Home, Icons.Outlined.Home, Res.string.nav_home),
NavTab(Icons.Filled.Favorite, Icons.Outlined.FavoriteBorder, Res.string.nav_records),
NavTab(Icons.Filled.Storage, Icons.Outlined.Storage, Res.string.nav_library),
NavTab(Icons.Filled.Settings, Icons.Outlined.Settings, Res.string.nav_settings),
NavTab(LumaIcon.Home, Res.string.nav_home),
NavTab(LumaIcon.Favorite, Res.string.nav_records),
NavTab(LumaIcon.Resources, Res.string.nav_library),
NavTab(LumaIcon.Settings, Res.string.nav_settings),
)
val LocalGlassBackdrop = staticCompositionLocalOf<Backdrop?> { null }
@@ -135,37 +122,34 @@ fun bottomBarContentPadding(extra: Dp = 86.dp): PaddingValues {
object GlassTokens {
val isDark get() = AppTheme.isDark
val text: Color get() = if (isDark) Color(0xFFF2F2F7) else Color(0xFF1C1C1E)
val subtext: Color get() = if (isDark) Color(0xFF98989F) else Color(0xFF8E8E93)
val subtext2: Color get() = if (isDark) Color(0xFF636366) else Color(0xFFAEAEB2)
val accent = Color(0xFF0A84FF)
val accentNav = Color(0xFF0A84FF)
val danger = Color(0xFFFF3B30)
val dangerDark = Color(0xFFD70015)
val navIconSelected: Color get() = if (isDark) Color(0xFFF2F2F7) else Color(0xFF1C1C1E)
val navIconUnselected: Color get() = if (isDark) Color(0xFF98989F) else Color(0xFF8E8E93)
val navPillSelected: Color get() = if (isDark) Color(0xFF48484A) else Color(0xFFE5E5EA)
val separator: Color get() = if (isDark) Color(0xFF38383A) else Color(0xFFD1D1D6)
val text: Color get() = LumaColors.text
val subtext: Color get() = LumaColors.textSecondary
val subtext2: Color get() = LumaColors.textTertiary
val accent: Color get() = LumaColors.accent
val accentNav: Color get() = LumaColors.accent
val danger: Color get() = LumaColors.danger
val dangerDark: Color get() = LumaColors.danger
val navIconSelected: Color get() = LumaColors.text
val navIconUnselected: Color get() = LumaColors.textSecondary
val navPillSelected: Color get() = LumaColors.strongTint
val separator: Color get() = LumaColors.divider
val glassFill: Color get() = if (isDark) Color(0xFF1C1C20).copy(alpha = 0.55f) else Color.White.copy(alpha = 0.42f)
val glassFillStrong: Color get() = if (isDark) Color(0xFF2C2C30).copy(alpha = 0.70f) else Color.White.copy(alpha = 0.58f)
val glassFillSoft: Color get() = if (isDark) Color(0xFF1C1C20).copy(alpha = 0.35f) else Color.White.copy(alpha = 0.28f)
val glassBorder: Color get() = if (isDark) Color(0xFF5A5A60).copy(alpha = 0.50f) else Color.White.copy(alpha = 0.65f)
val glassShadow: Color get() = if (isDark) Color.Black.copy(alpha = 0.25f) else Color(0xFF4A3B6B).copy(alpha = 0.08f)
val glassFill: Color get() = LumaColors.regularTint
val glassFillStrong: Color get() = LumaColors.strongTint
val glassFillSoft: Color get() = LumaColors.clearTint
val glassBorder: Color get() = LumaColors.glassBorder
val glassShadow: Color get() = LumaColors.glassShadow
val navBg: Color get() = if (isDark) Color(0xFF1C1C20).copy(alpha = 0.70f) else Color.White.copy(alpha = 0.60f)
val navSelectedPill: Color get() = if (isDark) Color(0xFF48484A).copy(alpha = 0.80f) else Color.White.copy(alpha = 0.75f)
val navBgTop: Color get() = if (isDark) Color(0xFF3A3A44).copy(alpha = 0.85f) else Color.White.copy(alpha = 0.65f)
val navBgBottom: Color get() = if (isDark) Color(0xFF2A2A32).copy(alpha = 0.80f) else Color.White.copy(alpha = 0.42f)
val navBorder: Color get() = if (isDark) Color(0xFF6A6A78).copy(alpha = 0.60f) else Color.White.copy(alpha = 0.70f)
val navBg: Color get() = LumaColors.floatingTint
val navSelectedPill: Color get() = LumaColors.strongTint
val navBgTop: Color get() = LumaColors.regularTint
val navBgBottom: Color get() = LumaColors.clearTint
val navBorder: Color get() = LumaColors.glassBorder
}
@Composable
fun LiquidGlassBackground(modifier: Modifier = Modifier) {
val density = androidx.compose.ui.platform.LocalDensity.current
val dark = AppTheme.isDark
val bgColor = if (dark) Color(0xFF0F1115) else Color(0xFFF2F4F7)
Box(modifier.fillMaxSize().background(bgColor))
Box(modifier.fillMaxSize().background(LumaColors.background))
}
@Composable
@@ -183,40 +167,17 @@ fun GlassSurface(
) {
Box(
modifier
.shadow(
elevation = shadowElevation,
shape = RoundedCornerShape(cornerRadius),
ambientColor = GlassTokens.glassShadow,
spotColor = GlassTokens.glassShadow,
)
.clip(RoundedCornerShape(cornerRadius))
.background(fill)
.then(
if (border != Color.Unspecified) {
Modifier.border(0.5.dp, border, RoundedCornerShape(cornerRadius))
} else {
Modifier
}
.lumaGlass(
backdrop = LocalGlassBackdrop.current,
radius = cornerRadius,
level = LumaGlassLevel.Regular,
border = border != Color.Unspecified,
tint = fill,
borderColor = border.takeUnless { it == Color.Unspecified },
shadowElevation = shadowElevation,
highlight = highlight,
)
) {
if (highlight) {
val hl = if (GlassTokens.isDark) 0.06f else 0.50f
val hlSoft = if (GlassTokens.isDark) 0.03f else 0.22f
val sheen = if (GlassTokens.isDark) 0.02f else 0.10f
Box(
Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
0f to Color.White.copy(alpha = hl),
0.04f to Color.White.copy(alpha = hlSoft),
0.15f to Color.Transparent,
0.85f to Color.Transparent,
1f to Color.White.copy(alpha = sheen),
)
)
)
}
Column(
Modifier.padding(padding),
horizontalAlignment = horizontalAlignment,
@@ -241,110 +202,6 @@ fun LiquidGlassTopBar(
}
}
@Composable
fun SectionHeader(
title: String,
modifier: Modifier = Modifier,
actionLabel: String? = null,
onAction: (() -> Unit)? = null,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = title,
color = GlassTokens.text,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
)
if (actionLabel != null) {
Box(
Modifier
.clickable(enabled = onAction != null) { onAction?.invoke() }
.padding(4.dp)
) {
Text(
text = actionLabel,
color = GlassTokens.accent,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
)
}
}
}
}
@Composable
fun PosterBox(
colors: List<Color>,
title: String,
subtitle: String = "",
modifier: Modifier = Modifier,
height: androidx.compose.ui.unit.Dp = 150.dp,
progress: Float? = null,
) {
Box(
modifier
.fillMaxWidth()
.height(height)
.clip(RoundedCornerShape(16.dp))
.background(
Brush.verticalGradient(
colors + listOf(colors.last().copy(alpha = 0.9f))
)
)
) {
Box(
Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
0f to Color.White.copy(alpha = 0.22f),
0.4f to Color.Transparent,
)
)
)
Column(
Modifier
.align(Alignment.BottomStart)
.padding(12.dp)
) {
Text(
text = title,
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
if (subtitle.isNotEmpty()) {
Spacer(Modifier.height(2.dp))
Text(
text = subtitle,
color = Color.White.copy(alpha = 0.82f),
fontSize = 11.sp,
)
}
}
if (progress != null) {
Box(
Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
.height(3.dp)
.background(Color.White.copy(alpha = 0.35f))
) {
Box(
Modifier
.fillMaxWidth(progress.coerceIn(0f, 1f))
.height(3.dp)
.background(Color.White)
)
}
}
}
}
@Composable
fun SegmentedControl(
items: List<String>,
@@ -353,26 +210,26 @@ fun SegmentedControl(
modifier: Modifier = Modifier,
) {
val backdrop = LocalGlassBackdrop.current
val containerShape = RoundedCornerShape(20.dp)
Row(
modifier
.clip(RoundedCornerShape(20.dp))
.clip(containerShape)
.then(
if (backdrop != null) {
if (backdrop != null && LumaGlassRuntime.useNativeBackdrop) {
Modifier.drawBackdrop(
backdrop = backdrop,
shape = { RoundedRectangle(20.dp) },
effects = {
vibrancy()
blur(8.dp.toPx())
lens(14.dp.toPx(), 14.dp.toPx())
},
onDrawSurface = { drawRect(GlassTokens.glassFillSoft) },
)
} else {
Modifier.background(GlassTokens.glassFillSoft)
Modifier.lumaFallbackGlass(containerShape, border = false)
}
)
.border(0.5.dp, GlassTokens.glassBorder, RoundedCornerShape(20.dp))
.border(0.5.dp, GlassTokens.glassBorder, containerShape)
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
@@ -380,17 +237,17 @@ fun SegmentedControl(
val sel = selected == index
val selectedFill by animateColorAsState(
targetValue = if (sel) GlassTokens.glassFillStrong else Color.Transparent,
animationSpec = tween(180),
animationSpec = tween(LumaMotion.fast),
label = "segmentFill$index",
)
val selectedText by animateColorAsState(
targetValue = if (sel) GlassTokens.text else GlassTokens.subtext,
animationSpec = tween(180),
animationSpec = tween(LumaMotion.fast),
label = "segmentText$index",
)
val selectedProgress by animateFloatAsState(
targetValue = if (sel) 1f else 0f,
animationSpec = tween(180),
animationSpec = tween(LumaMotion.fast),
label = "segmentProgress$index",
)
Box(
@@ -398,14 +255,13 @@ fun SegmentedControl(
.weight(1f)
.clip(RoundedCornerShape(16.dp))
.then(
if (sel && backdrop != null) {
if (sel && backdrop != null && LumaGlassRuntime.useNativeBackdrop) {
Modifier.drawBackdrop(
backdrop = backdrop,
shape = { RoundedRectangle(16.dp) },
effects = {
vibrancy()
blur(6.dp.toPx())
lens(10.dp.toPx(), 10.dp.toPx())
},
onDrawSurface = { drawRect(selectedFill) },
)
@@ -421,8 +277,8 @@ fun SegmentedControl(
}
)
.then(
if (sel && backdrop == null) {
Modifier.background(selectedFill)
if (sel && (backdrop == null || !LumaGlassRuntime.useNativeBackdrop)) {
Modifier.lumaFallbackGlass(RoundedCornerShape(16.dp), selected = true, border = false)
} else {
Modifier
}
@@ -456,32 +312,27 @@ fun GlassSearchField(
modifier: Modifier = Modifier,
) {
val backdrop = LocalGlassBackdrop.current
var focused by remember { mutableStateOf(false) }
val borderColor by animateColorAsState(
targetValue = if (focused) GlassTokens.accent.copy(alpha = 0.72f) else GlassTokens.glassBorder,
animationSpec = tween(LumaMotion.fast),
label = "searchFieldFocus",
)
Row(
modifier
.fillMaxWidth()
.clip(RoundedCornerShape(22.dp))
.then(
if (backdrop != null) {
Modifier.drawBackdrop(
backdrop = backdrop,
shape = { RoundedRectangle(22.dp) },
effects = {
vibrancy()
blur(8.dp.toPx())
lens(12.dp.toPx(), 12.dp.toPx())
},
onDrawSurface = { drawRect(GlassTokens.glassFillSoft) },
)
} else {
Modifier.background(GlassTokens.glassFillSoft)
}
.lumaGlass(
backdrop = backdrop,
radius = LumaShapes.large,
level = LumaGlassLevel.Regular,
borderColor = borderColor,
shadowElevation = 0.dp,
)
.border(0.5.dp, GlassTokens.glassBorder, RoundedCornerShape(22.dp))
.padding(horizontal = 14.dp, vertical = 13.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Search,
LumaIconView(
icon = LumaIcon.Search,
contentDescription = null,
tint = GlassTokens.subtext,
modifier = Modifier.size(20.dp),
@@ -495,7 +346,7 @@ fun GlassSearchField(
color = GlassTokens.text,
fontSize = 15.sp,
),
modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f).onFocusChanged { focused = it.isFocused },
decorationBox = { inner ->
if (value.isEmpty()) {
Text(text = placeholder, color = GlassTokens.subtext, fontSize = 15.sp)
@@ -504,6 +355,25 @@ fun GlassSearchField(
}
},
)
if (value.isNotEmpty()) {
Box(
Modifier
.size(48.dp)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { onValueChange("") },
),
contentAlignment = Alignment.Center,
) {
LumaIconView(
icon = LumaIcon.Close,
contentDescription = stringResource(Res.string.clear_search),
tint = GlassTokens.subtext,
modifier = Modifier.size(18.dp),
)
}
}
}
}
@@ -528,12 +398,12 @@ fun LiquidGlassPopupOverlay(
if (visible) {
progress.animateTo(
targetValue = 1f,
animationSpec = tween(180, easing = CubicBezierEasing(0.18f, 0.82f, 0.28f, 1f)),
animationSpec = tween(LumaMotion.menu, easing = LumaMotion.enter),
)
} else {
progress.animateTo(
targetValue = 0f,
animationSpec = tween(260, easing = CubicBezierEasing(0.35f, 0f, 0.72f, 0.18f)),
animationSpec = tween(LumaMotion.sheet, easing = LumaMotion.exit),
)
currentOnClosed()
}
@@ -587,7 +457,7 @@ fun LiquidGlassPopupOverlay(
.height(menuHeight)
.clip(shape)
.then(
if (backdrop != null) {
if (backdrop != null && LumaGlassRuntime.useNativeBackdrop) {
Modifier.drawBackdrop(
backdrop = backdrop,
shape = { RoundedRectangle(cornerRadius) },
@@ -598,14 +468,13 @@ fun LiquidGlassPopupOverlay(
)
vibrancy()
blur(if (GlassTokens.isDark) 8.dp.toPx() else 16.dp.toPx())
lens(18.dp.toPx(), 32.dp.toPx(), depthEffect = true)
},
highlight = { Highlight.Plain },
shadow = { Shadow(alpha = 0.18f) },
onDrawSurface = { drawRect(surface) },
)
} else {
Modifier.background(surface)
Modifier.lumaFallbackGlass(shape, border = false)
}
)
.border(0.7.dp, border, shape)
@@ -627,21 +496,20 @@ fun LiquidGlassPopupOverlay(
@Composable
fun GlassPopupItem(
text: String,
icon: ImageVector? = null,
tint: Color = Color(0xFF374151),
icon: LumaIcon,
tint: Color = LumaColors.text,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.heightIn(min = 48.dp)
.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
if (icon != null) {
Icon(imageVector = icon, contentDescription = null, tint = tint, modifier = Modifier.size(18.dp))
}
LumaIconView(icon, contentDescription = null, tint = tint, modifier = Modifier.size(18.dp))
Text(text = text, color = tint, fontSize = 14.sp, fontWeight = FontWeight.Medium)
}
}
@@ -657,27 +525,28 @@ fun LiquidGlassButton(
) {
val backdrop = LocalGlassBackdrop.current
val shape = RoundedCornerShape(cornerRadius)
val interactionSource = remember { MutableInteractionSource() }
val pressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (pressed) 0.97f else 1f,
animationSpec = tween(LumaMotion.fast),
label = "liquidGlassButtonPress",
)
Row(
modifier
.clip(shape)
.then(
if (backdrop != null) {
Modifier.drawBackdrop(
backdrop = backdrop,
shape = { RoundedRectangle(cornerRadius) },
effects = {
vibrancy()
blur(8.dp.toPx())
lens(12.dp.toPx(), 12.dp.toPx())
},
onDrawSurface = { drawRect(fill) },
)
} else {
Modifier.background(fill)
}
.graphicsLayer {
scaleX = scale
scaleY = scale
alpha = if (enabled) 1f else 0.52f
}
.lumaGlass(
backdrop = backdrop,
radius = cornerRadius,
level = LumaGlassLevel.Interactive,
tint = fill,
borderColor = GlassTokens.glassBorder,
)
.border(0.5.dp, GlassTokens.glassBorder, shape)
.clickable(enabled = enabled, onClick = onClick)
.clickable(enabled = enabled, indication = null, interactionSource = interactionSource, onClick = onClick)
.padding(horizontal = 16.dp, vertical = 11.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
@@ -725,7 +594,6 @@ fun GlassDialog(
val dialogCtx = LocalContext.current
val localizedCtx = remember(AppLanguage.language) { AppLanguage.getLocalizedContext(dialogCtx) }
val glassBackdrop = backdrop ?: LocalGlassBackdrop.current
val shape = RoundedCornerShape(24.dp)
CompositionLocalProvider(
LocalContext provides localizedCtx,
LocalGlassBackdrop provides glassBackdrop,
@@ -733,38 +601,13 @@ fun GlassDialog(
Box(
modifier
.width(IntrinsicSize.Min)
.clip(shape)
.then(
if (glassBackdrop != null) {
Modifier.drawBackdrop(
backdrop = glassBackdrop,
shape = { RoundedRectangle(24.dp) },
effects = {
colorControls(
brightness = if (GlassTokens.isDark) 0f else 0.08f,
saturation = 1.18f,
)
vibrancy()
blur(if (GlassTokens.isDark) 10.dp.toPx() else 18.dp.toPx())
lens(24.dp.toPx(), 42.dp.toPx(), depthEffect = true)
},
highlight = { Highlight.Plain },
shadow = { Shadow(alpha = 0.20f) },
onDrawSurface = {
drawRect(
if (GlassTokens.isDark) {
Color(0xFF171A20).copy(alpha = 0.42f)
} else {
Color.White.copy(alpha = 0.24f)
}
)
},
)
} else {
Modifier.background(GlassTokens.glassFillStrong)
}
.lumaGlass(
backdrop = glassBackdrop,
radius = 24.dp,
level = LumaGlassLevel.Strong,
borderColor = GlassTokens.glassBorder,
shadowElevation = 10.dp,
)
.border(0.7.dp, GlassTokens.glassBorder, shape)
.padding(20.dp),
) {
content()
@@ -773,6 +616,80 @@ fun GlassDialog(
}
}
@Composable
fun GlassSheet(
onDismissRequest: () -> Unit,
backdrop: Backdrop? = null,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
val dialogCtx = LocalContext.current
val localizedCtx = remember(AppLanguage.language) { AppLanguage.getLocalizedContext(dialogCtx) }
val glassBackdrop = backdrop ?: LocalGlassBackdrop.current
CompositionLocalProvider(
LocalContext provides localizedCtx,
LocalGlassBackdrop provides glassBackdrop,
) {
Box(
Modifier
.fillMaxSize()
.navigationBarsPadding(),
contentAlignment = Alignment.BottomCenter,
) {
Column(
modifier
.fillMaxWidth()
.widthIn(max = 520.dp)
.lumaGlass(
backdrop = glassBackdrop,
radius = 28.dp,
level = LumaGlassLevel.Floating,
borderColor = GlassTokens.glassBorder,
shadowElevation = 12.dp,
)
.padding(20.dp),
content = content,
)
}
}
}
}
@Composable
fun GlassConfirmDialog(
title: String,
message: String,
confirmText: String,
dismissText: String,
onDismissRequest: () -> Unit,
onConfirm: () -> Unit,
backdrop: Backdrop? = null,
) {
GlassDialog(backdrop = backdrop, onDismissRequest = onDismissRequest, modifier = Modifier.width(300.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(title, color = GlassTokens.text, fontSize = LumaTypography.title, fontWeight = FontWeight.Bold)
Text(message, color = GlassTokens.subtext, fontSize = LumaTypography.body, lineHeight = 21.sp)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
GlassButton(
text = dismissText,
onClick = onDismissRequest,
modifier = Modifier.weight(1f),
)
GlassButton(
text = confirmText,
onClick = onConfirm,
modifier = Modifier.weight(1f),
danger = true,
)
}
}
}
}
@Composable
fun GlassInputField(
label: String,
@@ -788,30 +705,25 @@ fun GlassInputField(
) {
val backdrop = LocalGlassBackdrop.current
Column(modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(label, fontSize = 13.sp, color = Color(0xFF6B7280), fontWeight = FontWeight.Medium)
Text(label, fontSize = 13.sp, color = GlassTokens.subtext, fontWeight = FontWeight.Medium)
val shape = RoundedCornerShape(12.dp)
Box(
Modifier.fillMaxWidth().clip(shape)
.then(
if (backdrop != null) {
Modifier.drawBackdrop(
backdrop = backdrop,
shape = { RoundedRectangle(12.dp) },
effects = { vibrancy(); blur(8.dp.toPx()); lens(10.dp.toPx(), 18.dp.toPx()) },
onDrawSurface = { drawRect(GlassTokens.glassFillSoft) },
)
} else {
Modifier.background(GlassTokens.glassFillSoft)
}
Modifier.fillMaxWidth()
.lumaGlass(
backdrop = backdrop,
radius = 12.dp,
level = LumaGlassLevel.Clear,
tint = GlassTokens.glassFillSoft,
border = false,
)
.border(1.dp, if (isError) GlassTokens.danger else GlassTokens.glassBorder, shape)
.border(0.7.dp, if (isError) GlassTokens.danger else GlassTokens.glassBorder, shape)
.padding(horizontal = 14.dp, vertical = 12.dp),
) {
BasicTextField(
value = value,
onValueChange = onValueChange,
singleLine = true,
textStyle = androidx.compose.ui.text.TextStyle(color = Color(0xFF111827), fontSize = 15.sp),
textStyle = androidx.compose.ui.text.TextStyle(color = GlassTokens.text, fontSize = 15.sp),
modifier = Modifier.fillMaxWidth().then(
focusRequester?.let { Modifier.focusRequester(it) } ?: Modifier
),
@@ -819,7 +731,7 @@ fun GlassInputField(
decorationBox = { inner ->
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.weight(1f)) {
if (value.text.isEmpty()) Text(placeholder, color = Color(0xFF9CA3AF), fontSize = 15.sp)
if (value.text.isEmpty()) Text(placeholder, color = GlassTokens.subtext2, fontSize = 15.sp)
inner()
}
if (trailingContent != null) {
@@ -830,56 +742,12 @@ fun GlassInputField(
}
)
}
}
}
@Composable
fun GlassSettingRow(
icon: ImageVector,
iconTint: Color,
bgTint: Color,
title: String,
subtitle: String = "",
onClick: () -> Unit = {},
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(vertical = 12.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
Modifier
.size(38.dp)
.clip(RoundedCornerShape(11.dp))
.background(bgTint.copy(alpha = 0.20f))
.border(0.5.dp, GlassTokens.glassBorder, RoundedCornerShape(11.dp)),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = iconTint,
modifier = Modifier.size(19.dp),
)
}
Spacer(Modifier.width(14.dp))
Column {
Text(text = title, color = GlassTokens.text, fontSize = 15.sp, fontWeight = FontWeight.Medium)
if (subtitle.isNotEmpty()) {
Spacer(Modifier.height(2.dp))
Text(text = subtitle, color = GlassTokens.subtext, fontSize = 12.sp)
}
}
if (isError && !errorMessage.isNullOrBlank()) {
Text(
text = errorMessage,
color = GlassTokens.danger,
fontSize = LumaTypography.caption,
)
}
Icon(
imageVector = Icons.Outlined.ChevronRight,
contentDescription = null,
tint = Color(0xFF9CA3AF),
modifier = Modifier.size(20.dp),
)
}
}
@@ -0,0 +1,374 @@
package com.example.myapplication.ui.design
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.LinearEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.setProgress
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import top.yukonga.miuix.kmp.basic.Text
import kotlin.math.abs
@Composable
fun LumaAsyncImage(
model: Any?,
contentDescription: String?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Crop,
) {
val fallback = ColorPainter(LumaColors.backgroundElevated)
AsyncImage(
model = model,
contentDescription = contentDescription,
contentScale = contentScale,
modifier = modifier,
placeholder = fallback,
error = fallback,
)
}
@Composable
fun LumaIconButton(
icon: LumaIcon,
contentDescription: String?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
selected: Boolean = false,
tint: Color = LumaColors.text,
radius: Dp = 18.dp,
level: LumaGlassLevel = LumaGlassLevel.Floating,
iconSize: Dp = 23.dp,
enabled: Boolean = true,
) {
val interactionSource = remember { MutableInteractionSource() }
val pressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (pressed) 0.94f else 1f,
animationSpec = tween(LumaMotion.fast),
label = "lumaIconPress",
)
Box(
modifier
.size(48.dp)
.graphicsLayer { scaleX = scale; scaleY = scale }
.lumaGlass(radius = radius, level = level)
.clickable(
enabled = enabled,
interactionSource = interactionSource,
indication = null,
onClick = onClick,
),
contentAlignment = Alignment.Center,
) {
LumaAnimatedIcon(
icon = icon,
selected = selected,
tint = tint,
contentDescription = contentDescription,
modifier = Modifier.size(iconSize),
)
}
}
@Composable
fun LumaSectionTitle(
title: String,
modifier: Modifier = Modifier,
action: String? = null,
onAction: (() -> Unit)? = null,
) {
Row(
modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(title, color = LumaColors.text, fontSize = LumaTypography.title, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold)
if (action != null) {
Box(
Modifier
.heightIn(min = 48.dp)
.clickable(
enabled = onAction != null,
indication = null,
interactionSource = remember { MutableInteractionSource() },
) { onAction?.invoke() },
contentAlignment = Alignment.Center,
) {
Text(action, color = LumaColors.accent, fontSize = LumaTypography.secondary)
}
}
}
}
@Composable
fun LumaSeekBar(
value: Float,
onValueChange: (Float) -> Unit,
onValueChangeFinished: () -> Unit,
modifier: Modifier = Modifier,
activeColor: Color = LumaColors.accent,
trackColor: Color = LumaColors.divider,
thumbColor: Color = activeColor,
bufferColor: Color = thumbColor.copy(alpha = 0.35f),
bufferedValue: Float = value,
) {
val density = LocalDensity.current
var trackWidthPx by remember { mutableFloatStateOf(0f) }
var dragging by remember { mutableStateOf(false) }
val currentValue by rememberUpdatedState(value.coerceIn(0f, 1f))
val currentBuffered by rememberUpdatedState(bufferedValue.coerceIn(0f, 1f))
val currentOnValueChange by rememberUpdatedState(onValueChange)
val currentOnValueChangeFinished by rememberUpdatedState(onValueChangeFinished)
val thumbRadius = with(density) { if (dragging) 7.dp.toPx() else 4.dp.toPx() }
fun updateFromPosition(x: Float) {
if (trackWidthPx > 0f) currentOnValueChange((x / trackWidthPx).coerceIn(0f, 1f))
}
Canvas(
modifier
.heightIn(min = 48.dp)
.onSizeChanged { trackWidthPx = it.width.toFloat() }
.semantics {
progressBarRangeInfo = ProgressBarRangeInfo(currentValue, 0f..1f)
setProgress { requestedValue ->
currentOnValueChange(requestedValue.coerceIn(0f, 1f))
currentOnValueChangeFinished()
true
}
}
.pointerInput(trackWidthPx) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
dragging = true
updateFromPosition(down.position.x)
var lastX = down.position.x
var moved = false
while (true) {
val event = awaitPointerEvent()
val change = event.changes.firstOrNull() ?: break
if (change.changedToUp()) {
updateFromPosition(change.position.x)
change.consume()
dragging = false
currentOnValueChangeFinished()
break
}
val delta = change.position.x - lastX
if (!moved && abs(change.position.x - down.position.x) > viewConfiguration.touchSlop) {
moved = true
}
if (moved || delta != 0f) {
updateFromPosition(change.position.x)
change.consume()
}
lastX = change.position.x
}
dragging = false
}
},
) {
val centerY = size.height / 2f
val trackHeight = 4.dp.toPx()
val progressWidth = size.width * currentValue
val bufferedWidth = size.width * currentBuffered
val radius = trackHeight / 2f
drawRoundRect(
color = trackColor,
topLeft = Offset(0f, centerY - radius),
size = Size(size.width, trackHeight),
cornerRadius = androidx.compose.ui.geometry.CornerRadius(radius),
)
if (bufferedWidth > progressWidth) {
drawRoundRect(
color = bufferColor,
topLeft = Offset(0f, centerY - radius),
size = Size(bufferedWidth, trackHeight),
cornerRadius = androidx.compose.ui.geometry.CornerRadius(radius),
)
}
drawRoundRect(
color = activeColor,
topLeft = Offset(0f, centerY - radius),
size = Size(progressWidth, trackHeight),
cornerRadius = androidx.compose.ui.geometry.CornerRadius(radius),
)
drawCircle(
color = thumbColor,
radius = thumbRadius,
center = Offset(progressWidth.coerceIn(0f, size.width), centerY),
)
}
}
@Composable
fun LumaSkeleton(
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(LumaShapes.small),
) {
val transition = rememberInfiniteTransition(label = "lumaSkeleton")
val alpha by transition.animateFloat(
initialValue = 0.52f,
targetValue = 0.86f,
animationSpec = infiniteRepeatable(
animation = tween(900, easing = LinearEasing),
repeatMode = RepeatMode.Reverse,
),
label = "lumaSkeletonAlpha",
)
Box(
modifier
.clip(shape)
.background(LumaColors.backgroundElevated)
.graphicsLayer { this.alpha = alpha },
)
}
@Composable
fun LumaLoading(
modifier: Modifier = Modifier,
label: String? = null,
) {
Column(
modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(LumaSpacing.sm),
) {
CircularProgressIndicator(
modifier = Modifier.size(28.dp),
color = LumaColors.accent,
strokeWidth = 2.5.dp,
)
if (label != null) Text(label, color = LumaColors.textSecondary, fontSize = LumaTypography.secondary)
}
}
@Composable
fun LumaEmptyState(
icon: LumaIcon,
title: String,
description: String,
modifier: Modifier = Modifier,
actionLabel: String? = null,
onAction: (() -> Unit)? = null,
) {
Column(
modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(LumaSpacing.sm),
) {
Box(
Modifier
.size(72.dp)
.lumaGlass(radius = 24.dp, level = LumaGlassLevel.Clear),
contentAlignment = Alignment.Center,
) {
LumaIconView(icon, modifier = Modifier.size(30.dp), tint = LumaColors.textTertiary)
}
Text(title, color = LumaColors.text, fontSize = LumaTypography.body, fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold)
Text(description, color = LumaColors.textSecondary, fontSize = LumaTypography.secondary)
if (actionLabel != null) {
Spacer(Modifier.size(LumaSpacing.xxs))
Box(
Modifier
.heightIn(min = 48.dp)
.clickable(
enabled = onAction != null,
indication = null,
interactionSource = remember { MutableInteractionSource() },
) { onAction?.invoke() },
contentAlignment = Alignment.Center,
) {
Text(actionLabel, color = LumaColors.accent, fontSize = LumaTypography.secondary)
}
}
}
}
@Composable
fun LumaErrorState(
title: String,
description: String,
retryLabel: String,
onRetry: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(LumaSpacing.sm),
) {
Box(
Modifier
.size(72.dp)
.lumaGlass(radius = 24.dp, level = LumaGlassLevel.Clear),
contentAlignment = Alignment.Center,
) {
LumaIconView(LumaIcon.Warning, modifier = Modifier.size(30.dp), tint = LumaColors.danger)
}
Text(title, color = LumaColors.text, fontSize = LumaTypography.body, fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold)
Text(description, color = LumaColors.textSecondary, fontSize = LumaTypography.secondary)
Box(
Modifier
.heightIn(min = 48.dp)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = onRetry,
)
.padding(horizontal = LumaSpacing.md),
contentAlignment = Alignment.Center,
) {
Text(retryLabel, color = LumaColors.accent, fontSize = LumaTypography.secondary, fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold)
}
}
}
@@ -0,0 +1,397 @@
package com.example.myapplication.ui.design
import android.os.Build
import android.view.Choreographer
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.animation.core.CubicBezierEasing
import com.example.myapplication.ui.AppTheme
import com.example.myapplication.ui.LocalGlassBackdrop
import com.kyant.backdrop.Backdrop
import com.kyant.backdrop.backdrops.LayerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import com.kyant.backdrop.drawBackdrop
import com.kyant.backdrop.effects.blur
import com.kyant.backdrop.effects.colorControls
import com.kyant.backdrop.effects.vibrancy
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.shadow.Shadow
import com.kyant.shapes.RoundedRectangle
object LumaSpacing {
val xxs = 4.dp
val xs = 8.dp
val sm = 12.dp
val md = 16.dp
val lg = 20.dp
val xl = 24.dp
val xxl = 32.dp
}
object LumaShapes {
val extraSmall = 10.dp
val small = 14.dp
val medium = 18.dp
val large = 24.dp
val extraLarge = 30.dp
}
object LumaTypography {
val display: TextUnit = 32.sp
val titleLarge: TextUnit = 22.sp
val title: TextUnit = 18.sp
val body: TextUnit = 15.sp
val secondary: TextUnit = 13.sp
val caption: TextUnit = 11.sp
}
object LumaMotion {
const val menu = 140
const val icon = 240
const val tab = 280
const val dialog = 220
const val fast = 180
const val standard = 240
const val page = 320
const val pagePop = 280
const val sheet = 300
const val slow = 380
val enter = CubicBezierEasing(0.18f, 0.82f, 0.28f, 1f)
val exit = CubicBezierEasing(0.35f, 0f, 0.72f, 0.18f)
val native = CubicBezierEasing(0.2f, 0.75f, 0.25f, 1f)
}
/** Kyant Backdrop uses RenderEffect/GraphicsLayer and is unstable on some emulator GPUs. */
internal object LumaGlassRuntime {
val isBackdropSafe: Boolean by lazy {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return@lazy false
val fingerprint = Build.FINGERPRINT.lowercase()
val board = Build.BOARD.lowercase()
val hardware = Build.HARDWARE.lowercase()
val product = Build.PRODUCT.lowercase()
val device = Build.DEVICE.lowercase()
val model = Build.MODEL.lowercase()
val brand = Build.BRAND.lowercase()
val manufacturer = Build.MANUFACTURER.lowercase()
val isX86Emulator = Build.SUPPORTED_ABIS.any { it.contains("x86") }
!(isX86Emulator ||
fingerprint.startsWith("generic") ||
fingerprint.contains("emulator") ||
board.contains("goldfish") ||
board.contains("ranchu") ||
board.contains("emulator") ||
hardware.contains("goldfish") ||
hardware.contains("ranchu") ||
hardware.contains("vbox") ||
product.contains("sdk_gphone") ||
product.contains("emulator") ||
device.contains("generic") ||
device.contains("emulator") ||
device.contains("emu") ||
device.contains("vbox") ||
model.contains("sdk_gphone") ||
model.contains("emulator") ||
model.contains("android sdk built for") ||
brand == "generic" ||
manufacturer.contains("genymotion"))
}
private val frameRateState = mutableFloatStateOf(0f)
private var measuredSamples = 0
private var frameCallback: Choreographer.FrameCallback? = null
private var sampleStartNanos = 0L
private var frameCount = 0
val useNativeBackdrop: Boolean
get() = isBackdropSafe && measuredSamples > 0 && frameRateState.floatValue >= 48f
fun startFrameMonitor() {
if (frameCallback != null) return
sampleStartNanos = 0L
frameCount = 0
measuredSamples = 0
frameRateState.floatValue = 0f
val callback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (sampleStartNanos == 0L) {
sampleStartNanos = frameTimeNanos
} else {
frameCount++
val elapsedNanos = frameTimeNanos - sampleStartNanos
if (elapsedNanos >= 500_000_000L) {
frameRateState.floatValue =
(frameCount * 1_000_000_000f / elapsedNanos.toFloat()).coerceIn(1f, 120f)
measuredSamples++
sampleStartNanos = frameTimeNanos
frameCount = 0
}
}
Choreographer.getInstance().postFrameCallback(this)
}
}
frameCallback = callback
Choreographer.getInstance().postFrameCallback(callback)
}
fun stopFrameMonitor() {
frameCallback?.let(Choreographer.getInstance()::removeFrameCallback)
frameCallback = null
measuredSamples = 0
frameRateState.floatValue = 0f
}
}
@Composable
fun rememberLumaLayerBackdrop(): LayerBackdrop? {
return if (LumaGlassRuntime.useNativeBackdrop) rememberLayerBackdrop() else null
}
object LumaColors {
val isDark: Boolean get() = AppTheme.isDark
val background: Color
get() = if (isDark) Color(0xFF0B0E12) else Color(0xFFF5F6F8)
val backgroundElevated: Color
get() = if (isDark) Color(0xFF131820) else Color(0xFFEEF1F5)
val text: Color
get() = if (isDark) Color(0xFFF4F6FA) else Color(0xFF18202B)
val textSecondary: Color
get() = if (isDark) Color(0xFFA9B1BE) else Color(0xFF687382)
val textTertiary: Color
get() = if (isDark) Color(0xFF727C8B) else Color(0xFF98A2AF)
val accent: Color get() = Color(0xFF3B82F6)
val accentSoft: Color get() = accent.copy(alpha = if (isDark) 0.22f else 0.13f)
val success: Color get() = Color(0xFF32C36C)
val danger: Color get() = if (isDark) Color(0xFFFF6B6B) else Color(0xFFD9485F)
val divider: Color
get() = if (isDark) Color.White.copy(alpha = 0.10f) else Color(0xFF1C2733).copy(alpha = 0.10f)
val clearTint: Color
get() = if (isDark) Color(0xFF18202A).copy(alpha = 0.28f) else Color.White.copy(alpha = 0.16f)
val regularTint: Color
get() = if (isDark) Color(0xFF19222D).copy(alpha = 0.50f) else Color.White.copy(alpha = 0.38f)
val strongTint: Color
get() = if (isDark) Color(0xFF202A36).copy(alpha = 0.68f) else Color.White.copy(alpha = 0.58f)
val floatingTint: Color
get() = if (isDark) Color(0xFF1B2430).copy(alpha = 0.62f) else Color.White.copy(alpha = 0.48f)
val glassBorder: Color
get() = if (isDark) Color.White.copy(alpha = 0.16f) else Color.White.copy(alpha = 0.56f)
val glassHighlight: Color
get() = if (isDark) Color.White.copy(alpha = 0.07f) else Color.White.copy(alpha = 0.40f)
val glassShadow: Color
get() = if (isDark) Color.Black.copy(alpha = 0.32f) else Color(0xFF344054).copy(alpha = 0.10f)
}
/** Stable frosted fallback for devices where Kyant Backdrop is disabled. */
fun Modifier.lumaFallbackGlass(
shape: Shape,
selected: Boolean = false,
border: Boolean = true,
): Modifier {
val light = !LumaColors.isDark
val top = if (selected) {
if (light) Color.White.copy(alpha = 0.34f) else Color.White.copy(alpha = 0.15f)
} else {
if (light) Color.White.copy(alpha = 0.24f) else Color.White.copy(alpha = 0.08f)
}
val bottom = if (light) {
Color(0xFFB8C5D5).copy(alpha = 0.14f)
} else {
Color(0xFF121B27).copy(alpha = 0.46f)
}
val edge = if (selected) {
if (light) {
Color(0xFF9BA8B8).copy(alpha = 0.28f)
} else {
Color.White.copy(alpha = 0.22f)
}
} else {
LumaColors.glassHighlight.copy(alpha = if (light) 0.18f else 0.14f)
}
val fill = if (selected) {
if (light) {
Brush.horizontalGradient(
listOf(
Color.White.copy(alpha = 0.44f),
Color.White.copy(alpha = 0.66f),
Color(0xFFDCE3EB).copy(alpha = 0.48f),
Color.White.copy(alpha = 0.38f),
),
)
} else {
Brush.horizontalGradient(
listOf(
Color.White.copy(alpha = 0.16f),
Color(0xFF46515F).copy(alpha = 0.30f),
Color.White.copy(alpha = 0.20f),
Color(0xFF202A36).copy(alpha = 0.28f),
),
)
}
} else {
Brush.verticalGradient(listOf(top, bottom))
}
return this
.clip(shape)
.background(fill, shape)
.then(
if (selected) {
Modifier.drawWithCache {
val leftGlow = Brush.radialGradient(
colors = listOf(
Color.White.copy(alpha = if (light) 0.26f else 0.18f),
Color.White.copy(alpha = if (light) 0.08f else 0.05f),
Color.Transparent,
),
center = Offset(size.width * 0.28f, size.height * 0.18f),
radius = size.height * 1.25f,
)
val rightGlow = Brush.radialGradient(
colors = listOf(
Color.White.copy(alpha = if (light) 0.34f else 0.18f),
Color.Transparent,
),
center = Offset(size.width * 0.86f, size.height * 0.22f),
radius = size.height * 0.92f,
)
onDrawWithContent {
drawCircle(leftGlow, center = Offset(size.width * 0.28f, size.height * 0.18f), radius = size.height * 1.25f)
drawCircle(rightGlow, center = Offset(size.width * 0.86f, size.height * 0.22f), radius = size.height * 0.92f)
drawContent()
}
}
} else {
Modifier
},
)
.then(if (border) Modifier.border(0.6.dp, edge, shape) else Modifier)
.clip(shape)
}
enum class LumaGlassLevel {
Clear,
Regular,
Strong,
Floating,
Interactive,
}
private data class GlassSpec(
val tint: Color,
val blur: Dp,
val shadowAlpha: Float,
)
private fun GlassSpec.forLevel(level: LumaGlassLevel): GlassSpec = when (level) {
LumaGlassLevel.Clear -> this.copy(tint = LumaColors.clearTint, blur = 5.dp)
LumaGlassLevel.Regular -> this.copy(tint = LumaColors.regularTint, blur = 8.dp)
LumaGlassLevel.Strong -> this.copy(tint = LumaColors.strongTint, blur = 12.dp, shadowAlpha = 0.16f)
LumaGlassLevel.Floating -> this.copy(tint = LumaColors.floatingTint, blur = 10.dp, shadowAlpha = 0.18f)
LumaGlassLevel.Interactive -> this.copy(tint = LumaColors.floatingTint, blur = 11.dp, shadowAlpha = 0.20f)
}
@Composable
fun Modifier.lumaGlass(
backdrop: Backdrop? = LocalGlassBackdrop.current,
radius: Dp = LumaShapes.medium,
level: LumaGlassLevel = LumaGlassLevel.Regular,
border: Boolean = true,
tint: Color? = null,
borderColor: Color? = null,
shadowElevation: Dp = 8.dp,
highlight: Boolean = true,
): Modifier {
val spec = remember(AppTheme.isDark, level) {
GlassSpec(
tint = LumaColors.regularTint,
blur = 8.dp,
shadowAlpha = 0.10f,
).forLevel(level)
}
val shape = RoundedCornerShape(radius)
val surfaceTint = tint ?: spec.tint
val usesDefaultGlassTint = tint == null || tint == LumaColors.clearTint ||
tint == LumaColors.regularTint || tint == LumaColors.strongTint || tint == LumaColors.floatingTint
val edgeColor = borderColor ?: LumaColors.glassBorder
val safeBackdrop = backdrop.takeIf { LumaGlassRuntime.useNativeBackdrop }
return this
.shadow(shadowElevation, shape, ambientColor = LumaColors.glassShadow, spotColor = LumaColors.glassShadow)
.clip(shape)
.then(
if (safeBackdrop != null) {
Modifier.drawBackdrop(
backdrop = safeBackdrop,
shape = { RoundedRectangle(radius) },
effects = {
colorControls(
brightness = if (LumaColors.isDark) 0f else 0.04f,
saturation = 1.08f,
)
vibrancy()
blur(spec.blur.toPx())
},
highlight = if (highlight) ({ Highlight.Plain.copy(alpha = 0.75f) }) else null,
shadow = { Shadow(alpha = spec.shadowAlpha) },
onDrawSurface = { drawRect(surfaceTint) },
)
} else {
if (usesDefaultGlassTint) {
Modifier.lumaFallbackGlass(shape, border = false)
} else {
Modifier.background(
surfaceTint.copy(alpha = surfaceTint.alpha.coerceAtMost(0.34f)),
shape,
)
}
},
)
.then(if (border) Modifier.border(0.6.dp, edgeColor, shape) else Modifier)
}
@Composable
fun LumaGlassSurface(
modifier: Modifier = Modifier,
backdrop: Backdrop? = LocalGlassBackdrop.current,
radius: Dp = LumaShapes.medium,
level: LumaGlassLevel = LumaGlassLevel.Regular,
content: @Composable () -> Unit,
) {
Box(modifier.lumaGlass(backdrop = backdrop, radius = radius, level = level), content = { content() })
}
@Composable
fun LumaPageBackground(modifier: Modifier = Modifier) {
Box(
modifier
.background(
Brush.linearGradient(
listOf(
LumaColors.background,
LumaColors.backgroundElevated,
LumaColors.background,
),
),
),
)
}
@@ -0,0 +1,143 @@
package com.example.myapplication.ui.design
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import com.example.myapplication.R
enum class LumaIcon(
val regular: Int,
val selected: Int,
) {
Activity(R.drawable.aurora_activity, R.drawable.aurora_activity_selected),
Home(R.drawable.aurora_home, R.drawable.aurora_home_selected),
Search(R.drawable.aurora_search, R.drawable.aurora_search_selected),
Library(R.drawable.aurora_library, R.drawable.aurora_library_selected),
Discover(R.drawable.aurora_discover, R.drawable.aurora_discover_selected),
Media(R.drawable.aurora_media, R.drawable.aurora_media_selected),
Watch(R.drawable.aurora_watch, R.drawable.aurora_watch_selected),
Resources(R.drawable.aurora_resources, R.drawable.aurora_resources_selected),
Settings(R.drawable.aurora_settings, R.drawable.aurora_settings_selected),
Favorite(R.drawable.aurora_favorite, R.drawable.aurora_favorite_selected),
Play(R.drawable.aurora_play, R.drawable.aurora_play_selected),
Pause(R.drawable.aurora_pause, R.drawable.aurora_pause_selected),
Stop(R.drawable.aurora_stop, R.drawable.aurora_stop_selected),
Previous(R.drawable.aurora_previous, R.drawable.aurora_previous_selected),
Next(R.drawable.aurora_next, R.drawable.aurora_next_selected),
Replay(R.drawable.aurora_replay, R.drawable.aurora_replay_selected),
Shuffle(R.drawable.aurora_shuffle, R.drawable.aurora_shuffle_selected),
Speed(R.drawable.aurora_speed, R.drawable.aurora_speed_selected),
Folder(R.drawable.aurora_folder, R.drawable.aurora_folder_selected),
Collection(R.drawable.aurora_collection, R.drawable.aurora_collection_selected),
Playlist(R.drawable.aurora_playlist, R.drawable.aurora_playlist_selected),
History(R.drawable.aurora_history, R.drawable.aurora_history_selected),
Download(R.drawable.aurora_download, R.drawable.aurora_download_selected),
Upload(R.drawable.aurora_upload, R.drawable.aurora_upload_selected),
Subtitle(R.drawable.aurora_subtitle, R.drawable.aurora_subtitle_selected),
Server(R.drawable.aurora_server, R.drawable.aurora_server_selected),
Users(R.drawable.aurora_users, R.drawable.aurora_users_selected),
Badge(R.drawable.aurora_badge, R.drawable.aurora_badge_selected),
User(R.drawable.aurora_user, R.drawable.aurora_user_selected),
ProfileEdit(R.drawable.aurora_profile_edit, R.drawable.aurora_profile_edit_selected),
SwitchAccount(R.drawable.aurora_switch_account, R.drawable.aurora_switch_account_selected),
Login(R.drawable.aurora_login, R.drawable.aurora_login_selected),
Logout(R.drawable.aurora_logout, R.drawable.aurora_logout_selected),
Qr(R.drawable.aurora_qr, R.drawable.aurora_qr_selected),
Database(R.drawable.aurora_database, R.drawable.aurora_database_selected),
Cloud(R.drawable.aurora_cloud, R.drawable.aurora_cloud_selected),
Network(R.drawable.aurora_network, R.drawable.aurora_network_selected),
Globe(R.drawable.aurora_globe, R.drawable.aurora_globe_selected),
Shield(R.drawable.aurora_shield, R.drawable.aurora_shield_selected),
Key(R.drawable.aurora_key, R.drawable.aurora_key_selected),
Phone(R.drawable.aurora_phone, R.drawable.aurora_phone_selected),
Tablet(R.drawable.aurora_tablet, R.drawable.aurora_tablet_selected),
Laptop(R.drawable.aurora_laptop, R.drawable.aurora_laptop_selected),
Tv(R.drawable.aurora_tv, R.drawable.aurora_tv_selected),
Cast(R.drawable.aurora_cast, R.drawable.aurora_cast_selected),
Speaker(R.drawable.aurora_speaker, R.drawable.aurora_speaker_selected),
Headphones(R.drawable.aurora_headphones, R.drawable.aurora_headphones_selected),
Remote(R.drawable.aurora_remote, R.drawable.aurora_remote_selected),
Add(R.drawable.aurora_add, R.drawable.aurora_add_selected),
Remove(R.drawable.aurora_remove, R.drawable.aurora_remove_selected),
Edit(R.drawable.aurora_edit, R.drawable.aurora_edit_selected),
Delete(R.drawable.aurora_delete, R.drawable.aurora_delete_selected),
Refresh(R.drawable.aurora_refresh, R.drawable.aurora_refresh_selected),
Filter(R.drawable.aurora_filter, R.drawable.aurora_filter_selected),
Sort(R.drawable.aurora_sort, R.drawable.aurora_sort_selected),
Share(R.drawable.aurora_share, R.drawable.aurora_share_selected),
Info(R.drawable.aurora_info, R.drawable.aurora_info_selected),
Warning(R.drawable.aurora_warning, R.drawable.aurora_warning_selected),
Notification(R.drawable.aurora_notification, R.drawable.aurora_notification_selected),
Lock(R.drawable.aurora_lock, R.drawable.aurora_lock_selected),
Unlock(R.drawable.aurora_unlock, R.drawable.aurora_unlock_selected),
Eye(R.drawable.aurora_eye, R.drawable.aurora_eye_selected),
File(R.drawable.aurora_file, R.drawable.aurora_file_selected),
FileVideo(R.drawable.aurora_file_video, R.drawable.aurora_file_video_selected),
FileAudio(R.drawable.aurora_file_audio, R.drawable.aurora_file_audio_selected),
Image(R.drawable.aurora_image, R.drawable.aurora_image_selected),
Archive(R.drawable.aurora_archive, R.drawable.aurora_archive_selected),
Link(R.drawable.aurora_link, R.drawable.aurora_link_selected),
Attachment(R.drawable.aurora_attachment, R.drawable.aurora_attachment_selected),
Calendar(R.drawable.aurora_calendar, R.drawable.aurora_calendar_selected),
Check(R.drawable.aurora_check, R.drawable.aurora_check_selected),
Close(R.drawable.aurora_close, R.drawable.aurora_close_selected),
Back(R.drawable.aurora_back, R.drawable.aurora_back_selected),
ChevronRight(R.drawable.aurora_chevron_right, R.drawable.aurora_chevron_right_selected),
More(R.drawable.aurora_more, R.drawable.aurora_more_selected),
EyeOff(R.drawable.aurora_eye_off, R.drawable.aurora_eye_off_selected),
Star(R.drawable.aurora_star, R.drawable.aurora_star_selected),
}
@Composable
fun LumaIconView(
icon: LumaIcon,
modifier: Modifier = Modifier,
selected: Boolean = false,
tint: Color = Color.Unspecified,
contentDescription: String? = null,
) {
Icon(
painter = painterResource(if (selected) icon.selected else icon.regular),
contentDescription = contentDescription,
tint = tint,
modifier = modifier.graphicsLayer {
scaleX = 1.08f
scaleY = 1.08f
},
)
}
@Composable
fun LumaAnimatedIcon(
icon: LumaIcon,
selected: Boolean,
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified,
) {
val scale by animateFloatAsState(
targetValue = if (selected) 1.02f else 1.08f,
animationSpec = spring(dampingRatio = 0.78f, stiffness = 650f),
label = "lumaIconStateScale",
)
Crossfade(
targetState = selected,
animationSpec = tween(LumaMotion.icon),
label = "lumaIconState",
modifier = modifier.graphicsLayer { scaleX = scale; scaleY = scale },
) { isSelected ->
Icon(
painter = painterResource(if (isSelected) icon.selected else icon.regular),
contentDescription = contentDescription,
tint = tint,
)
}
}
@@ -1,7 +1,5 @@
package com.example.myapplication.ui.liquidglass
import android.graphics.RuntimeShader
import android.os.Build
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.VisibilityThreshold
@@ -11,9 +9,8 @@ import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.util.fastCoerceIn
import kotlinx.coroutines.CoroutineScope
@@ -38,55 +35,30 @@ class InteractiveHighlight(
val pressProgress: Float get() = pressProgressAnimation.value
val offset: Offset get() = positionAnimation.value - startPosition
private val shader =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RuntimeShader(
"""
uniform float2 size;
layout(color) uniform half4 color;
uniform float radius;
uniform float2 position;
half4 main(float2 coord) {
float dist = distance(coord, position);
float intensity = smoothstep(radius, radius * 0.5, dist);
return color * intensity;
}"""
)
} else {
null
}
val modifier: Modifier =
Modifier.drawWithContent {
val progress = pressProgressAnimation.value
if (progress > 0f) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && shader != null) {
drawRect(
Color.White.copy(0.08f * progress),
blendMode = BlendMode.Plus
)
shader.apply {
val position = position(size, positionAnimation.value)
setFloatUniform("size", size.width, size.height)
setColorUniform("color", Color.White.copy(0.15f * progress).toArgb())
setFloatUniform("radius", size.minDimension * 1.5f)
setFloatUniform(
"position",
position.x.fastCoerceIn(0f, size.width),
position.y.fastCoerceIn(0f, size.height)
)
}
drawRect(
ShaderBrush(shader),
blendMode = BlendMode.Plus
)
} else {
drawRect(
Color.White.copy(0.25f * progress),
blendMode = BlendMode.Plus
drawRect(Color.White.copy(0.08f * progress), blendMode = BlendMode.Plus)
val center = position(size, positionAnimation.value).let {
Offset(
it.x.fastCoerceIn(0f, size.width),
it.y.fastCoerceIn(0f, size.height),
)
}
drawCircle(
brush = Brush.radialGradient(
colors = listOf(
Color.White.copy(alpha = 0.15f * progress),
Color.Transparent,
),
center = center,
radius = size.minDimension * 1.5f,
),
center = center,
radius = size.minDimension * 1.5f,
blendMode = BlendMode.Plus,
)
}
drawContent()
@@ -29,11 +29,17 @@ fun RowScope.LiquidBottomTab(
Column(
modifier
.clip(Capsule())
.clickable(
interactionSource = null,
indication = null,
role = Role.Tab,
onClick = onClick
.then(
if (LocalLiquidBottomTabsGhostLayer.current) {
Modifier
} else {
Modifier.clickable(
interactionSource = null,
indication = null,
role = Role.Tab,
onClick = onClick,
)
},
)
.fillMaxHeight()
.weight(1f)
@@ -3,7 +3,6 @@ package com.example.myapplication.ui.liquidglass
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.spring
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
@@ -11,6 +10,7 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.border
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -25,6 +25,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
@@ -40,7 +41,6 @@ import androidx.compose.ui.util.lerp
import com.kyant.backdrop.Backdrop
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import com.kyant.backdrop.drawBackdrop
import com.kyant.backdrop.effects.blur
import com.kyant.backdrop.effects.lens
@@ -49,6 +49,11 @@ import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.shadow.InnerShadow
import com.kyant.backdrop.shadow.Shadow
import com.kyant.shapes.Capsule
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.LumaSpacing
import com.example.myapplication.ui.design.lumaFallbackGlass
import com.example.myapplication.ui.design.rememberLumaLayerBackdrop
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
@@ -65,18 +70,18 @@ val LocalLiquidBottomTabsPillValue = staticCompositionLocalOf { 0f }
fun LiquidBottomTabs(
selectedTabIndex: () -> Int,
onTabSelected: (index: Int) -> Unit,
backdrop: Backdrop,
backdrop: Backdrop?,
tabsCount: Int,
showSelection: () -> Boolean = { true },
modifier: Modifier = Modifier,
accentColor: Color = if (!isSystemInDarkTheme()) Color(0xFF0A84FF) else Color(0xFF0091FF),
accentColor: Color = LumaColors.accent,
content: @Composable RowScope.() -> Unit
) {
val isLightTheme = !isSystemInDarkTheme()
val containerColor =
if (isLightTheme) Color(0xFFFAFAFA).copy(0.4f)
else Color(0xFF121212).copy(0.4f)
val isLightTheme = !LumaColors.isDark
val containerColor = LumaColors.clearTint
val useBackdrop = LumaGlassRuntime.useNativeBackdrop && backdrop != null
val tabsBackdrop = rememberLayerBackdrop()
val tabsBackdrop = rememberLumaLayerBackdrop()
BoxWithConstraints(
modifier,
@@ -100,12 +105,12 @@ fun LiquidBottomTabs(
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val animationScope = rememberCoroutineScope()
var currentIndex by remember {
mutableIntStateOf(selectedTabIndex())
mutableIntStateOf(selectedTabIndex().fastCoerceIn(0, tabsCount - 1))
}
val dampedDragAnimation = remember(animationScope) {
DampedDragAnimation(
animationScope = animationScope,
initialValue = selectedTabIndex().toFloat(),
initialValue = selectedTabIndex().fastCoerceIn(0, tabsCount - 1).toFloat(),
valueRange = 0f..(tabsCount - 1).toFloat(),
visibilityThreshold = 0.001f,
initialScale = 1f,
@@ -133,7 +138,7 @@ fun LiquidBottomTabs(
}
)
}
val externalIndex = selectedTabIndex()
val externalIndex = selectedTabIndex().fastCoerceIn(0, tabsCount - 1)
LaunchedEffect(externalIndex) {
if (externalIndex != currentIndex) {
currentIndex = externalIndex
@@ -167,26 +172,41 @@ fun LiquidBottomTabs(
.graphicsLayer {
translationX = panelOffset
}
.drawBackdrop(
backdrop = backdrop,
shape = { Capsule() },
effects = {
vibrancy()
blur(8f.dp.toPx())
lens(24f.dp.toPx(), 24f.dp.toPx())
.clip(Capsule())
.then(
if (useBackdrop) {
Modifier.drawBackdrop(
backdrop = backdrop,
shape = { Capsule() },
effects = {
vibrancy()
blur(8f.dp.toPx())
lens(24f.dp.toPx(), 24f.dp.toPx())
},
layerBlock = {
val progress = dampedDragAnimation.pressProgress
val scale = lerp(1f, 1f + 16f.dp.toPx() / size.width, progress)
scaleX = scale
scaleY = scale
},
onDrawSurface = { drawRect(containerColor) },
)
} else {
Modifier.lumaFallbackGlass(Capsule())
},
layerBlock = {
val progress = dampedDragAnimation.pressProgress
val scale = lerp(1f, 1f + 16f.dp.toPx() / size.width, progress)
scaleX = scale
scaleY = scale
)
.then(
if (useBackdrop) {
Modifier.border(0.6.dp, LumaColors.glassBorder.copy(alpha = 0.72f), Capsule())
} else {
Modifier
},
onDrawSurface = { drawRect(containerColor) }
)
.then(interactiveHighlight.modifier)
.height(64f.dp)
.fillMaxWidth()
.padding(4f.dp),
.padding(LumaSpacing.xxs)
.clip(Capsule()),
verticalAlignment = Alignment.CenterVertically,
content = {
CompositionLocalProvider(
@@ -207,27 +227,34 @@ fun LiquidBottomTabs(
Modifier
.clearAndSetSemantics {}
.alpha(0f)
.layerBackdrop(tabsBackdrop)
.clip(Capsule())
.then(if (useBackdrop && tabsBackdrop != null) Modifier.layerBackdrop(tabsBackdrop) else Modifier)
.graphicsLayer {
translationX = panelOffset
}
.drawBackdrop(
backdrop = backdrop,
shape = { Capsule() },
effects = {
val progress = dampedDragAnimation.pressProgress
vibrancy()
blur(8f.dp.toPx())
lens(
24f.dp.toPx() * progress,
24f.dp.toPx() * progress
.then(
if (useBackdrop) {
Modifier.drawBackdrop(
backdrop = backdrop,
shape = { Capsule() },
effects = {
val progress = dampedDragAnimation.pressProgress
vibrancy()
blur(8f.dp.toPx())
lens(
24f.dp.toPx() * progress,
24f.dp.toPx() * progress,
)
},
highlight = {
val progress = dampedDragAnimation.pressProgress
Highlight.Plain.copy(alpha = progress)
},
onDrawSurface = { drawRect(containerColor) },
)
} else {
Modifier
},
highlight = {
val progress = dampedDragAnimation.pressProgress
Highlight.Default.copy(alpha = progress)
},
onDrawSurface = { drawRect(containerColor) }
)
.then(interactiveHighlight.modifier)
.height(56f.dp)
@@ -246,6 +273,7 @@ fun LiquidBottomTabs(
Box(
Modifier
.padding(horizontal = 4f.dp)
.clip(Capsule())
.graphicsLayer {
translationX =
if (isLtr) dampedDragAnimation.value * tabWidth + panelOffset
@@ -253,52 +281,57 @@ fun LiquidBottomTabs(
}
.then(interactiveHighlight.gestureModifier)
.then(dampedDragAnimation.modifier)
.drawBackdrop(
backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop),
shape = { Capsule() },
effects = {
val progress = dampedDragAnimation.pressProgress
lens(
10f.dp.toPx() * progress,
14f.dp.toPx() * progress,
chromaticAberration = true
.then(
if (useBackdrop) {
Modifier.drawBackdrop(
backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop!!),
shape = { Capsule() },
effects = {
lens(
10f.dp.toPx(),
14f.dp.toPx(),
chromaticAberration = true,
)
},
highlight = {
val progress = dampedDragAnimation.pressProgress
Highlight.Plain.copy(alpha = progress)
},
shadow = {
val progress = dampedDragAnimation.pressProgress
Shadow(alpha = progress)
},
innerShadow = {
val progress = dampedDragAnimation.pressProgress
InnerShadow(
radius = 8f.dp * progress,
alpha = progress,
)
},
layerBlock = {
scaleX = dampedDragAnimation.scaleX
scaleY = dampedDragAnimation.scaleY
val velocity = dampedDragAnimation.velocity / 10f
scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f)
scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f)
},
onDrawSurface = {
val progress = dampedDragAnimation.pressProgress
drawRect(
if (isLightTheme) Color.Black.copy(0.1f)
else Color.White.copy(0.1f),
alpha = 1f - progress,
)
drawRect(Color.Black.copy(alpha = 0.03f * progress))
},
)
} else {
Modifier.lumaFallbackGlass(Capsule(), selected = true)
},
highlight = {
val progress = dampedDragAnimation.pressProgress
Highlight.Default.copy(alpha = progress)
},
shadow = {
val progress = dampedDragAnimation.pressProgress
Shadow(alpha = progress)
},
innerShadow = {
val progress = dampedDragAnimation.pressProgress
InnerShadow(
radius = 8f.dp * progress,
alpha = progress
)
},
layerBlock = {
scaleX = dampedDragAnimation.scaleX
scaleY = dampedDragAnimation.scaleY
val velocity = dampedDragAnimation.velocity / 10f
scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f)
scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f)
},
onDrawSurface = {
val progress = dampedDragAnimation.pressProgress
drawRect(
if (isLightTheme) Color.Black.copy(0.1f)
else Color.White.copy(0.1f),
alpha = 1f - progress
)
drawRect(Color.Black.copy(alpha = 0.03f * progress))
}
)
.height(56f.dp)
.fillMaxWidth(1f / tabsCount)
.alpha(if (showSelection()) 1f else 0f)
)
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M18 53 H31 L40 35 L50 68 L59 46 L66 53 H82"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M18 53 H31 L40 35 L50 68 L59 46 L66 53 H82"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M36.0,22.0 H64.0 A14.0,14.0 0 0 1 78.0,36.0 V64.0 A14.0,14.0 0 0 1 64.0,78.0 H36.0 A14.0,14.0 0 0 1 22.0,64.0 V36.0 A14.0,14.0 0 0 1 36.0,22.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50,36 L50,64"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M36,50 L64,50"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M36.0,22.0 H64.0 A14.0,14.0 0 0 1 78.0,36.0 V64.0 A14.0,14.0 0 0 1 64.0,78.0 H36.0 A14.0,14.0 0 0 1 22.0,64.0 V36.0 A14.0,14.0 0 0 1 36.0,22.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M50,36 L50,64"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M36,50 L64,50"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M34.0,30.0 H66.0 A10.0,10.0 0 0 1 76.0,40.0 V68.0 A10.0,10.0 0 0 1 66.0,78.0 H34.0 A10.0,10.0 0 0 1 24.0,68.0 V40.0 A10.0,10.0 0 0 1 34.0,30.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M26.0,20.0 H74.0 A6.0,6.0 0 0 1 80.0,26.0 V29.0 A6.0,6.0 0 0 1 74.0,35.0 H26.0 A6.0,6.0 0 0 1 20.0,29.0 V26.0 A6.0,6.0 0 0 1 26.0,20.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M45,48 L55,48"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M45,59 L55,59"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M34.0,30.0 H66.0 A10.0,10.0 0 0 1 76.0,40.0 V68.0 A10.0,10.0 0 0 1 66.0,78.0 H34.0 A10.0,10.0 0 0 1 24.0,68.0 V40.0 A10.0,10.0 0 0 1 34.0,30.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M26.0,20.0 H74.0 A6.0,6.0 0 0 1 80.0,26.0 V29.0 A6.0,6.0 0 0 1 74.0,35.0 H26.0 A6.0,6.0 0 0 1 20.0,29.0 V26.0 A6.0,6.0 0 0 1 26.0,20.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M45,48 L55,48"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M45,59 L55,59"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M61 28 L37 52 C28 61 35 68 42 75 C49 82 56 75 62 69 L78 53 C91 40 82 25 72 20 C62 15 53 20 47 26 L28 45 C13 60 16 74 25 82 C34 90 45 87 54 78 L72 60"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M61 28 L37 52 C28 61 35 68 42 75 C49 82 56 75 62 69 L78 53 C91 40 82 25 72 20 C62 15 53 20 47 26 L28 45 C13 60 16 74 25 82 C34 90 45 87 54 78 L72 60"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M78,47 H30 L50,27 L45,22 L17,50 L45,78 L50,73 L30,53 H78 Z"
android:fillColor="#1C1C1E" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M78,47 H30 L50,27 L45,22 L17,50 L45,78 L50,73 L30,53 H78 Z"
android:fillColor="#0A84FF" />
</vector>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M50 18 L77 29 V48 C77 66 65 78 50 84 C35 78 23 66 23 48 V29 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50 35 L54 44 L64 45 L56 52 L58 62 L50 57 L42 62 L44 52 L36 45 L46 44 Z"
android:fillColor="#1C1C1E" />
</vector>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M50 18 L77 29 V48 C77 66 65 78 50 84 C35 78 23 66 23 48 V29 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M50 35 L54 44 L64 45 L56 52 L58 62 L50 57 L42 62 L44 52 L36 45 L46 44 Z"
android:fillColor="#FFFFFF" />
</vector>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M30.0,25.0 H70.0 A10.0,10.0 0 0 1 80.0,35.0 V70.0 A10.0,10.0 0 0 1 70.0,80.0 H30.0 A10.0,10.0 0 0 1 20.0,70.0 V35.0 A10.0,10.0 0 0 1 30.0,25.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M20,39 L80,39"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M35,18 L35,31"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M65,18 L65,31"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M36.0,55.0 A3.0,3.0 0 1 0 42.0,55.0 A3.0,3.0 0 1 0 36.0,55.0 Z"
android:fillColor="#1C1C1E" />
<path
android:pathData="M48.0,55.0 A3.0,3.0 0 1 0 54.0,55.0 A3.0,3.0 0 1 0 48.0,55.0 Z"
android:fillColor="#1C1C1E" />
<path
android:pathData="M60.0,55.0 A3.0,3.0 0 1 0 66.0,55.0 A3.0,3.0 0 1 0 60.0,55.0 Z"
android:fillColor="#1C1C1E" />
<path
android:pathData="M36.0,67.0 A3.0,3.0 0 1 0 42.0,67.0 A3.0,3.0 0 1 0 36.0,67.0 Z"
android:fillColor="#1C1C1E" />
<path
android:pathData="M48.0,67.0 A3.0,3.0 0 1 0 54.0,67.0 A3.0,3.0 0 1 0 48.0,67.0 Z"
android:fillColor="#1C1C1E" />
</vector>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M30.0,25.0 H70.0 A10.0,10.0 0 0 1 80.0,35.0 V70.0 A10.0,10.0 0 0 1 70.0,80.0 H30.0 A10.0,10.0 0 0 1 20.0,70.0 V35.0 A10.0,10.0 0 0 1 30.0,25.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M20,39 L80,39"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M35,18 L35,31"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M65,18 L65,31"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M36.0,55.0 A3.0,3.0 0 1 0 42.0,55.0 A3.0,3.0 0 1 0 36.0,55.0 Z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M48.0,55.0 A3.0,3.0 0 1 0 54.0,55.0 A3.0,3.0 0 1 0 48.0,55.0 Z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M60.0,55.0 A3.0,3.0 0 1 0 66.0,55.0 A3.0,3.0 0 1 0 60.0,55.0 Z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M36.0,67.0 A3.0,3.0 0 1 0 42.0,67.0 A3.0,3.0 0 1 0 36.0,67.0 Z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M48.0,67.0 A3.0,3.0 0 1 0 54.0,67.0 A3.0,3.0 0 1 0 48.0,67.0 Z"
android:fillColor="#FFFFFF" />
</vector>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M43.0,20.0 H74.0 A9.0,9.0 0 0 1 83.0,29.0 V49.0 A9.0,9.0 0 0 1 74.0,58.0 H43.0 A9.0,9.0 0 0 1 34.0,49.0 V29.0 A9.0,9.0 0 0 1 43.0,20.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.0"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M18.8,73.0 A3.2,3.2 0 1 0 25.2,73.0 A3.2,3.2 0 1 0 18.8,73.0 Z"
android:fillColor="#1C1C1E" />
<path
android:pathData="M21 61 C29 61 35 67 35 75"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M21 49 C36 49 47 60 47 75"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M43.0,20.0 H74.0 A9.0,9.0 0 0 1 83.0,29.0 V49.0 A9.0,9.0 0 0 1 74.0,58.0 H43.0 A9.0,9.0 0 0 1 34.0,49.0 V29.0 A9.0,9.0 0 0 1 43.0,20.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.0"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M18.8,73.0 A3.2,3.2 0 1 0 25.2,73.0 A3.2,3.2 0 1 0 18.8,73.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M21 61 C29 61 35 67 35 75"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M21 49 C36 49 47 60 47 75"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20.0,50.0 A30.0,30.0 0 1 0 80.0,50.0 A30.0,30.0 0 1 0 20.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M35 50 L45 60 L66 38"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20.0,50.0 A30.0,30.0 0 1 0 80.0,50.0 A30.0,30.0 0 1 0 20.0,50.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M35 50 L45 60 L66 38"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M36,18 L68,50 L36,82"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="7"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M36,18 L68,50 L36,82"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="7"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20.0,50.0 A30.0,30.0 0 1 0 80.0,50.0 A30.0,30.0 0 1 0 20.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M38,38 L62,62"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M62,38 L38,62"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20.0,50.0 A30.0,30.0 0 1 0 80.0,50.0 A30.0,30.0 0 1 0 20.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M38,38 L62,62"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M62,38 L38,62"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M29 70 C20 70 16 62 19 54 C22 46 29 43 36 45 C38 34 47 27 57 29 C67 30 73 39 72 49 C80 50 84 56 82 63 C80 68 76 70 70 70 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M29 70 C20 70 16 62 19 54 C22 46 29 43 36 45 C38 34 47 27 57 29 C67 30 73 39 72 49 C80 50 84 56 82 63 C80 68 76 70 70 70 Z"
android:fillColor="#0A84FF" />
</vector>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M28.0,27.0 H54.0 A10.0,10.0 0 0 1 64.0,37.0 V51.0 A10.0,10.0 0 0 1 54.0,61.0 H28.0 A10.0,10.0 0 0 1 18.0,51.0 V37.0 A10.0,10.0 0 0 1 28.0,27.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M42.0,39.0 H70.0 A10.0,10.0 0 0 1 80.0,49.0 V65.0 A10.0,10.0 0 0 1 70.0,75.0 H42.0 A10.0,10.0 0 0 1 32.0,65.0 V49.0 A10.0,10.0 0 0 1 42.0,39.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M28.0,27.0 H54.0 A10.0,10.0 0 0 1 64.0,37.0 V51.0 A10.0,10.0 0 0 1 54.0,61.0 H28.0 A10.0,10.0 0 0 1 18.0,51.0 V37.0 A10.0,10.0 0 0 1 28.0,27.0 Z"
android:fillColor="#8BC1FF"
android:fillAlpha="0.65" />
<path
android:pathData="M42.0,39.0 H70.0 A10.0,10.0 0 0 1 80.0,49.0 V65.0 A10.0,10.0 0 0 1 70.0,75.0 H42.0 A10.0,10.0 0 0 1 32.0,65.0 V49.0 A10.0,10.0 0 0 1 42.0,39.0 Z"
android:fillColor="#0A84FF" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M24 30 C24 20 76 20 76 30 V68 C76 78 24 78 24 68 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M24 30 C24 40 76 40 76 30"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M24 49 C24 59 76 59 76 49"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M24 30 C24 20 76 20 76 30 V68 C76 78 24 78 24 68 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M24 30 C24 40 76 40 76 30"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M24 49 C24 59 76 59 76 49"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M39.0,36.0 H61.0 A10.0,10.0 0 0 1 71.0,46.0 V68.0 A10.0,10.0 0 0 1 61.0,78.0 H39.0 A10.0,10.0 0 0 1 29.0,68.0 V46.0 A10.0,10.0 0 0 1 39.0,36.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M24 29 H76"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M39 29 C39 23 43 20 49 20 H51 C57 20 61 23 61 29"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M42,48 L42,66"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58,48 L58,66"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M39.0,36.0 H61.0 A10.0,10.0 0 0 1 71.0,46.0 V68.0 A10.0,10.0 0 0 1 61.0,78.0 H39.0 A10.0,10.0 0 0 1 29.0,68.0 V46.0 A10.0,10.0 0 0 1 39.0,36.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M24 29 H76"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M39 29 C39 23 43 20 49 20 H51 C57 20 61 23 61 29"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M42,48 L42,66"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.0"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58,48 L58,66"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.0"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20.0,50.0 A30.0,30.0 0 1 0 80.0,50.0 A30.0,30.0 0 1 0 20.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58 34 L53 52 L38 65 L43 47 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20.0,50.0 A30.0,30.0 0 1 0 80.0,50.0 A30.0,30.0 0 1 0 20.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58 34 L53 52 L38 65 L43 47 Z"
android:fillColor="#0A84FF" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M50 26 V55"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="7.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M36 49 C41 55 46 60 50 64 C54 60 59 55 64 49"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="7.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M25 69 V74 C25 78 28 81 33 81 H67 C72 81 75 78 75 74 V69"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="7.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M50 26 V55"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="7.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M36 49 C41 55 46 60 50 64 C54 60 59 55 64 49"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="7.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M25 69 V74 C25 78 28 81 33 81 H67 C72 81 75 78 75 74 V69"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="7.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M29 69 L31 58 L63 26 C66 23 70 23 73 26 L78 31 C81 34 81 38 78 41 L46 73 L35 76 C31 77 28 73 29 69 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M59 30 L74 45"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M29 69 L31 58 L63 26 C66 23 70 23 73 26 L78 31 C81 34 81 38 78 41 L46 73 L35 76 C31 77 28 73 29 69 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M59 30 L74 45"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.0"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M17 50 C28 34 39 29 50 29 C61 29 72 34 83 50 C72 66 61 71 50 71 C39 71 28 66 17 50 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M41.0,50.0 A9.0,9.0 0 1 0 59.0,50.0 A9.0,9.0 0 1 0 41.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M17,50 C28,34 39,29 50,29 C61,29 72,34 83,50 C72,66 61,71 50,71 C39,71 28,66 17,50 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M24,24 L76,76"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round" />
</vector>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M17,50 C28,34 39,29 50,29 C61,29 72,34 83,50 C72,66 61,71 50,71 C39,71 28,66 17,50 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M24,24 L76,76"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round" />
</vector>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M17 50 C28 34 39 29 50 29 C61 29 72 34 83 50 C72 66 61 71 50 71 C39 71 28 66 17 50 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M41.0,50.0 A9.0,9.0 0 1 0 59.0,50.0 A9.0,9.0 0 1 0 41.0,50.0 Z"
android:fillColor="#0A84FF" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M50 34 C46 26 41 22 35 22 C25 22 19 31 19 43 C19 60 36 74 50 83 C64 74 81 60 81 43 C81 31 75 22 65 22 C59 22 54 26 50 34 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M50 34 C46 26 41 22 35 22 C25 22 19 31 19 43 C19 60 36 74 50 83 C64 74 81 60 81 43 C81 31 75 22 65 22 C59 22 54 26 50 34 Z"
android:fillColor="#0A84FF" />
</vector>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M31 18 H56 C59 18 61 19 63 21 L72 30 C74 32 75 35 75 38 V75 C75 80 72 83 67 83 H31 C26 83 23 80 23 75 V26 C23 21 26 18 31 18 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58 20 V32 C58 35 60 37 63 37 H73"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M31 18 H56 C59 18 61 19 63 21 L72 30 C74 32 75 35 75 38 V75 C75 80 72 83 67 83 H31 C26 83 23 80 23 75 V26 C23 21 26 18 31 18 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58 20 V32 C58 35 60 37 63 37 H73"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58 44 V64"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58 44 L68 47"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M45.0,67.0 A6.0,6.0 0 1 0 57.0,67.0 A6.0,6.0 0 1 0 45.0,67.0 Z"
android:fillColor="#1C1C1E" />
</vector>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M31 18 H56 C59 18 61 19 63 21 L72 30 C74 32 75 35 75 38 V75 C75 80 72 83 67 83 H31 C26 83 23 80 23 75 V26 C23 21 26 18 31 18 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M58 20 V32 C58 35 60 37 63 37 H73"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58 44 V64"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58 44 L68 47"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M45.0,67.0 A6.0,6.0 0 1 0 57.0,67.0 A6.0,6.0 0 1 0 45.0,67.0 Z"
android:fillColor="#FFFFFF" />
</vector>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M31 18 H56 C59 18 61 19 63 21 L72 30 C74 32 75 35 75 38 V75 C75 80 72 83 67 83 H31 C26 83 23 80 23 75 V26 C23 21 26 18 31 18 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M58 20 V32 C58 35 60 37 63 37 H73"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M31 18 H56 C59 18 61 19 63 21 L72 30 C74 32 75 35 75 38 V75 C75 80 72 83 67 83 H31 C26 83 23 80 23 75 V26 C23 21 26 18 31 18 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M58 20 V32 C58 35 60 37 63 37 H73"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M42 46 C42 43 45 41 48 43 L61 51 C64 53 64 57 61 59 L48 67 C45 69 42 67 42 64 Z"
android:fillColor="#1C1C1E" />
</vector>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M31 18 H56 C59 18 61 19 63 21 L72 30 C74 32 75 35 75 38 V75 C75 80 72 83 67 83 H31 C26 83 23 80 23 75 V26 C23 21 26 18 31 18 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M58 20 V32 C58 35 60 37 63 37 H73"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M42 46 C42 43 45 41 48 43 L61 51 C64 53 64 57 61 59 L48 67 C45 69 42 67 42 64 Z"
android:fillColor="#FFFFFF" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20 24 H80 L59 50 V70 C59 73 57 75 54 77 L42 84 V50 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20 24 H80 L59 50 V70 C59 73 57 75 54 77 L42 84 V50 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20 38 C20 31 24 27 31 27 H42 C46 27 49 29 52 33 L55 36 H71 C78 36 81 40 81 47 V69 C81 76 77 80 70 80 H30 C23 80 20 76 20 69 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M19 38 C19 30 24 25 32 25 H42 C47 25 50 28 53 31 L56 34 H71 C79 34 83 39 83 47 V69 C83 78 78 83 70 83 H30 C22 83 19 78 19 69 Z"
android:fillColor="#0A84FF" />
</vector>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M19.0,50.0 A31.0,31.0 0 1 0 81.0,50.0 A31.0,31.0 0 1 0 19.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50 19 C39 29 35 40 35 50 C35 60 39 71 50 81"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50 19 C61 29 65 40 65 50 C65 60 61 71 50 81"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M20,42 L80,42"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M20,58 L80,58"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M19.0,50.0 A31.0,31.0 0 1 0 81.0,50.0 A31.0,31.0 0 1 0 19.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50 19 C39 29 35 40 35 50 C35 60 39 71 50 81"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50 19 C61 29 65 40 65 50 C65 60 61 71 50 81"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M20,42 L80,42"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M20,58 L80,58"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M22 53 C22 33 34 20 50 20 C66 20 78 33 78 53"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M24.0,49.0 H24.0 A6.0,6.0 0 0 1 30.0,55.0 V68.0 A6.0,6.0 0 0 1 24.0,74.0 H24.0 A6.0,6.0 0 0 1 18.0,68.0 V55.0 A6.0,6.0 0 0 1 24.0,49.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M76.0,49.0 H76.0 A6.0,6.0 0 0 1 82.0,55.0 V68.0 A6.0,6.0 0 0 1 76.0,74.0 H76.0 A6.0,6.0 0 0 1 70.0,68.0 V55.0 A6.0,6.0 0 0 1 76.0,49.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M22 53 C22 33 34 20 50 20 C66 20 78 33 78 53"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M24.0,49.0 H24.0 A6.0,6.0 0 0 1 30.0,55.0 V68.0 A6.0,6.0 0 0 1 24.0,74.0 H24.0 A6.0,6.0 0 0 1 18.0,68.0 V55.0 A6.0,6.0 0 0 1 24.0,49.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M76.0,49.0 H76.0 A6.0,6.0 0 0 1 82.0,55.0 V68.0 A6.0,6.0 0 0 1 76.0,74.0 H76.0 A6.0,6.0 0 0 1 70.0,68.0 V55.0 A6.0,6.0 0 0 1 76.0,49.0 Z"
android:fillColor="#0A84FF" />
</vector>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M75 36 C70 26 61 21 50 21 C33 21 20 34 20 51 C20 68 33 80 50 80 C64 80 75 72 79 60"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M22 34 L20 51 L34 44"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50,34 L50,52"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50,52 L61,59"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M75 36 C70 26 61 21 50 21 C33 21 20 34 20 51 C20 68 33 80 50 80 C64 80 75 72 79 60"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M22 34 L20 51 L34 44"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50,34 L50,52"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M50,52 L61,59"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20 48 C20 43 22 40 26 36 L43 22 C47 18 53 18 57 22 L74 36 C78 40 80 43 80 48 V70 C80 77 76 81 69 81 H31 C24 81 20 77 20 70 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M49.0,57.0 H51.0 A6.0,6.0 0 0 1 57.0,63.0 V75.0 A6.0,6.0 0 0 1 51.0,81.0 H49.0 A6.0,6.0 0 0 1 43.0,75.0 V63.0 A6.0,6.0 0 0 1 49.0,57.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M19 48 C19 42 22 38 26 34 L43 21 C47 17 53 17 57 21 L74 34 C78 38 81 42 81 48 V71 C81 78 77 82 69 82 H31 C23 82 19 78 19 71 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M49.0,58.0 H51.0 A6.0,6.0 0 0 1 57.0,64.0 V76.0 A6.0,6.0 0 0 1 51.0,82.0 H49.0 A6.0,6.0 0 0 1 43.0,76.0 V64.0 A6.0,6.0 0 0 1 49.0,58.0 Z"
android:fillColor="#FFFFFF" />
</vector>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M30.0,22.0 H70.0 A10.0,10.0 0 0 1 80.0,32.0 V68.0 A10.0,10.0 0 0 1 70.0,78.0 H30.0 A10.0,10.0 0 0 1 20.0,68.0 V32.0 A10.0,10.0 0 0 1 30.0,22.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M30.0,39.0 A6.0,6.0 0 1 0 42.0,39.0 A6.0,6.0 0 1 0 30.0,39.0 Z"
android:fillColor="#1C1C1E" />
<path
android:pathData="M25 69 L43 50 L55 62 L64 53 L76 69"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M30.0,22.0 H70.0 A10.0,10.0 0 0 1 80.0,32.0 V68.0 A10.0,10.0 0 0 1 70.0,78.0 H30.0 A10.0,10.0 0 0 1 20.0,68.0 V32.0 A10.0,10.0 0 0 1 30.0,22.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M30.0,39.0 A6.0,6.0 0 1 0 42.0,39.0 A6.0,6.0 0 1 0 30.0,39.0 Z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M25 69 L43 50 L55 62 L64 53 L76 69"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.6"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20.0,50.0 A30.0,30.0 0 1 0 80.0,50.0 A30.0,30.0 0 1 0 20.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M47.0,35.0 A3.0,3.0 0 1 0 53.0,35.0 A3.0,3.0 0 1 0 47.0,35.0 Z"
android:fillColor="#1C1C1E" />
<path
android:pathData="M50,46 L50,65"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M20.0,50.0 A30.0,30.0 0 1 0 80.0,50.0 A30.0,30.0 0 1 0 20.0,50.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M47.0,35.0 A3.0,3.0 0 1 0 53.0,35.0 A3.0,3.0 0 1 0 47.0,35.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M50,46 L50,65"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M22.0,48.0 A12.0,12.0 0 1 0 46.0,48.0 A12.0,12.0 0 1 0 22.0,48.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M46,48 L78,48"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M68,48 L68,60"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M78,48 L78,56"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M22.0,48.0 A12.0,12.0 0 1 0 46.0,48.0 A12.0,12.0 0 1 0 22.0,48.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M46,48 L78,48"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M68,48 L68,60"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M78,48 L78,56"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M32.0,21.0 H68.0 A8.0,8.0 0 0 1 76.0,29.0 V52.0 A8.0,8.0 0 0 1 68.0,60.0 H32.0 A8.0,8.0 0 0 1 24.0,52.0 V29.0 A8.0,8.0 0 0 1 32.0,21.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M17 70 H83 L77 77 H23 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M32.0,21.0 H68.0 A8.0,8.0 0 0 1 76.0,29.0 V52.0 A8.0,8.0 0 0 1 68.0,60.0 H32.0 A8.0,8.0 0 0 1 24.0,52.0 V29.0 A8.0,8.0 0 0 1 32.0,21.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M17 70 H83 L77 77 H23 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M35.0,27.0 H65.0 A5.0,5.0 0 0 1 70.0,32.0 V49.0 A5.0,5.0 0 0 1 65.0,54.0 H35.0 A5.0,5.0 0 0 1 30.0,49.0 V32.0 A5.0,5.0 0 0 1 35.0,27.0 Z"
android:fillColor="#FFFFFF" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M31.0,25.0 H57.0 A11.0,11.0 0 0 1 68.0,36.0 V66.0 A11.0,11.0 0 0 1 57.0,77.0 H31.0 A11.0,11.0 0 0 1 20.0,66.0 V36.0 A11.0,11.0 0 0 1 31.0,25.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M43.0,36.0 H69.0 A11.0,11.0 0 0 1 80.0,47.0 V66.0 A11.0,11.0 0 0 1 69.0,77.0 H43.0 A11.0,11.0 0 0 1 32.0,66.0 V47.0 A11.0,11.0 0 0 1 43.0,36.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M44 25 V47 L50 43 L56 47 V25"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M31.0,25.0 H57.0 A11.0,11.0 0 0 1 68.0,36.0 V66.0 A11.0,11.0 0 0 1 57.0,77.0 H31.0 A11.0,11.0 0 0 1 20.0,66.0 V36.0 A11.0,11.0 0 0 1 31.0,25.0 Z"
android:fillColor="#0A84FF"
android:fillAlpha="0.35" />
<path
android:pathData="M43.0,36.0 H69.0 A11.0,11.0 0 0 1 80.0,47.0 V66.0 A11.0,11.0 0 0 1 69.0,77.0 H43.0 A11.0,11.0 0 0 1 32.0,66.0 V47.0 A11.0,11.0 0 0 1 43.0,36.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M44 25 V47 L50 43 L56 47 V25"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.6"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M39 60 L31 68 C23 76 14 75 10 68 C7 62 9 57 15 51 L29 38 C37 31 44 31 49 36"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M61 40 L69 32 C77 24 86 25 90 32 C93 38 91 43 85 49 L71 62 C63 69 56 69 51 64"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M38,62 L62,38"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M39 60 L31 68 C23 76 14 75 10 68 C7 62 9 57 15 51 L29 38 C37 31 44 31 49 36"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M61 40 L69 32 C77 24 86 25 90 32 C93 38 91 43 85 49 L71 62 C63 69 56 69 51 64"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M38,62 L62,38"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M36.0,42.0 H64.0 A10.0,10.0 0 0 1 74.0,52.0 V67.0 A10.0,10.0 0 0 1 64.0,77.0 H36.0 A10.0,10.0 0 0 1 26.0,67.0 V52.0 A10.0,10.0 0 0 1 36.0,42.0 Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M33 42 V37 C33 26 40 20 50 20 C60 20 67 26 67 37 V42"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M47.0,58.0 A3.0,3.0 0 1 0 53.0,58.0 A3.0,3.0 0 1 0 47.0,58.0 Z"
android:fillColor="#1C1C1E" />
<path
android:pathData="M50,61 L50,68"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="5.6"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M36.0,42.0 H64.0 A10.0,10.0 0 0 1 74.0,52.0 V67.0 A10.0,10.0 0 0 1 64.0,77.0 H36.0 A10.0,10.0 0 0 1 26.0,67.0 V52.0 A10.0,10.0 0 0 1 36.0,42.0 Z"
android:fillColor="#0A84FF" />
<path
android:pathData="M33 42 V37 C33 26 40 20 50 20 C60 20 67 26 67 37 V42"
android:fillColor="@android:color/transparent"
android:strokeColor="#0A84FF"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M47.0,58.0 A3.0,3.0 0 1 0 53.0,58.0 A3.0,3.0 0 1 0 47.0,58.0 Z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M50,61 L50,68"
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFF"
android:strokeWidth="5.6"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M57 22 H72 C78 22 81 26 81 32 V68 C81 74 78 78 72 78 H57 M57 22 V39 M57 61 V78"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M18,50 L53,50"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M42 39 L53 50 L42 61"
android:fillColor="@android:color/transparent"
android:strokeColor="#1C1C1E"
android:strokeWidth="6.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
Loaded 100 of 185 files, more files were not shown because too many files have changed in this diff. Show more