Skip to content

Commit e0412a4

Browse files
authored
Merge pull request #90 from sameerasw/develop
Develop - Disconnected toast and widget transparency
2 parents ddeeeed + 9f5a149 commit e0412a4

12 files changed

Lines changed: 229 additions & 5 deletions

File tree

app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ class DataStoreManager(private val context: Context) {
9292
private val PITCH_BLACK_THEME = booleanPreferencesKey("pitch_black_theme")
9393
private val SENTRY_REPORTING_ENABLED = booleanPreferencesKey("sentry_reporting_enabled")
9494

95+
// Widget preferences
96+
private val WIDGET_TRANSPARENCY = androidx.datastore.preferences.core.floatPreferencesKey("widget_transparency")
97+
9598
private const val NETWORK_DEVICES_PREFIX = "network_device_"
9699
private const val NETWORK_CONNECTIONS_PREFIX = "network_connections_"
97100

@@ -576,6 +579,18 @@ class DataStoreManager(private val context: Context) {
576579
}
577580
}
578581

582+
suspend fun setWidgetTransparency(alpha: Float) {
583+
context.dataStore.edit { preferences ->
584+
preferences[WIDGET_TRANSPARENCY] = alpha
585+
}
586+
}
587+
588+
fun getWidgetTransparency(): Flow<Float> {
589+
return context.dataStore.data.map { preferences ->
590+
preferences[WIDGET_TRANSPARENCY] ?: 1f
591+
}
592+
}
593+
579594
// Network-aware device connections
580595
suspend fun saveNetworkDeviceConnection(
581596
deviceName: String,

app/src/main/java/com/sameerasw/airsync/data/repository/AirSyncRepositoryImpl.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,14 @@ class AirSyncRepositoryImpl(
232232
return dataStoreManager.getSentryReportingEnabled()
233233
}
234234

235+
override suspend fun setWidgetTransparency(alpha: Float) {
236+
dataStoreManager.setWidgetTransparency(alpha)
237+
}
238+
239+
override fun getWidgetTransparency(): Flow<Float> {
240+
return dataStoreManager.getWidgetTransparency()
241+
}
242+
235243
override suspend fun setEssentialsConnectionEnabled(enabled: Boolean) {
236244
dataStoreManager.setEssentialsConnectionEnabled(enabled)
237245
}

app/src/main/java/com/sameerasw/airsync/domain/model/UiState.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@ data class UiState(
4747
val isPitchBlackThemeEnabled: Boolean = false,
4848
val isBlurEnabled: Boolean = true,
4949
val isSentryReportingEnabled: Boolean = true,
50-
val isOnboardingCompleted: Boolean = true
50+
val isOnboardingCompleted: Boolean = true,
51+
val widgetTransparency: Float = 1f
5152
)

app/src/main/java/com/sameerasw/airsync/domain/repository/AirSyncRepository.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ interface AirSyncRepository {
105105
suspend fun setSentryReportingEnabled(enabled: Boolean)
106106
fun getSentryReportingEnabled(): Flow<Boolean>
107107

108+
// Widget specific settings
109+
suspend fun setWidgetTransparency(alpha: Float)
110+
fun getWidgetTransparency(): Flow<Float>
111+
108112
// Essentials Bridge
109113
suspend fun setEssentialsConnectionEnabled(enabled: Boolean)
110114
fun getEssentialsConnectionEnabled(): Flow<Boolean>

app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,18 @@ fun SettingsView(
327327
}
328328
}
329329

330+
// Widget Section
331+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
332+
SettingsCategoryTitle("Widget")
333+
RoundedCardContainer {
334+
com.sameerasw.airsync.presentation.ui.components.sliders.ConfigSliderItem(
335+
title = "Widget Transparency",
336+
value = uiState.widgetTransparency,
337+
onValueChange = { viewModel.setWidgetTransparency(it) }
338+
)
339+
}
340+
}
341+
330342
// Connection Section
331343
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
332344
SettingsCategoryTitle("Connection")
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.sameerasw.airsync.presentation.ui.components.sliders
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.shape.RoundedCornerShape
9+
import androidx.compose.material3.Icon
10+
import androidx.compose.material3.IconButton
11+
import androidx.compose.material3.MaterialTheme
12+
import androidx.compose.material3.Slider
13+
import androidx.compose.material3.Text
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.ui.Alignment
16+
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.platform.LocalView
18+
import androidx.compose.ui.res.painterResource
19+
import androidx.compose.ui.unit.dp
20+
import androidx.compose.runtime.LaunchedEffect
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableFloatStateOf
23+
import androidx.compose.runtime.remember
24+
import androidx.compose.runtime.setValue
25+
import androidx.compose.foundation.layout.Spacer
26+
import androidx.compose.foundation.layout.height
27+
import androidx.compose.ui.platform.LocalHapticFeedback
28+
import com.sameerasw.airsync.R
29+
import com.sameerasw.airsync.utils.HapticUtil
30+
import java.math.BigDecimal
31+
import java.math.RoundingMode
32+
33+
@Composable
34+
fun ConfigSliderItem(
35+
title: String,
36+
value: Float,
37+
onValueChange: (Float) -> Unit,
38+
modifier: Modifier = Modifier,
39+
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
40+
steps: Int = 0,
41+
increment: Float = 0.1f,
42+
onValueChangeFinished: (() -> Unit)? = null,
43+
enabled: Boolean = true
44+
) {
45+
46+
val haptics = LocalHapticFeedback.current
47+
Column(
48+
modifier = modifier
49+
.fillMaxWidth()
50+
.background(
51+
color = MaterialTheme.colorScheme.surfaceBright,
52+
shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd)
53+
)
54+
.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 8.dp)
55+
) {
56+
var sliderValue by remember(value) { mutableFloatStateOf(value) }
57+
val view = LocalView.current
58+
59+
Text(
60+
text = title,
61+
style = MaterialTheme.typography.bodyMedium,
62+
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
63+
)
64+
65+
Spacer(modifier = Modifier.height(8.dp))
66+
67+
Row(
68+
modifier = Modifier.fillMaxWidth(),
69+
verticalAlignment = Alignment.CenterVertically
70+
) {
71+
IconButton(
72+
onClick = {
73+
val newValue = (BigDecimal.valueOf(sliderValue.toDouble())
74+
.subtract(BigDecimal.valueOf(increment.toDouble()))
75+
.setScale(2, RoundingMode.HALF_UP))
76+
.toFloat()
77+
val clamped = newValue.coerceIn(valueRange)
78+
sliderValue = clamped
79+
onValueChange(clamped)
80+
onValueChangeFinished?.invoke()
81+
HapticUtil.performClick(haptics)
82+
},
83+
modifier = Modifier.padding(end = 4.dp),
84+
enabled = enabled
85+
) {
86+
Icon(
87+
painter = painterResource(id = R.drawable.rounded_remove_24),
88+
contentDescription = "Decrease",
89+
tint = MaterialTheme.colorScheme.primary
90+
)
91+
}
92+
93+
Slider(
94+
value = sliderValue,
95+
onValueChange = {
96+
sliderValue = it
97+
HapticUtil.performLightTick(haptics)
98+
},
99+
valueRange = valueRange,
100+
steps = steps,
101+
onValueChangeFinished = {
102+
onValueChange(sliderValue)
103+
onValueChangeFinished?.invoke()
104+
},
105+
modifier = Modifier.weight(1f),
106+
enabled = enabled
107+
)
108+
109+
IconButton(
110+
onClick = {
111+
val newValue = (BigDecimal.valueOf(sliderValue.toDouble())
112+
.add(BigDecimal.valueOf(increment.toDouble()))
113+
.setScale(2, RoundingMode.HALF_UP))
114+
.toFloat()
115+
val clamped = newValue.coerceIn(valueRange)
116+
sliderValue = clamped
117+
onValueChange(clamped)
118+
onValueChangeFinished?.invoke()
119+
HapticUtil.performClick(haptics)
120+
},
121+
modifier = Modifier.padding(start = 4.dp),
122+
enabled = enabled
123+
) {
124+
Icon(
125+
painter = painterResource(id = R.drawable.rounded_add_24),
126+
contentDescription = "Increase",
127+
tint = MaterialTheme.colorScheme.primary
128+
)
129+
}
130+
}
131+
}
132+
}

