修复播放上报并完善自适应液态玻璃
This commit is contained in:
1 parent
5df288ba9d
commit
d3ce44e3da
175 files changed
+6873
-1675
No files matched your search
@@ -7,6 +7,10 @@ android {
|
||||
namespace = "com.example.myapplication"
|
||||
compileSdk = 35
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.myapplication"
|
||||
minSdk = 24
|
||||
@@ -49,12 +53,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()
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,12 @@ 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.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
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 +43,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
|
||||
@@ -96,8 +100,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 +127,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 +142,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 +160,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,7 @@ 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 top.yukonga.miuix.kmp.basic.Text
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@@ -254,7 +255,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,12 +268,13 @@ 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) }
|
||||
|
||||
@@ -299,9 +301,10 @@ private fun PlayerDestination(
|
||||
if (server == null || server.apiKey.isBlank() || target == null) return@LaunchedEffect
|
||||
playbackLoading = true
|
||||
playbackError = null
|
||||
playbackUrl = EmbyRepository.resolvePlaybackUrl(server, target)
|
||||
playbackSession = EmbyRepository.resolvePlaybackSession(server, target)
|
||||
playbackUrl = playbackSession?.streamUrl
|
||||
playbackLoading = false
|
||||
if (playbackUrl.isNullOrBlank()) playbackError = "获取播放地址失败"
|
||||
if (playbackUrl.isNullOrBlank()) playbackError = playbackUrlFailedMessage
|
||||
}
|
||||
|
||||
when {
|
||||
@@ -321,28 +324,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +355,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 {
|
||||
|
||||
@@ -32,10 +32,6 @@ 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
|
||||
@@ -60,7 +56,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 +68,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.LumaLoading
|
||||
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
|
||||
@@ -93,10 +97,6 @@ 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 +104,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 +178,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 +214,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 +256,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 +285,7 @@ private fun HeroCarousel(
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Spacer(Modifier.height(LumaSpacing.sm))
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
@@ -286,7 +331,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 +357,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 +393,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 +428,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 +444,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 +465,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 +492,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 +517,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 +532,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 +543,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 +574,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 +652,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 ->
|
||||
@@ -582,7 +662,16 @@ fun HomeScreen(
|
||||
}
|
||||
} else if (isLoading) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = adaptiveAccent)
|
||||
LumaLoading(label = stringResource(Res.string.loading_home))
|
||||
}
|
||||
} else if (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++ },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val server = activeServer
|
||||
@@ -612,12 +701,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 +743,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 +761,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 +778,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,6 +21,7 @@ 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
|
||||
@@ -32,7 +33,6 @@ 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 +58,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 +101,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 +124,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 +159,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 +168,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 +212,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 +225,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 +255,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 +291,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 +311,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 +341,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 +360,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 = "返回",
|
||||
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 +429,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 +522,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) {
|
||||
detail.communityRating?.takeIf { it > 0.0 }?.let { rating ->
|
||||
Icon(imageVector = Icons.Filled.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 +540,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 +551,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 +563,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 +600,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 +624,7 @@ private fun EpisodeListDialog(
|
||||
serverUrl: String,
|
||||
fallbackImageUrl: String?,
|
||||
background: PosterImmersivePalette,
|
||||
backdrop: Backdrop?,
|
||||
onDismiss: () -> Unit,
|
||||
onSelect: (EmbyItem) -> Unit,
|
||||
) {
|
||||
@@ -589,20 +634,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 +657,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 +699,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 +718,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 +737,7 @@ private fun CompactEpisodeRow(
|
||||
.background(Color(0xFF152036)),
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
LumaAsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = item.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
@@ -695,7 +746,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 +762,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 +793,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 +830,7 @@ private fun EpisodeCard(
|
||||
val radius = (21.dp - strokeWidth / 2f).toPx()
|
||||
drawRoundRect(
|
||||
brush = if (selected) {
|
||||
rainbowBrush
|
||||
selectedBorderBrush
|
||||
} else {
|
||||
normalBorderBrush
|
||||
},
|
||||
@@ -814,7 +842,7 @@ private fun EpisodeCard(
|
||||
},
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
LumaAsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = item.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
@@ -828,7 +856,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 +869,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 +900,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 +919,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 +950,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.LumaLoading
|
||||
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,32 @@ 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) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = GlassTokens.accent)
|
||||
LumaLoading(label = stringResource(Res.string.loading_favorites))
|
||||
}
|
||||
} else if (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++ },
|
||||
)
|
||||
}
|
||||
} 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 +172,7 @@ fun RecordsScreen(
|
||||
|
||||
@Composable
|
||||
private fun RecordsEmptyState(
|
||||
icon: ImageVector,
|
||||
icon: LumaIcon,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
) {
|
||||
@@ -162,23 +182,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 +197,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.LumaLoading
|
||||
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,33 @@ 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)
|
||||
LumaLoading(label = stringResource(Res.string.loading_search))
|
||||
}
|
||||
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 +173,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 +195,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
|
||||
@@ -24,21 +26,39 @@ 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 +68,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))
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, 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 +83,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 +321,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
|
||||
@@ -27,12 +29,9 @@ 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 +46,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 +62,27 @@ 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.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 +96,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 +110,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 +163,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 +229,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 +238,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,
|
||||
tint = LumaColors.textTertiary,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.then(
|
||||
@@ -242,7 +295,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 +390,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 +427,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 +453,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.background(LumaColors.fallbackGlass)
|
||||
},
|
||||
)
|
||||
.border(0.7.dp, border, shape)
|
||||
@@ -397,11 +518,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))
|
||||
|
||||
@@ -7,12 +7,15 @@ import android.content.pm.ActivityInfo
|
||||
import android.view.Surface
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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
|
||||
@@ -31,21 +34,9 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.List
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.SkipNext
|
||||
import androidx.compose.material.icons.filled.SkipPrevious
|
||||
import androidx.compose.material.icons.filled.FastForward
|
||||
import androidx.compose.material.icons.filled.FastRewind
|
||||
import androidx.compose.material.icons.filled.Subtitles
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
@@ -62,8 +53,8 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
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.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -82,18 +73,14 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.example.myapplication.data.EmbyItem
|
||||
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
|
||||
import com.kyant.shapes.Capsule
|
||||
import com.kyant.shapes.RoundedRectangle
|
||||
import com.example.myapplication.R as Res
|
||||
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.LumaGlassLevel
|
||||
import com.example.myapplication.ui.design.lumaGlass
|
||||
import com.example.myapplication.ui.design.LumaShapes
|
||||
import com.example.myapplication.ui.LocalGlassBackdrop
|
||||
import kotlinx.coroutines.delay
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
|
||||
@@ -111,8 +98,6 @@ private tailrec fun Context.findActivity(): Activity? = when (this) {
|
||||
else -> null
|
||||
}
|
||||
|
||||
private val LocalPlayerBackdrop = staticCompositionLocalOf<Backdrop?> { null }
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
fun VideoPlayerScreen(
|
||||
@@ -126,19 +111,20 @@ fun VideoPlayerScreen(
|
||||
onSelectEpisode: (EmbyItem) -> Unit = {},
|
||||
onClose: () -> Unit = {},
|
||||
onPlaybackStarted: (positionMs: Long) -> Unit = {},
|
||||
onPlaybackProgress: (positionMs: Long, paused: Boolean) -> Unit = { _, _ -> },
|
||||
onPlaybackProgress: (positionMs: Long, paused: Boolean, eventName: String) -> Unit = { _, _, _ -> },
|
||||
onPlaybackStopped: (positionMs: Long) -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val cannotPlayMessage = stringResource(Res.string.player_cannot_play)
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var showSubtitleMenu by remember { mutableStateOf(false) }
|
||||
var showEpisodeMenu by remember { mutableStateOf(false) }
|
||||
val player = remember { ExoPlayer.Builder(context).build() }
|
||||
val playerBackdrop = rememberLayerBackdrop()
|
||||
val latestPlaybackUrl by rememberUpdatedState(playbackUrl)
|
||||
val latestOnPlaybackStarted by rememberUpdatedState(onPlaybackStarted)
|
||||
val latestOnPlaybackProgress by rememberUpdatedState(onPlaybackProgress)
|
||||
val latestOnPlaybackStopped by rememberUpdatedState(onPlaybackStopped)
|
||||
var stopReported by remember(playbackUrl) { mutableStateOf(false) }
|
||||
|
||||
var progress by remember { mutableFloatStateOf(0f) }
|
||||
var isPlaying by remember { mutableStateOf(false) }
|
||||
@@ -164,8 +150,8 @@ fun VideoPlayerScreen(
|
||||
|
||||
LaunchedEffect(player, playbackUrl) {
|
||||
while (true) {
|
||||
if (!playbackUrl.isNullOrBlank() && player.currentMediaItem != null) {
|
||||
latestOnPlaybackProgress(player.currentPosition, !player.isPlaying)
|
||||
if (!playbackUrl.isNullOrBlank() && player.currentMediaItem != null && player.isPlaying) {
|
||||
latestOnPlaybackProgress(player.currentPosition, false, "TimeUpdate")
|
||||
}
|
||||
delay(10_000)
|
||||
}
|
||||
@@ -185,16 +171,30 @@ fun VideoPlayerScreen(
|
||||
player.play()
|
||||
latestOnPlaybackStarted(initialPositionMs.coerceAtLeast(0L))
|
||||
}
|
||||
onDispose { }
|
||||
onDispose {
|
||||
if (!playbackUrl.isNullOrBlank() && !stopReported) {
|
||||
stopReported = true
|
||||
latestOnPlaybackStopped(player.currentPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
DisposableEffect(player) {
|
||||
val listener = object : Player.Listener {
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
playerError = error.message ?: "播放器无法播放该资源"
|
||||
playerError = cannotPlayMessage
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (!latestPlaybackUrl.isNullOrBlank()) {
|
||||
latestOnPlaybackProgress(player.currentPosition, !isPlaying, if (isPlaying) "Unpause" else "Pause")
|
||||
}
|
||||
}
|
||||
}
|
||||
player.addListener(listener)
|
||||
onDispose { player.removeListener(listener) }
|
||||
onDispose {
|
||||
player.removeListener(listener)
|
||||
player.release()
|
||||
}
|
||||
}
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
var resumeAfterStop = false
|
||||
@@ -213,31 +213,22 @@ fun VideoPlayerScreen(
|
||||
lifecycleOwner.lifecycle.addObserver(obs)
|
||||
onDispose { lifecycleOwner.lifecycle.removeObserver(obs) }
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (!latestPlaybackUrl.isNullOrBlank()) {
|
||||
latestOnPlaybackStopped(player.currentPosition)
|
||||
}
|
||||
player.release()
|
||||
}
|
||||
}
|
||||
BackHandler(enabled = true, onBack = onClose)
|
||||
|
||||
CompositionLocalProvider(LocalPlayerBackdrop provides playerBackdrop) {
|
||||
Box(Modifier.fillMaxSize().background(Color.Black).clickable(
|
||||
Box(Modifier.fillMaxSize().background(Color.Black).clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) { controlsVisible = !controlsVisible }) {
|
||||
if (playbackUrl.isNullOrBlank()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
when {
|
||||
isLoading -> CircularProgressIndicator(color = Color.White)
|
||||
isLoading -> LumaLoading(label = stringResource(Res.string.player_preparing))
|
||||
errorText != null -> Text(text = errorText, color = Color.White, modifier = Modifier.padding(24.dp))
|
||||
else -> Text(text = "正在准备播放...", color = Color.White)
|
||||
else -> Text(text = stringResource(Res.string.player_preparing), color = Color.White)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize().layerBackdrop(playerBackdrop)) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
PlayerView(ctx).also { pv ->
|
||||
@@ -246,9 +237,7 @@ fun VideoPlayerScreen(
|
||||
pv.resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { alpha = 0.999f },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -256,7 +245,7 @@ fun VideoPlayerScreen(
|
||||
if (playerError != null) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = playerError ?: "播放失败",
|
||||
text = playerError ?: stringResource(Res.string.player_failed),
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(24.dp),
|
||||
)
|
||||
@@ -288,28 +277,53 @@ fun VideoPlayerScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
LiquidSurfaceButton(icon = Icons.Filled.FastRewind, active = false, size = 48.dp) {
|
||||
LiquidSurfaceButton(
|
||||
icon = LumaIcon.Replay,
|
||||
contentDescription = stringResource(Res.string.player_seek_backward),
|
||||
active = false,
|
||||
size = 48.dp,
|
||||
) {
|
||||
val pos = player.currentPosition - 10_000L
|
||||
player.seekTo(maxOf(0L, pos))
|
||||
latestOnPlaybackProgress(player.currentPosition, !player.isPlaying, "TimeUpdate")
|
||||
}
|
||||
LiquidSurfaceButton(icon = Icons.Filled.SkipPrevious, active = false, size = 56.dp) {
|
||||
LiquidSurfaceButton(
|
||||
icon = LumaIcon.Previous,
|
||||
contentDescription = stringResource(Res.string.player_previous_episode),
|
||||
active = false,
|
||||
size = 56.dp,
|
||||
) {
|
||||
val idx = episodes.indexOfFirst { it.id == currentEpisodeId }
|
||||
if (idx > 0) onSelectEpisode(episodes[idx - 1])
|
||||
}
|
||||
LiquidSurfaceButton(icon = if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, active = true, size = 72.dp, iconSize = 40.dp) {
|
||||
LiquidSurfaceButton(
|
||||
icon = if (isPlaying) LumaIcon.Pause else LumaIcon.Play,
|
||||
contentDescription = stringResource(if (isPlaying) Res.string.player_pause else Res.string.player_play),
|
||||
active = true,
|
||||
size = 72.dp,
|
||||
iconSize = 40.dp,
|
||||
) {
|
||||
if (player.isPlaying) {
|
||||
player.pause()
|
||||
latestOnPlaybackProgress(player.currentPosition, true)
|
||||
} else {
|
||||
player.play()
|
||||
latestOnPlaybackProgress(player.currentPosition, false)
|
||||
}
|
||||
}
|
||||
LiquidSurfaceButton(icon = Icons.Filled.SkipNext, active = false, size = 56.dp) {
|
||||
LiquidSurfaceButton(
|
||||
icon = LumaIcon.Next,
|
||||
contentDescription = stringResource(Res.string.player_next_episode),
|
||||
active = false,
|
||||
size = 56.dp,
|
||||
) {
|
||||
val idx = episodes.indexOfFirst { it.id == currentEpisodeId }
|
||||
if (idx >= 0 && idx < episodes.lastIndex) onSelectEpisode(episodes[idx + 1])
|
||||
}
|
||||
LiquidSurfaceButton(icon = Icons.Filled.FastForward, active = false, size = 48.dp) {
|
||||
LiquidSurfaceButton(
|
||||
icon = LumaIcon.Speed,
|
||||
contentDescription = stringResource(Res.string.player_seek_forward),
|
||||
active = false,
|
||||
size = 48.dp,
|
||||
) {
|
||||
val pos = player.currentPosition + 10_000L
|
||||
player.seekTo(minOf(pos, player.duration))
|
||||
}
|
||||
@@ -322,10 +336,20 @@ fun VideoPlayerScreen(
|
||||
) {
|
||||
Text(title, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
LiquidSurfaceButton(icon = Icons.Filled.Subtitles, active = showSubtitleMenu, size = 40.dp) {
|
||||
LiquidSurfaceButton(
|
||||
icon = LumaIcon.Subtitle,
|
||||
contentDescription = stringResource(Res.string.player_subtitle),
|
||||
active = showSubtitleMenu,
|
||||
size = 40.dp,
|
||||
) {
|
||||
showSubtitleMenu = !showSubtitleMenu; showEpisodeMenu = false
|
||||
}
|
||||
LiquidSurfaceButton(icon = Icons.Filled.List, active = showEpisodeMenu, size = 40.dp) {
|
||||
LiquidSurfaceButton(
|
||||
icon = LumaIcon.Watch,
|
||||
contentDescription = stringResource(Res.string.player_episode_list),
|
||||
active = showEpisodeMenu,
|
||||
size = 40.dp,
|
||||
) {
|
||||
showEpisodeMenu = !showEpisodeMenu; showSubtitleMenu = false
|
||||
}
|
||||
}
|
||||
@@ -337,11 +361,13 @@ fun VideoPlayerScreen(
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(0.85f)
|
||||
.padding(bottom = 32.dp)
|
||||
.drawBackdrop(
|
||||
backdrop = playerBackdrop,
|
||||
shape = { Capsule() },
|
||||
effects = { blur(16f.dp.toPx()); vibrancy() },
|
||||
onDrawSurface = { drawRect(Color.Black.copy(alpha = 0.4f)) },
|
||||
.lumaGlass(
|
||||
backdrop = null,
|
||||
radius = 24.dp,
|
||||
level = LumaGlassLevel.Strong,
|
||||
tint = Color.Black.copy(alpha = 0.40f),
|
||||
borderColor = Color.White.copy(alpha = 0.16f),
|
||||
shadowElevation = 4.dp,
|
||||
)
|
||||
.padding(horizontal = 20.dp, vertical = 12.dp),
|
||||
) {
|
||||
@@ -368,7 +394,11 @@ fun VideoPlayerScreen(
|
||||
Slider(
|
||||
value = if (isSeeking) seekPosition else progress,
|
||||
onValueChange = { v -> isSeeking = true; seekPosition = v },
|
||||
onValueChangeFinished = { player.seekTo((seekPosition * player.duration).toLong()); isSeeking = false },
|
||||
onValueChangeFinished = {
|
||||
player.seekTo((seekPosition * player.duration).toLong())
|
||||
isSeeking = false
|
||||
latestOnPlaybackProgress(player.currentPosition, !player.isPlaying, "TimeUpdate")
|
||||
},
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 12.dp),
|
||||
colors = SliderDefaults.colors(thumbColor = Color.White, activeTrackColor = Color.White)
|
||||
)
|
||||
@@ -380,12 +410,12 @@ fun VideoPlayerScreen(
|
||||
}
|
||||
|
||||
if (showSubtitleMenu) {
|
||||
LiquidGlassPanel(title = "字幕", onDismiss = { showSubtitleMenu = false }) {
|
||||
LiquidGlassPanel(title = stringResource(Res.string.player_subtitle), onDismiss = { showSubtitleMenu = false }) {
|
||||
SubtitleMenuContent(player = player) { showSubtitleMenu = false }
|
||||
}
|
||||
}
|
||||
if (showEpisodeMenu) {
|
||||
LiquidGlassPanel(title = title.ifBlank { "剧集列表" }, onDismiss = { showEpisodeMenu = false }) {
|
||||
LiquidGlassPanel(title = title.ifBlank { stringResource(Res.string.player_episode_list) }, onDismiss = { showEpisodeMenu = false }) {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(items = episodes) { ep ->
|
||||
LiquidEpisodeItem(
|
||||
@@ -398,41 +428,79 @@ fun VideoPlayerScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LiquidSurfaceButton(
|
||||
icon: ImageVector,
|
||||
icon: LumaIcon,
|
||||
contentDescription: String? = null,
|
||||
active: Boolean,
|
||||
size: Dp = 44.dp,
|
||||
iconSize: Dp = 24.dp,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val bd = LocalPlayerBackdrop.current
|
||||
val bd = LocalGlassBackdrop.current
|
||||
val surfaceColor = if (active) Color.White.copy(alpha = 0.3f) else Color.White.copy(alpha = 0.1f)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (pressed) 0.94f else 1f,
|
||||
animationSpec = tween(180),
|
||||
label = "playerMaterialIconPress",
|
||||
)
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.then(
|
||||
if (bd != null) {
|
||||
Modifier.drawBackdrop(
|
||||
backdrop = bd,
|
||||
shape = { Capsule() },
|
||||
effects = { blur(12f.dp.toPx()); vibrancy() },
|
||||
onDrawSurface = { drawRect(surfaceColor) },
|
||||
)
|
||||
} else {
|
||||
Modifier.background(surfaceColor)
|
||||
}
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.lumaGlass(
|
||||
backdrop = bd,
|
||||
radius = size / 2f,
|
||||
level = LumaGlassLevel.Interactive,
|
||||
tint = surfaceColor,
|
||||
borderColor = Color.White.copy(alpha = 0.3f),
|
||||
shadowElevation = 4.dp,
|
||||
)
|
||||
.border(0.5.dp, Color.White.copy(alpha = 0.3f), CircleShape)
|
||||
.clickable(onClick = onClick),
|
||||
.clickable(indication = null, interactionSource = interactionSource, onClick = onClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(icon, null, tint = Color.White, modifier = Modifier.size(iconSize))
|
||||
LumaIconView(icon, contentDescription = contentDescription, tint = Color.White, modifier = Modifier.size(iconSize))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LiquidSurfaceButton(
|
||||
icon: LumaIcon,
|
||||
active: Boolean,
|
||||
size: Dp = 44.dp,
|
||||
iconSize: Dp = 24.dp,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val bd = LocalGlassBackdrop.current
|
||||
val surfaceColor = if (active) Color.White.copy(alpha = 0.26f) else Color.White.copy(alpha = 0.09f)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (pressed) 0.94f else 1f,
|
||||
animationSpec = tween(180),
|
||||
label = "playerIconPress",
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.size(size)
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.lumaGlass(
|
||||
backdrop = bd,
|
||||
radius = size / 2f,
|
||||
level = LumaGlassLevel.Interactive,
|
||||
tint = surfaceColor,
|
||||
borderColor = Color.White.copy(alpha = 0.22f),
|
||||
shadowElevation = 4.dp,
|
||||
)
|
||||
.clickable(indication = null, interactionSource = interactionSource, onClick = onClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LumaIconView(icon, contentDescription = null, tint = Color.White, modifier = Modifier.size(iconSize))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,39 +510,32 @@ private fun LiquidGlassPanel(
|
||||
onDismiss: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val bd = LocalPlayerBackdrop.current
|
||||
val bd = LocalGlassBackdrop.current
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.3f))
|
||||
.clickable(onClick = onDismiss),
|
||||
.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = onDismiss),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.width(320.dp)
|
||||
.clip(RoundedCornerShape(topStart = 24.dp, bottomStart = 24.dp))
|
||||
.then(
|
||||
if (bd != null) {
|
||||
Modifier.drawBackdrop(
|
||||
backdrop = bd,
|
||||
shape = { RoundedRectangle(24.dp) },
|
||||
effects = {
|
||||
colorControls(brightness = 0f, saturation = 1.15f)
|
||||
vibrancy()
|
||||
blur(24f.dp.toPx())
|
||||
lens(24f.dp.toPx(), 42f.dp.toPx(), depthEffect = true)
|
||||
},
|
||||
highlight = { Highlight.Plain },
|
||||
shadow = { Shadow(alpha = 0.22f) },
|
||||
onDrawSurface = { drawRect(Color.Black.copy(alpha = 0.5f)) },
|
||||
)
|
||||
} else {
|
||||
Modifier.background(Color.Black.copy(alpha = 0.8f))
|
||||
}
|
||||
.lumaGlass(
|
||||
backdrop = bd,
|
||||
radius = 24.dp,
|
||||
level = LumaGlassLevel.Strong,
|
||||
tint = Color.White.copy(alpha = 0.08f),
|
||||
borderColor = Color.White.copy(alpha = 0.16f),
|
||||
shadowElevation = 8.dp,
|
||||
)
|
||||
.clickable(
|
||||
enabled = false,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {},
|
||||
)
|
||||
.clickable(enabled = false) {}
|
||||
.padding(20.dp),
|
||||
) {
|
||||
Text(title, color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||
@@ -501,7 +562,7 @@ private fun SubtitleMenuContent(player: ExoPlayer, onDismiss: () -> Unit) {
|
||||
}
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
item {
|
||||
LiquidMenuItem(text = "关闭字幕", selected = !player.currentTracks.groups.any { it.type == C.TRACK_TYPE_TEXT && it.isSelected }) {
|
||||
LiquidMenuItem(text = stringResource(Res.string.player_disable_subtitle), selected = !player.currentTracks.groups.any { it.type == C.TRACK_TYPE_TEXT && it.isSelected }) {
|
||||
player.trackSelectionParameters = player.trackSelectionParameters.buildUpon().setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true).build()
|
||||
onDismiss()
|
||||
}
|
||||
@@ -519,65 +580,51 @@ private fun SubtitleMenuContent(player: ExoPlayer, onDismiss: () -> Unit) {
|
||||
}
|
||||
|
||||
private data class SubtitleTrackInfo(
|
||||
@androidx.annotation.OptIn(UnstableApi::class) val group: androidx.media3.common.Tracks.Group,
|
||||
@param:androidx.annotation.OptIn(UnstableApi::class) val group: androidx.media3.common.Tracks.Group,
|
||||
val index: Int,
|
||||
val label: String
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun LiquidMenuItem(text: String, selected: Boolean, onClick: () -> Unit) {
|
||||
val bd = LocalPlayerBackdrop.current
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
val surface = if (selected) Color.White.copy(alpha = 0.18f) else Color.White.copy(alpha = 0.05f)
|
||||
val bd = LocalGlassBackdrop.current
|
||||
val surface = if (selected) Color.White.copy(alpha = 0.18f) else Color.White.copy(alpha = 0.06f)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.then(
|
||||
if (bd != null) {
|
||||
Modifier.drawBackdrop(
|
||||
backdrop = bd,
|
||||
shape = { RoundedRectangle(12.dp) },
|
||||
effects = { vibrancy(); blur(8.dp.toPx()); lens(10.dp.toPx(), 16.dp.toPx()) },
|
||||
onDrawSurface = { drawRect(surface) },
|
||||
)
|
||||
} else {
|
||||
Modifier.background(surface)
|
||||
}
|
||||
.lumaGlass(
|
||||
backdrop = bd,
|
||||
radius = LumaShapes.small,
|
||||
level = LumaGlassLevel.Interactive,
|
||||
tint = surface,
|
||||
borderColor = Color.White.copy(alpha = 0.16f),
|
||||
shadowElevation = 0.dp,
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.border(0.5.dp, Color.White.copy(alpha = 0.18f), shape)
|
||||
.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = onClick)
|
||||
.padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text, color = Color.White, fontSize = 14.sp, modifier = Modifier.weight(1f))
|
||||
if (selected) Icon(Icons.Filled.PlayArrow, null, tint = Color.White, modifier = Modifier.size(16.dp))
|
||||
if (selected) LumaIconView(LumaIcon.Play, contentDescription = null, tint = Color.White, modifier = Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LiquidEpisodeItem(episode: EmbyItem, displayIndex: Int, selected: Boolean, onClick: () -> Unit) {
|
||||
val bd = LocalPlayerBackdrop.current
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
val surface = if (selected) Color.White.copy(alpha = 0.18f) else Color.White.copy(alpha = 0.05f)
|
||||
val bd = LocalGlassBackdrop.current
|
||||
val surface = if (selected) Color.White.copy(alpha = 0.18f) else Color.White.copy(alpha = 0.06f)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.then(
|
||||
if (bd != null) {
|
||||
Modifier.drawBackdrop(
|
||||
backdrop = bd,
|
||||
shape = { RoundedRectangle(12.dp) },
|
||||
effects = { vibrancy(); blur(8.dp.toPx()); lens(10.dp.toPx(), 16.dp.toPx()) },
|
||||
onDrawSurface = { drawRect(surface) },
|
||||
)
|
||||
} else {
|
||||
Modifier.background(surface)
|
||||
}
|
||||
.lumaGlass(
|
||||
backdrop = bd,
|
||||
radius = LumaShapes.small,
|
||||
level = LumaGlassLevel.Interactive,
|
||||
tint = surface,
|
||||
borderColor = Color.White.copy(alpha = 0.16f),
|
||||
shadowElevation = 0.dp,
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.border(0.5.dp, Color.White.copy(alpha = 0.18f), shape)
|
||||
.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = onClick)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
@@ -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(30.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(34.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
|
||||
@@ -79,6 +74,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
|
||||
@@ -96,6 +92,15 @@ 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 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 androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Apps
|
||||
import androidx.compose.material.icons.filled.GridView
|
||||
@@ -113,16 +118,15 @@ import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.Storage
|
||||
|
||||
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 +139,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 +184,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 +219,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>,
|
||||
@@ -357,14 +231,13 @@ fun SegmentedControl(
|
||||
modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.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) },
|
||||
)
|
||||
@@ -380,17 +253,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 +271,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,7 +293,7 @@ fun SegmentedControl(
|
||||
}
|
||||
)
|
||||
.then(
|
||||
if (sel && backdrop == null) {
|
||||
if (sel && (backdrop == null || !LumaGlassRuntime.useNativeBackdrop)) {
|
||||
Modifier.background(selectedFill)
|
||||
} else {
|
||||
Modifier
|
||||
@@ -456,32 +328,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 +362,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 +371,25 @@ fun GlassSearchField(
|
||||
}
|
||||
},
|
||||
)
|
||||
if (value.isNotEmpty()) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(40.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 +414,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 +473,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 +484,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.background(LumaColors.fallbackGlass)
|
||||
}
|
||||
)
|
||||
.border(0.7.dp, border, shape)
|
||||
@@ -631,10 +516,12 @@ fun GlassPopupItem(
|
||||
tint: Color = Color(0xFF374151),
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.heightIn(min = 48.dp)
|
||||
.clickable(indication = null, interactionSource = interactionSource, onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
@@ -646,6 +533,27 @@ fun GlassPopupItem(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlassPopupItem(
|
||||
text: String,
|
||||
icon: LumaIcon,
|
||||
tint: Color = LumaColors.text,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.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),
|
||||
) {
|
||||
LumaIconView(icon, contentDescription = null, tint = tint, modifier = Modifier.size(18.dp))
|
||||
Text(text = text, color = tint, fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LiquidGlassButton(
|
||||
onClick: () -> Unit,
|
||||
@@ -657,18 +565,29 @@ 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
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
alpha = if (enabled) 1f else 0.52f
|
||||
}
|
||||
.clip(shape)
|
||||
.then(
|
||||
if (backdrop != null) {
|
||||
if (backdrop != null && LumaGlassRuntime.useNativeBackdrop) {
|
||||
Modifier.drawBackdrop(
|
||||
backdrop = backdrop,
|
||||
shape = { RoundedRectangle(cornerRadius) },
|
||||
effects = {
|
||||
vibrancy()
|
||||
blur(8.dp.toPx())
|
||||
lens(12.dp.toPx(), 12.dp.toPx())
|
||||
},
|
||||
onDrawSurface = { drawRect(fill) },
|
||||
)
|
||||
@@ -677,7 +596,7 @@ fun LiquidGlassButton(
|
||||
}
|
||||
)
|
||||
.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 +644,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 +651,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 +666,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,16 +755,16 @@ 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) {
|
||||
if (backdrop != null && LumaGlassRuntime.useNativeBackdrop) {
|
||||
Modifier.drawBackdrop(
|
||||
backdrop = backdrop,
|
||||
shape = { RoundedRectangle(12.dp) },
|
||||
effects = { vibrancy(); blur(8.dp.toPx()); lens(10.dp.toPx(), 18.dp.toPx()) },
|
||||
effects = { vibrancy(); blur(8.dp.toPx()) },
|
||||
onDrawSurface = { drawRect(GlassTokens.glassFillSoft) },
|
||||
)
|
||||
} else {
|
||||
@@ -811,7 +778,7 @@ fun GlassInputField(
|
||||
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 +786,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 +797,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,217 @@
|
||||
package com.example.myapplication.ui.design
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
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.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
|
||||
@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) {
|
||||
Text(
|
||||
action,
|
||||
color = LumaColors.accent,
|
||||
fontSize = LumaTypography.secondary,
|
||||
modifier = Modifier.clickable(enabled = onAction != null, indication = null, interactionSource = remember { MutableInteractionSource() }) { onAction?.invoke() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,387 @@
|
||||
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 tint used when native backdrop sampling is disabled on an emulator or old API. */
|
||||
val fallbackGlass: Color
|
||||
get() = if (isDark) Color(0xFF1A2330).copy(alpha = 0.86f) else Color(0xFFE4E9F0).copy(alpha = 0.86f)
|
||||
}
|
||||
|
||||
/** Stable frosted fallback for devices where Kyant Backdrop is disabled. */
|
||||
fun Modifier.lumaFallbackGlass(
|
||||
shape: Shape,
|
||||
selected: Boolean = false,
|
||||
): 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) {
|
||||
LumaColors.glassHighlight.copy(alpha = if (light) 0.34f else 0.26f)
|
||||
} else {
|
||||
LumaColors.glassHighlight.copy(alpha = if (light) 0.18f else 0.14f)
|
||||
}
|
||||
val fill = if (selected) {
|
||||
if (light) {
|
||||
Brush.horizontalGradient(
|
||||
listOf(
|
||||
LumaColors.accent.copy(alpha = 0.24f),
|
||||
Color.White.copy(alpha = 0.36f),
|
||||
Color.White.copy(alpha = 0.22f),
|
||||
LumaColors.accent.copy(alpha = 0.12f),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Brush.horizontalGradient(
|
||||
listOf(
|
||||
LumaColors.accent.copy(alpha = 0.28f),
|
||||
Color.White.copy(alpha = 0.16f),
|
||||
Color.White.copy(alpha = 0.08f),
|
||||
LumaColors.accent.copy(alpha = 0.18f),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Brush.verticalGradient(listOf(top, bottom))
|
||||
}
|
||||
return this
|
||||
.background(fill, shape)
|
||||
.then(
|
||||
if (selected) {
|
||||
Modifier.drawWithCache {
|
||||
val leftGlow = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
LumaColors.accent.copy(alpha = if (light) 0.22f else 0.26f),
|
||||
LumaColors.accent.copy(alpha = 0.06f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(size.width * 0.14f, size.height * 0.18f),
|
||||
radius = size.height * 1.25f,
|
||||
)
|
||||
val rightGlow = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = if (light) 0.22f else 0.12f),
|
||||
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.14f, 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
|
||||
},
|
||||
)
|
||||
.border(0.6.dp, edge, 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 fallbackSurface = if (usesDefaultGlassTint) LumaColors.fallbackGlass else surfaceTint
|
||||
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 {
|
||||
Modifier.background(fallbackSurface)
|
||||
},
|
||||
)
|
||||
.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,138 @@
|
||||
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),
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+19
-47
@@ -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,12 @@ 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.LumaShapes
|
||||
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 +71,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 +106,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 +139,7 @@ fun LiquidBottomTabs(
|
||||
}
|
||||
)
|
||||
}
|
||||
val externalIndex = selectedTabIndex()
|
||||
val externalIndex = selectedTabIndex().fastCoerceIn(0, tabsCount - 1)
|
||||
LaunchedEffect(externalIndex) {
|
||||
if (externalIndex != currentIndex) {
|
||||
currentIndex = externalIndex
|
||||
@@ -167,26 +173,40 @@ 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),
|
||||
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,58 @@ 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 = {
|
||||
val progress = dampedDragAnimation.pressProgress
|
||||
lens(
|
||||
10f.dp.toPx() * progress,
|
||||
14f.dp.toPx() * progress,
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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="#0A84FF"
|
||||
android:strokeWidth="6.2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M18,50 L53,50"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
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="#0A84FF"
|
||||
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="M31.0,22.0 H49.0 A10.0,10.0 0 0 1 59.0,32.0 V68.0 A10.0,10.0 0 0 1 49.0,78.0 H31.0 A10.0,10.0 0 0 1 21.0,68.0 V32.0 A10.0,10.0 0 0 1 31.0,22.0 Z"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M42,50 L80,50"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.4"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M69 39 L80 50 L69 61"
|
||||
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="M31.0,22.0 H49.0 A10.0,10.0 0 0 1 59.0,32.0 V68.0 A10.0,10.0 0 0 1 49.0,78.0 H31.0 A10.0,10.0 0 0 1 21.0,68.0 V32.0 A10.0,10.0 0 0 1 31.0,22.0 Z"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
android:strokeWidth="6.2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M42,50 L80,50"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
android:strokeWidth="6.4"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M69 39 L80 50 L69 61"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
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="M29.0,28.0 H71.0 A11.0,11.0 0 0 1 82.0,39.0 V61.0 A11.0,11.0 0 0 1 71.0,72.0 H29.0 A11.0,11.0 0 0 1 18.0,61.0 V39.0 A11.0,11.0 0 0 1 29.0,28.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 37.7 C40.8 36.4 38.4 38.0 38.4 40.6 V59.4 C38.4 62.0 40.8 63.6 43.0 62.3 L60.5 52.1 C63.8 50.2 63.8 49.8 60.5 47.9 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="M29.0,28.0 H71.0 A11.0,11.0 0 0 1 82.0,39.0 V61.0 A11.0,11.0 0 0 1 71.0,72.0 H29.0 A11.0,11.0 0 0 1 18.0,61.0 V39.0 A11.0,11.0 0 0 1 29.0,28.0 Z"
|
||||
android:fillColor="#0A84FF" />
|
||||
<path
|
||||
android:pathData="M43.0 37.7 C40.8 36.4 38.4 38.0 38.4 40.6 V59.4 C38.4 62.0 40.8 63.6 43.0 62.3 L60.5 52.1 C63.8 50.2 63.8 49.8 60.5 47.9 Z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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="M46.0,18.0 H54.0 A7.0,7.0 0 0 1 61.0,25.0 V29.0 A7.0,7.0 0 0 1 54.0,36.0 H46.0 A7.0,7.0 0 0 1 39.0,29.0 V25.0 A7.0,7.0 0 0 1 46.0,18.0 Z"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M25.0,64.0 H35.0 A7.0,7.0 0 0 1 42.0,71.0 V75.0 A7.0,7.0 0 0 1 35.0,82.0 H25.0 A7.0,7.0 0 0 1 18.0,75.0 V71.0 A7.0,7.0 0 0 1 25.0,64.0 Z"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M65.0,64.0 H75.0 A7.0,7.0 0 0 1 82.0,71.0 V75.0 A7.0,7.0 0 0 1 75.0,82.0 H65.0 A7.0,7.0 0 0 1 58.0,75.0 V71.0 A7.0,7.0 0 0 1 65.0,64.0 Z"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M50 36 V50"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M50 50 H30 V64"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M50 50 H70 V64"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user