feat: Add options in the settings menu to disable alarm and vibration
#36
This commit is contained in:
@@ -13,8 +13,8 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Interface for reading/writing app preferences to the app's database. This style of storage aims
|
||||
* to mimic the Preferences DataStore library, preventing the requirement of migration if the
|
||||
* database schema changes.
|
||||
* to mimic the Preferences DataStore library, preventing the requirement of migration if new
|
||||
* preferences are added
|
||||
*/
|
||||
interface PreferenceRepository {
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,8 @@ interface TimerRepository {
|
||||
var longBreakTime: Long
|
||||
var sessionLength: Int
|
||||
var timerFrequency: Float
|
||||
var alarmEnabled: Boolean
|
||||
var vibrateEnabled: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,4 +30,6 @@ class AppTimerRepository : TimerRepository {
|
||||
override var longBreakTime = 15 * 60 * 1000L
|
||||
override var sessionLength = 4
|
||||
override var timerFrequency: Float = 10f
|
||||
override var alarmEnabled = true
|
||||
override var vibrateEnabled = true
|
||||
}
|
||||
@@ -344,23 +344,28 @@ class TimerService : Service() {
|
||||
}
|
||||
|
||||
fun startAlarm() {
|
||||
alarm.start()
|
||||
if (timerRepository.alarmEnabled) alarm.start()
|
||||
|
||||
if (!vibrator.hasVibrator()) {
|
||||
return
|
||||
if (timerRepository.vibrateEnabled) {
|
||||
if (!vibrator.hasVibrator()) {
|
||||
return
|
||||
}
|
||||
val vibrationPattern = longArrayOf(0, 1000, 1000, 1000)
|
||||
val repeat = 2
|
||||
val effect = VibrationEffect.createWaveform(vibrationPattern, repeat)
|
||||
vibrator.vibrate(effect)
|
||||
}
|
||||
|
||||
val vibrationPattern = longArrayOf(0, 1000, 1000, 1000)
|
||||
val repeat = 2
|
||||
|
||||
val effect = VibrationEffect.createWaveform(vibrationPattern, repeat)
|
||||
vibrator.vibrate(effect)
|
||||
}
|
||||
|
||||
fun stopAlarm() {
|
||||
alarm.pause()
|
||||
alarm.seekTo(0)
|
||||
vibrator.cancel()
|
||||
if (timerRepository.alarmEnabled) {
|
||||
alarm.pause()
|
||||
alarm.seekTo(0)
|
||||
}
|
||||
|
||||
if (timerRepository.vibrateEnabled) {
|
||||
vibrator.cancel()
|
||||
}
|
||||
|
||||
_timerState.update { currentState ->
|
||||
currentState.copy(alarmRinging = false)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package org.nsh07.pomodoro.ui.settingsScreen
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
@@ -18,8 +19,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
@@ -32,10 +35,11 @@ import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.MaterialTheme.shapes
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderState
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -56,12 +60,17 @@ import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -80,6 +89,9 @@ fun SettingsScreenRoot(
|
||||
viewModel.longBreakTimeTextFieldState
|
||||
}
|
||||
|
||||
val alarmEnabled = viewModel.alarmEnabled.collectAsStateWithLifecycle()
|
||||
val vibrateEnabled = viewModel.vibrateEnabled.collectAsStateWithLifecycle()
|
||||
|
||||
val sessionsSliderState = rememberSaveable(
|
||||
saver = SliderState.Saver(
|
||||
viewModel.sessionsSliderState.onValueChangeFinished,
|
||||
@@ -94,6 +106,10 @@ fun SettingsScreenRoot(
|
||||
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||
sessionsSliderState = sessionsSliderState,
|
||||
alarmEnabled = alarmEnabled.value,
|
||||
vibrateEnabled = vibrateEnabled.value,
|
||||
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
|
||||
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
@@ -105,9 +121,35 @@ private fun SettingsScreen(
|
||||
shortBreakTimeInputFieldState: TextFieldState,
|
||||
longBreakTimeInputFieldState: TextFieldState,
|
||||
sessionsSliderState: SliderState,
|
||||
alarmEnabled: Boolean,
|
||||
vibrateEnabled: Boolean,
|
||||
onAlarmEnabledChange: (Boolean) -> Unit,
|
||||
onVibrateEnabledChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
val switchColors = SwitchDefaults.colors(
|
||||
checkedIconColor = colorScheme.primary,
|
||||
)
|
||||
|
||||
val switchItems = remember(alarmEnabled, vibrateEnabled) {
|
||||
listOf(
|
||||
SettingsSwitchItem(
|
||||
checked = alarmEnabled,
|
||||
icon = R.drawable.alarm_on,
|
||||
label = "Alarm",
|
||||
description = "Ring your system alarm sound when a timer completes",
|
||||
onClick = onAlarmEnabledChange
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
checked = vibrateEnabled,
|
||||
icon = R.drawable.mobile_vibrate,
|
||||
label = "Vibrate",
|
||||
description = "Vibrate in a repeating pattern when a timer completes",
|
||||
onClick = onVibrateEnabledChange
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||
TopAppBar(
|
||||
@@ -155,10 +197,10 @@ private fun SettingsScreen(
|
||||
MinuteInputField(
|
||||
state = focusTimeInputFieldState,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = 16.dp,
|
||||
bottomStart = 16.dp,
|
||||
topEnd = 4.dp,
|
||||
bottomEnd = 4.dp
|
||||
topStart = topListItemShape.topStart,
|
||||
bottomStart = topListItemShape.topStart,
|
||||
topEnd = topListItemShape.bottomStart,
|
||||
bottomEnd = topListItemShape.bottomStart
|
||||
),
|
||||
imeAction = ImeAction.Next
|
||||
)
|
||||
@@ -174,7 +216,7 @@ private fun SettingsScreen(
|
||||
)
|
||||
MinuteInputField(
|
||||
state = shortBreakTimeInputFieldState,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
shape = RoundedCornerShape(middleListItemShape.topStart),
|
||||
imeAction = ImeAction.Next
|
||||
)
|
||||
}
|
||||
@@ -190,10 +232,10 @@ private fun SettingsScreen(
|
||||
MinuteInputField(
|
||||
state = longBreakTimeInputFieldState,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = 4.dp,
|
||||
bottomStart = 4.dp,
|
||||
topEnd = 16.dp,
|
||||
bottomEnd = 16.dp
|
||||
topStart = bottomListItemShape.topStart,
|
||||
bottomStart = bottomListItemShape.topStart,
|
||||
topEnd = bottomListItemShape.bottomStart,
|
||||
bottomEnd = bottomListItemShape.bottomStart
|
||||
),
|
||||
imeAction = ImeAction.Done
|
||||
)
|
||||
@@ -224,7 +266,48 @@ private fun SettingsScreen(
|
||||
}
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(shapes.large)
|
||||
modifier = Modifier.clip(cardShape)
|
||||
)
|
||||
}
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
itemsIndexed(switchItems) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(painterResource(item.icon), contentDescription = null)
|
||||
},
|
||||
headlineContent = { Text(item.label) },
|
||||
supportingContent = { Text(item.description) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = item.checked,
|
||||
onCheckedChange = { item.onClick(it) },
|
||||
thumbContent = {
|
||||
if (item.checked) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.check),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.clear),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = switchColors
|
||||
)
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier
|
||||
.clip(
|
||||
when (index) {
|
||||
0 -> topListItemShape
|
||||
switchItems.lastIndex -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
item {
|
||||
@@ -275,7 +358,19 @@ fun SettingsScreenPreview() {
|
||||
shortBreakTimeInputFieldState = rememberTextFieldState((5 * 60 * 1000).toString()),
|
||||
longBreakTimeInputFieldState = rememberTextFieldState((15 * 60 * 1000).toString()),
|
||||
sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f),
|
||||
alarmEnabled = true,
|
||||
vibrateEnabled = true,
|
||||
onAlarmEnabledChange = {},
|
||||
onVibrateEnabledChange = {},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class SettingsSwitchItem(
|
||||
val checked: Boolean,
|
||||
@DrawableRes val icon: Int,
|
||||
val label: String,
|
||||
val description: String,
|
||||
val onClick: (Boolean) -> Unit
|
||||
)
|
||||
|
||||
@@ -19,6 +19,9 @@ import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import org.nsh07.pomodoro.TomatoApplication
|
||||
@@ -44,6 +47,14 @@ class SettingsViewModel(
|
||||
onValueChangeFinished = ::updateSessionLength
|
||||
)
|
||||
|
||||
private val _alarmEnabled: MutableStateFlow<Boolean> =
|
||||
MutableStateFlow(timerRepository.alarmEnabled)
|
||||
val alarmEnabled: StateFlow<Boolean> = _alarmEnabled.asStateFlow()
|
||||
|
||||
private val _vibrateEnabled: MutableStateFlow<Boolean> =
|
||||
MutableStateFlow(timerRepository.alarmEnabled)
|
||||
val vibrateEnabled: StateFlow<Boolean> = _vibrateEnabled.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
snapshotFlow { focusTimeTextFieldState.text }
|
||||
@@ -92,6 +103,22 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun saveAlarmEnabled(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
timerRepository.alarmEnabled = preferenceRepository
|
||||
.saveIntPreference("alarm_enabled", if (enabled) 1 else 0) == 1
|
||||
_alarmEnabled.value = enabled
|
||||
}
|
||||
}
|
||||
|
||||
fun saveVibrateEnabled(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
timerRepository.vibrateEnabled = preferenceRepository
|
||||
.saveIntPreference("vibrate_enabled", if (enabled) 1 else 0) == 1
|
||||
_vibrateEnabled.value = enabled
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
|
||||
43
app/src/main/java/org/nsh07/pomodoro/ui/theme/Shape.kt
Normal file
43
app/src/main/java/org/nsh07/pomodoro/ui/theme/Shape.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.ui.theme
|
||||
|
||||
import androidx.compose.foundation.shape.CornerBasedShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialTheme.shapes
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
object TomatoShapeDefaults {
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
val topListItemShape: RoundedCornerShape
|
||||
@Composable get() =
|
||||
RoundedCornerShape(
|
||||
topStart = shapes.largeIncreased.topStart,
|
||||
topEnd = shapes.largeIncreased.topEnd,
|
||||
bottomStart = shapes.extraSmall.bottomStart,
|
||||
bottomEnd = shapes.extraSmall.bottomStart
|
||||
)
|
||||
|
||||
val middleListItemShape: RoundedCornerShape
|
||||
@Composable get() = RoundedCornerShape(shapes.extraSmall.topStart)
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
val bottomListItemShape: RoundedCornerShape
|
||||
@Composable get() =
|
||||
RoundedCornerShape(
|
||||
topStart = shapes.extraSmall.topStart,
|
||||
topEnd = shapes.extraSmall.topEnd,
|
||||
bottomStart = shapes.largeIncreased.bottomStart,
|
||||
bottomEnd = shapes.largeIncreased.bottomEnd
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
val cardShape: CornerBasedShape
|
||||
@Composable get() = shapes.largeIncreased
|
||||
}
|
||||
@@ -77,6 +77,18 @@ class TimerViewModel(
|
||||
timerRepository.sessionLength
|
||||
)
|
||||
|
||||
timerRepository.alarmEnabled = (preferenceRepository.getIntPreference("alarm_enabled")
|
||||
?: preferenceRepository.saveIntPreference(
|
||||
"alarm_enabled",
|
||||
1
|
||||
)) == 1
|
||||
timerRepository.vibrateEnabled =
|
||||
(preferenceRepository.getIntPreference("vibrate_enabled")
|
||||
?: preferenceRepository.saveIntPreference(
|
||||
"vibrate_enabled",
|
||||
1
|
||||
)) == 1
|
||||
|
||||
resetTimer()
|
||||
|
||||
var lastDate = statRepository.getLastDate()
|
||||
|
||||
16
app/src/main/res/drawable/alarm_on.xml
Normal file
16
app/src/main/res/drawable/alarm_on.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
~ Copyright (c) 2025 Nishant Mishra
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#e3e3e3"
|
||||
android:pathData="m438,548 l-57,-57q-12,-12 -28,-12t-28,12q-12,12 -12,28.5t12,28.5l85,86q12,12 28,12t28,-12l170,-170q12,-12 12,-28.5T636,407q-12,-12 -28.5,-12T579,407L438,548ZM480,880q-75,0 -140.5,-28.5t-114,-77q-48.5,-48.5 -77,-114T120,520q0,-75 28.5,-140.5t77,-114q48.5,-48.5 114,-77T480,160q75,0 140.5,28.5t114,77q48.5,48.5 77,114T840,520q0,75 -28.5,140.5t-77,114q-48.5,48.5 -114,77T480,880ZM82,292q-11,-11 -11,-28t11,-28l114,-114q11,-11 28,-11t28,11q11,11 11,28t-11,28L138,292q-11,11 -28,11t-28,-11ZM878,292q-11,11 -28,11t-28,-11L708,178q-11,-11 -11,-28t11,-28q11,-11 28,-11t28,11l114,114q11,11 11,28t-11,28Z" />
|
||||
</vector>
|
||||
16
app/src/main/res/drawable/check.xml
Normal file
16
app/src/main/res/drawable/check.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
~ Copyright (c) 2025 Nishant Mishra
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#e3e3e3"
|
||||
android:pathData="m382,606 l339,-339q12,-12 28,-12t28,12q12,12 12,28.5T777,324L410,692q-12,12 -28,12t-28,-12L182,520q-12,-12 -11.5,-28.5T183,463q12,-12 28.5,-12t28.5,12l142,143Z" />
|
||||
</vector>
|
||||
16
app/src/main/res/drawable/clear.xml
Normal file
16
app/src/main/res/drawable/clear.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
~ Copyright (c) 2025 Nishant Mishra
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#e3e3e3"
|
||||
android:pathData="M480,536 L284,732q-11,11 -28,11t-28,-11q-11,-11 -11,-28t11,-28l196,-196 -196,-196q-11,-11 -11,-28t11,-28q11,-11 28,-11t28,11l196,196 196,-196q11,-11 28,-11t28,11q11,11 11,28t-11,28L536,480l196,196q11,11 11,28t-11,28q-11,11 -28,11t-28,-11L480,536Z" />
|
||||
</vector>
|
||||
16
app/src/main/res/drawable/mobile_vibrate.xml
Normal file
16
app/src/main/res/drawable/mobile_vibrate.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
~ Copyright (c) 2025 Nishant Mishra
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#e3e3e3"
|
||||
android:pathData="M320,840q-33,0 -56.5,-23.5T240,760v-560q0,-33 23.5,-56.5T320,120h320q33,0 56.5,23.5T720,200v560q0,33 -23.5,56.5T640,840L320,840ZM480,320q17,0 28.5,-11.5T520,280q0,-17 -11.5,-28.5T480,240q-17,0 -28.5,11.5T440,280q0,17 11.5,28.5T480,320ZM0,560v-160q0,-17 11.5,-28.5T40,360q17,0 28.5,11.5T80,400v160q0,17 -11.5,28.5T40,600q-17,0 -28.5,-11.5T0,560ZM120,640v-320q0,-17 11.5,-28.5T160,280q17,0 28.5,11.5T200,320v320q0,17 -11.5,28.5T160,680q-17,0 -28.5,-11.5T120,640ZM880,560v-160q0,-17 11.5,-28.5T920,360q17,0 28.5,11.5T960,400v160q0,17 -11.5,28.5T920,600q-17,0 -28.5,-11.5T880,560ZM760,640v-320q0,-17 11.5,-28.5T800,280q17,0 28.5,11.5T840,320v320q0,17 -11.5,28.5T800,680q-17,0 -28.5,-11.5T760,640Z" />
|
||||
</vector>
|
||||
Reference in New Issue
Block a user