app/src/main/java/com/sameerasw/airsync/presentation/ui/composables/WelcomeScreen.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ fun WelcomeScreen(
6767
onBeginClick: () -> Unit
6868
) {
6969
val haptics = LocalHapticFeedback.current
70-
val context = LocalContext.current
71-
val scope = rememberCoroutineScope()
7270
val uiState by viewModel.uiState.collectAsState()
7371

7472
var currentStep by remember { mutableStateOf(OnboardingStep.WELCOME) }

app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ class AirSyncViewModel(
153153
}
154154
}
155155

156+
// Observe widget transparency preference
157+
viewModelScope.launch {
158+
repository.getWidgetTransparency().collect { trans ->
159+
_uiState.value = _uiState.value.copy(widgetTransparency = trans)
160+
}
161+
}
162+
156163
// Observe first run preference for onboarding status
157164
viewModelScope.launch {
158165
repository.getFirstRun().collect { firstRun ->
@@ -600,6 +607,17 @@ class AirSyncViewModel(
600607
_uiState.value = _uiState.value.copy(isPitchBlackThemeEnabled = enabled)
601608
viewModelScope.launch {
602609
repository.setPitchBlackThemeEnabled(enabled)
610+
// Note: Currently theme changes check via MainActivity collection instead of restart
611+
}
612+
}
613+
614+
fun setWidgetTransparency(alpha: Float) {
615+
_uiState.value = _uiState.value.copy(widgetTransparency = alpha)
616+
viewModelScope.launch {
617+
repository.setWidgetTransparency(alpha)
618+
appContext?.let { context ->
619+
com.sameerasw.airsync.widget.AirSyncWidgetProvider.updateAllWidgets(context)
620+
}
603621
}
604622
}
605623

app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,12 @@ object WebSocketUtil {
343343
reason: String
344344
) {
345345
if (webSocket == WebSocketUtil.webSocket) {
346+
if (code != 1000) {
347+
CoroutineScope(Dispatchers.Main).launch {
348+
val msg = reason.ifEmpty { "Unknown Server Disconnect" }
349+
android.widget.Toast.makeText(context, "Disconnected: $msg", android.widget.Toast.LENGTH_SHORT).show()
350+
}
351+
}
346352
isConnected.set(false)
347353
isSocketOpen.set(false)
348354
isConnecting.set(false)
@@ -377,8 +383,22 @@ object WebSocketUtil {
377383
) {
378384
val totalToTry = ipList.size
379385
val failedCount = failedAttempts.incrementAndGet()
380-
381-
if (webSocket == WebSocketUtil.webSocket || (!connectionStarted.get() && failedCount >= totalToTry)) {
386+
val wasActive = webSocket == WebSocketUtil.webSocket
387+
val isFinalManualAttempt = manualAttempt && !connectionStarted.get() && failedCount >= totalToTry
388+
389+
if (wasActive || isFinalManualAttempt) {
390+
if (manualAttempt || isSocketOpen.get()) {
391+
CoroutineScope(Dispatchers.Main).launch {
392+
val msg = when (t) {
393+
is java.net.ConnectException -> "Connection Refused (Is AirSync Mac running?)"
394+
is java.net.SocketTimeoutException -> "Could not discover your mac"
395+
is java.net.UnknownHostException -> "Could not reach your mac"
396+
is java.io.EOFException, is java.net.SocketException -> "Lost connection to your mac"
397+
else -> t.message ?: "Unknown connection error"
398+
}
399+
android.widget.Toast.makeText(context, "AirSync: $msg", android.widget.Toast.LENGTH_LONG).show()
400+
}
401+
}
382402
isConnected.set(false)
383403
isConnecting.set(false)
384404
isSocketOpen.set(false)

app/src/main/java/com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ class AirSyncWidgetProvider : AppWidgetProvider() {
103103
val isConnected = WebSocketUtil.isConnected()
104104
val isConnecting = WebSocketUtil.isConnecting()
105105
val lastDevice = runBlocking { ds.getLastConnectedDevice().first() }
106+
val widgetAlpha = runBlocking { ds.getWidgetTransparency().first() }
107+
108+
// Apply background transparency
109+
val baseBg = androidx.core.content.ContextCompat.getColor(context, R.color.widget_background)
110+
val bgWithAlpha = androidx.core.graphics.ColorUtils.setAlphaComponent(baseBg, (widgetAlpha * 255).toInt().coerceIn(0, 255))
111+
views.setInt(R.id.widget_container, "setBackgroundColor", bgWithAlpha)
106112

107113
// Device image (large preview) and name
108114
val previewRes = DevicePreviewResolver.getPreviewRes(lastDevice)

0 commit comments

Comments
 (0)