mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
UI improvements & bug fixes for queuing
This commit is contained in:
parent
49adae9365
commit
82f8a63c86
7 changed files with 267 additions and 60 deletions
|
|
@ -87,7 +87,6 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
|
|
@ -326,9 +325,8 @@ class MainActivity : AppCompatActivity() {
|
|||
// ?: Destination.Home(),
|
||||
// TODO undo
|
||||
?: Destination.MediaItem(
|
||||
itemId = "7e64e319-657a-9516-ec78-490da03edccb".toUUID(),
|
||||
type = BaseItemKind.COLLECTION_FOLDER,
|
||||
collectionType = CollectionType.MUSIC,
|
||||
itemId = "011ef0c7-ca45-684f-2cd9-dd3b020ca5f6".toUUID(),
|
||||
type = BaseItemKind.MUSIC_ALBUM,
|
||||
),
|
||||
navigationManager = navigationManager,
|
||||
preferences = preferences,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class MusicService
|
|||
player.shuffleModeEnabled = shuffled
|
||||
player.play()
|
||||
}
|
||||
addAllToQueue(items)
|
||||
addAllToQueue(items, startIndex)
|
||||
}
|
||||
|
||||
suspend fun setQueue(
|
||||
|
|
@ -140,16 +140,26 @@ class MusicService
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun addAllToQueue(list: BlockingList<BaseItem?>) {
|
||||
suspend fun addAllToQueue(
|
||||
list: BlockingList<BaseItem?>,
|
||||
startIndex: Int,
|
||||
) {
|
||||
var remaining = startIndex
|
||||
list.indices
|
||||
.chunked(25)
|
||||
.forEach {
|
||||
val mediaItems =
|
||||
it.mapNotNull {
|
||||
list
|
||||
.getBlocking(it)
|
||||
?.takeIf { it.type == BaseItemKind.AUDIO }
|
||||
?.let(::convert)
|
||||
if (remaining == 0) {
|
||||
list
|
||||
.getBlocking(it)
|
||||
?.takeIf { it.type == BaseItemKind.AUDIO }
|
||||
?.let(::convert)
|
||||
} else {
|
||||
Timber.v("Skipping $remaining")
|
||||
remaining--
|
||||
null
|
||||
}
|
||||
}
|
||||
onMain { player.addMediaItems(mediaItems) }
|
||||
}
|
||||
|
|
@ -243,18 +253,24 @@ private class MusicPlayerListener(
|
|||
timeline: Timeline,
|
||||
reason: Int,
|
||||
) {
|
||||
Timber.v("MusicPlayerListener onTimelineChanged")
|
||||
// Timber.v("MusicPlayerListener onTimelineChanged")
|
||||
if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
|
||||
updateCurrentIndex()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCurrentIndex() {
|
||||
state.update {
|
||||
it.copy(
|
||||
currentIndex = player.currentMediaItemIndex,
|
||||
currentItemId = player.getMediaItemAt(player.currentMediaItemIndex).mediaId.toUUIDOrNull(),
|
||||
)
|
||||
state.update { state ->
|
||||
player.currentMediaItemIndex.takeIf { it >= 0 }?.let { currentMediaItemIndex ->
|
||||
if (currentMediaItemIndex in (0..<player.mediaItemCount)) {
|
||||
state.copy(
|
||||
currentIndex = currentMediaItemIndex,
|
||||
currentItemId = player.getMediaItemAt(currentMediaItemIndex).mediaId.toUUIDOrNull(),
|
||||
)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
} ?: state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,59 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.music
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.compose.state.rememberNextButtonState
|
||||
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
|
||||
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
||||
import androidx.media3.ui.compose.state.rememberRepeatButtonState
|
||||
import androidx.media3.ui.compose.state.rememberShuffleButtonState
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.AudioItem
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
||||
import com.github.damontecres.wholphin.ui.playback.PlaybackAction
|
||||
import com.github.damontecres.wholphin.ui.playback.PlaybackButtons
|
||||
import com.github.damontecres.wholphin.ui.playback.PlaybackFaButton
|
||||
import com.github.damontecres.wholphin.ui.playback.SeekBar
|
||||
import com.github.damontecres.wholphin.ui.roundSeconds
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
|
@ -43,20 +67,62 @@ fun NowPlayingOverlay(
|
|||
controllerViewState: ControllerViewState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
val playPauseState = rememberPlayPauseButtonState(player)
|
||||
val previousState = rememberPreviousButtonState(player)
|
||||
val nextState = rememberNextButtonState(player)
|
||||
Column(modifier = modifier.padding(16.dp)) {
|
||||
val shuffleState = rememberShuffleButtonState(player)
|
||||
val repeatState = rememberRepeatButtonState(player)
|
||||
|
||||
var queueHasFocus by remember { mutableStateOf(false) }
|
||||
val height by animateFloatAsState(
|
||||
if (queueHasFocus) {
|
||||
1f
|
||||
} else {
|
||||
.5f
|
||||
},
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
)
|
||||
val listState = rememberLazyListState()
|
||||
val hideButtons by remember {
|
||||
derivedStateOf {
|
||||
listState.firstVisibleItemIndex > 0
|
||||
}
|
||||
}
|
||||
|
||||
val firstFocusRequester = remember { FocusRequester() }
|
||||
BackHandler(hideButtons) {
|
||||
scope.launch {
|
||||
listState.animateScrollToItem(0)
|
||||
firstFocusRequester.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxHeight(height),
|
||||
) {
|
||||
current?.title?.let {
|
||||
Text(it)
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
current?.albumTitle?.let {
|
||||
Text(it)
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
current?.artistNames?.let {
|
||||
Text(it)
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
SeekBar(
|
||||
player = player,
|
||||
|
|
@ -69,47 +135,97 @@ fun NowPlayingOverlay(
|
|||
seekForward = Duration.ZERO,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth(.95f),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
AnimatedVisibility(
|
||||
visible = !hideButtons,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically(),
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
PlaybackButtons(
|
||||
player = player,
|
||||
initialFocusRequester = focusRequester,
|
||||
onControllerInteraction = { controllerViewState.pulseControls() },
|
||||
onPlaybackActionClick = {
|
||||
when (it) {
|
||||
PlaybackAction.Next -> {
|
||||
nextState.onClick()
|
||||
}
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
PlaybackButtons(
|
||||
player = player,
|
||||
initialFocusRequester = focusRequester,
|
||||
onControllerInteraction = { controllerViewState.pulseControls() },
|
||||
onPlaybackActionClick = {
|
||||
when (it) {
|
||||
PlaybackAction.Next -> {
|
||||
nextState.onClick()
|
||||
}
|
||||
|
||||
PlaybackAction.Previous -> {
|
||||
previousState.onClick()
|
||||
}
|
||||
PlaybackAction.Previous -> {
|
||||
previousState.onClick()
|
||||
}
|
||||
|
||||
is PlaybackAction.ToggleCaptions -> {
|
||||
TODO()
|
||||
}
|
||||
is PlaybackAction.ToggleCaptions -> {
|
||||
TODO()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
showPlay = playPauseState.showPlay,
|
||||
previousEnabled = previousState.isEnabled,
|
||||
nextEnabled = nextState.isEnabled,
|
||||
seekBack = 10.seconds,
|
||||
skipBackOnResume = null,
|
||||
seekForward = 30.seconds,
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
showPlay = playPauseState.showPlay,
|
||||
previousEnabled = previousState.isEnabled,
|
||||
nextEnabled = nextState.isEnabled,
|
||||
seekBack = 10.seconds,
|
||||
skipBackOnResume = null,
|
||||
seekForward = 30.seconds, // TODO
|
||||
)
|
||||
PlaybackFaButton(
|
||||
iconRes = R.string.fa_shuffle,
|
||||
onClick = {
|
||||
shuffleState.onClick()
|
||||
},
|
||||
onControllerInteraction = { controllerViewState.pulseControls() },
|
||||
textColor =
|
||||
if (shuffleState.shuffleOn) {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
} else {
|
||||
Color.Unspecified
|
||||
},
|
||||
)
|
||||
PlaybackFaButton(
|
||||
iconRes = R.string.fa_repeat,
|
||||
onClick = {
|
||||
repeatState.onClick()
|
||||
},
|
||||
onControllerInteraction = { controllerViewState.pulseControls() },
|
||||
textColor =
|
||||
when (repeatState.repeatModeState) {
|
||||
Player.REPEAT_MODE_ALL -> MaterialTheme.colorScheme.secondary
|
||||
|
||||
// TODO
|
||||
Player.REPEAT_MODE_ONE -> MaterialTheme.colorScheme.tertiary
|
||||
|
||||
else -> Color.Unspecified
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (queue.isEmpty()) {
|
||||
Text("No items")
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.queue),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(bottom = 16.dp, start = 16.dp, end = 16.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.onFocusChanged {
|
||||
queueHasFocus = it.hasFocus
|
||||
},
|
||||
) {
|
||||
itemsIndexed(queue) { index, song ->
|
||||
SongListItem(
|
||||
|
|
@ -123,7 +239,11 @@ fun NowPlayingOverlay(
|
|||
player.seekTo(index, 0L)
|
||||
},
|
||||
onLongClick = {},
|
||||
modifier = Modifier,
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
index == 0,
|
||||
Modifier.focusRequester(firstFocusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.annotation.OptIn
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -31,6 +32,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.preferences.skipBackOnResume
|
||||
import com.github.damontecres.wholphin.services.rememberQueue
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
|
@ -127,14 +129,22 @@ fun NowPlayingPage(
|
|||
BackHandler(controllerViewState.controlsVisible) {
|
||||
controllerViewState.hideControls()
|
||||
}
|
||||
AnimatedVisibility(controllerViewState.controlsVisible) {
|
||||
AnimatedVisibility(
|
||||
controllerViewState.controlsVisible,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter),
|
||||
) {
|
||||
NowPlayingOverlay(
|
||||
state = state,
|
||||
player = player,
|
||||
current = current,
|
||||
queue = queue,
|
||||
controllerViewState = controllerViewState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier =
|
||||
Modifier
|
||||
.background(AppColors.TransparentBlack50)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,8 +87,12 @@ class NowPlayingViewModel
|
|||
|
||||
private suspend fun getCurrent(): AudioItem? {
|
||||
val mediaItem =
|
||||
onMain { player.currentMediaItemIndex.let { player.getMediaItemAt(it) } }
|
||||
return mediaItem.localConfiguration?.tag as? AudioItem
|
||||
onMain {
|
||||
player.currentMediaItemIndex
|
||||
.takeIf { it >= 0 }
|
||||
?.let { player.getMediaItemAt(it) }
|
||||
}
|
||||
return mediaItem?.localConfiguration?.tag as? AudioItem
|
||||
}
|
||||
|
||||
private fun playbackLoop() {
|
||||
|
|
@ -125,7 +129,7 @@ class NowPlayingViewModel
|
|||
}
|
||||
}
|
||||
|
||||
delay(250)
|
||||
delay(150)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ package com.github.damontecres.wholphin.ui.playback
|
|||
import android.view.Gravity
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -24,7 +24,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -37,14 +36,18 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
|
|
@ -60,6 +63,7 @@ import androidx.tv.material3.surfaceColorAtElevation
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.preferences.AppThemeColors
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.components.Button
|
||||
import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent
|
||||
|
|
@ -462,6 +466,54 @@ fun PlaybackButton(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaybackFaButton(
|
||||
@StringRes iconRes: Int,
|
||||
onClick: () -> Unit,
|
||||
onControllerInteraction: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
textColor: Color = Color.Unspecified,
|
||||
) {
|
||||
val selectedColor = MaterialTheme.colorScheme.border
|
||||
Button(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
// shape = ButtonDefaults.shape(CircleShape),
|
||||
colors =
|
||||
ClickableSurfaceDefaults.colors(
|
||||
containerColor = AppColors.TransparentBlack25,
|
||||
focusedContainerColor = selectedColor,
|
||||
),
|
||||
contentPadding = PaddingValues(4.dp),
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
modifier
|
||||
.size(36.dp, 36.dp)
|
||||
.onFocusChanged { onControllerInteraction.invoke() },
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(iconRes),
|
||||
fontSize = 18.sp,
|
||||
fontFamily = FontAwesome,
|
||||
textAlign = TextAlign.Center,
|
||||
color =
|
||||
if (textColor.isSpecified) {
|
||||
textColor
|
||||
} else if (LocalTheme.current == AppThemeColors.OLED_BLACK) {
|
||||
LocalContentColor.current
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterVertically),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> BottomDialog(
|
||||
choices: List<BottomDialogItem<T>>,
|
||||
|
|
@ -545,12 +597,18 @@ data class BottomDialogItem<T>(
|
|||
@Composable
|
||||
private fun ButtonPreview() {
|
||||
WholphinTheme {
|
||||
Row {
|
||||
Row(Modifier.background(Color.Red)) {
|
||||
PlaybackButton(
|
||||
iconRes = R.drawable.baseline_play_arrow_24,
|
||||
onClick = {},
|
||||
onControllerInteraction = {},
|
||||
)
|
||||
PlaybackFaButton(
|
||||
iconRes = R.string.fa_shuffle,
|
||||
onClick = {},
|
||||
onControllerInteraction = {},
|
||||
textColor = Color.Green,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,4 +54,5 @@
|
|||
<string name="fa_cloud_arrow_up" translatable="false"></string>
|
||||
<string name="fa_cloud_arrow_down" translatable="false"></string>
|
||||
<string name="fa_compass" translatable="false"></string>
|
||||
<string name="fa_repeat" translatable="false"></string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue