Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ class DataStoreManager(private val context: Context) {
private val PITCH_BLACK_THEME = booleanPreferencesKey("pitch_black_theme")
private val SENTRY_REPORTING_ENABLED = booleanPreferencesKey("sentry_reporting_enabled")

// Widget preferences
private val WIDGET_TRANSPARENCY = androidx.datastore.preferences.core.floatPreferencesKey("widget_transparency")

private const val NETWORK_DEVICES_PREFIX = "network_device_"
private const val NETWORK_CONNECTIONS_PREFIX = "network_connections_"

Expand Down Expand Up @@ -576,6 +579,18 @@ class DataStoreManager(private val context: Context) {
}
}

suspend fun setWidgetTransparency(alpha: Float) {
context.dataStore.edit { preferences ->
preferences[WIDGET_TRANSPARENCY] = alpha
}
}

fun getWidgetTransparency(): Flow<Float> {
return context.dataStore.data.map { preferences ->
preferences[WIDGET_TRANSPARENCY] ?: 1f
}
}

// Network-aware device connections
suspend fun saveNetworkDeviceConnection(
deviceName: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,14 @@ class AirSyncRepositoryImpl(
return dataStoreManager.getSentryReportingEnabled()
}

override suspend fun setWidgetTransparency(alpha: Float) {
dataStoreManager.setWidgetTransparency(alpha)
}

override fun getWidgetTransparency(): Flow<Float> {
return dataStoreManager.getWidgetTransparency()
}

override suspend fun setEssentialsConnectionEnabled(enabled: Boolean) {
dataStoreManager.setEssentialsConnectionEnabled(enabled)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ data class UiState(
val isPitchBlackThemeEnabled: Boolean = false,
val isBlurEnabled: Boolean = true,
val isSentryReportingEnabled: Boolean = true,
val isOnboardingCompleted: Boolean = true
val isOnboardingCompleted: Boolean = true,
val widgetTransparency: Float = 1f
)
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ interface AirSyncRepository {
suspend fun setSentryReportingEnabled(enabled: Boolean)
fun getSentryReportingEnabled(): Flow<Boolean>

// Widget specific settings
suspend fun setWidgetTransparency(alpha: Float)
fun getWidgetTransparency(): Flow<Float>

// Essentials Bridge
suspend fun setEssentialsConnectionEnabled(enabled: Boolean)
fun getEssentialsConnectionEnabled(): Flow<Boolean>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,18 @@ fun SettingsView(
}
}

// Widget Section
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SettingsCategoryTitle("Widget")
RoundedCardContainer {
com.sameerasw.airsync.presentation.ui.components.sliders.ConfigSliderItem(
title = "Widget Transparency",
value = uiState.widgetTransparency,
onValueChange = { viewModel.setWidgetTransparency(it) }
)
}
}

// Connection Section
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SettingsCategoryTitle("Connection")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.sameerasw.airsync.presentation.ui.components.sliders

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.ui.platform.LocalHapticFeedback
import com.sameerasw.airsync.R
import com.sameerasw.airsync.utils.HapticUtil
import java.math.BigDecimal
import java.math.RoundingMode

@Composable
fun ConfigSliderItem(
title: String,
value: Float,
onValueChange: (Float) -> Unit,
modifier: Modifier = Modifier,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
steps: Int = 0,
increment: Float = 0.1f,
onValueChangeFinished: (() -> Unit)? = null,
enabled: Boolean = true
) {

val haptics = LocalHapticFeedback.current
Column(
modifier = modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceBright,
shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd)
)
.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 8.dp)
) {
var sliderValue by remember(value) { mutableFloatStateOf(value) }
val view = LocalView.current

Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
)

Spacer(modifier = Modifier.height(8.dp))

Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = {
val newValue = (BigDecimal.valueOf(sliderValue.toDouble())
.subtract(BigDecimal.valueOf(increment.toDouble()))
.setScale(2, RoundingMode.HALF_UP))
.toFloat()
val clamped = newValue.coerceIn(valueRange)
sliderValue = clamped
onValueChange(clamped)
onValueChangeFinished?.invoke()
HapticUtil.performClick(haptics)
},
modifier = Modifier.padding(end = 4.dp),
enabled = enabled
) {
Icon(
painter = painterResource(id = R.drawable.rounded_remove_24),
contentDescription = "Decrease",
tint = MaterialTheme.colorScheme.primary
)
}

Slider(
value = sliderValue,
onValueChange = {
sliderValue = it
HapticUtil.performLightTick(haptics)
},
valueRange = valueRange,
steps = steps,
onValueChangeFinished = {
onValueChange(sliderValue)
onValueChangeFinished?.invoke()
},
modifier = Modifier.weight(1f),
enabled = enabled
)

IconButton(
onClick = {
val newValue = (BigDecimal.valueOf(sliderValue.toDouble())
.add(BigDecimal.valueOf(increment.toDouble()))
.setScale(2, RoundingMode.HALF_UP))
.toFloat()
val clamped = newValue.coerceIn(valueRange)
sliderValue = clamped
onValueChange(clamped)
onValueChangeFinished?.invoke()
HapticUtil.performClick(haptics)
},
modifier = Modifier.padding(start = 4.dp),
enabled = enabled
) {
Icon(
painter = painterResource(id = R.drawable.rounded_add_24),
contentDescription = "Increase",
tint = MaterialTheme.colorScheme.primary
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ fun WelcomeScreen(
onBeginClick: () -> Unit
) {
val haptics = LocalHapticFeedback.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
val uiState by viewModel.uiState.collectAsState()

var currentStep by remember { mutableStateOf(OnboardingStep.WELCOME) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ class AirSyncViewModel(
}
}

// Observe widget transparency preference
viewModelScope.launch {
repository.getWidgetTransparency().collect { trans ->
_uiState.value = _uiState.value.copy(widgetTransparency = trans)
}
}

// Observe first run preference for onboarding status
viewModelScope.launch {
repository.getFirstRun().collect { firstRun ->
Expand Down Expand Up @@ -600,6 +607,17 @@ class AirSyncViewModel(
_uiState.value = _uiState.value.copy(isPitchBlackThemeEnabled = enabled)
viewModelScope.launch {
repository.setPitchBlackThemeEnabled(enabled)
// Note: Currently theme changes check via MainActivity collection instead of restart
}
}

fun setWidgetTransparency(alpha: Float) {
_uiState.value = _uiState.value.copy(widgetTransparency = alpha)
viewModelScope.launch {
repository.setWidgetTransparency(alpha)
appContext?.let { context ->
com.sameerasw.airsync.widget.AirSyncWidgetProvider.updateAllWidgets(context)
}
}
}

Expand Down
24 changes: 22 additions & 2 deletions app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,12 @@ object WebSocketUtil {
reason: String
) {
if (webSocket == WebSocketUtil.webSocket) {
if (code != 1000) {
CoroutineScope(Dispatchers.Main).launch {
val msg = reason.ifEmpty { "Unknown Server Disconnect" }
android.widget.Toast.makeText(context, "Disconnected: $msg", android.widget.Toast.LENGTH_SHORT).show()
}
}
isConnected.set(false)
isSocketOpen.set(false)
isConnecting.set(false)
Expand Down Expand Up @@ -377,8 +383,22 @@ object WebSocketUtil {
) {
val totalToTry = ipList.size
val failedCount = failedAttempts.incrementAndGet()

if (webSocket == WebSocketUtil.webSocket || (!connectionStarted.get() && failedCount >= totalToTry)) {
val wasActive = webSocket == WebSocketUtil.webSocket
val isFinalManualAttempt = manualAttempt && !connectionStarted.get() && failedCount >= totalToTry

if (wasActive || isFinalManualAttempt) {
if (manualAttempt || isSocketOpen.get()) {
CoroutineScope(Dispatchers.Main).launch {
val msg = when (t) {
is java.net.ConnectException -> "Connection Refused (Is AirSync Mac running?)"
is java.net.SocketTimeoutException -> "Could not discover your mac"
is java.net.UnknownHostException -> "Could not reach your mac"
is java.io.EOFException, is java.net.SocketException -> "Lost connection to your mac"
else -> t.message ?: "Unknown connection error"
}
android.widget.Toast.makeText(context, "AirSync: $msg", android.widget.Toast.LENGTH_LONG).show()
}
}
isConnected.set(false)
isConnecting.set(false)
isSocketOpen.set(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ class AirSyncWidgetProvider : AppWidgetProvider() {
val isConnected = WebSocketUtil.isConnected()
val isConnecting = WebSocketUtil.isConnecting()
val lastDevice = runBlocking { ds.getLastConnectedDevice().first() }
val widgetAlpha = runBlocking { ds.getWidgetTransparency().first() }

// Apply background transparency
val baseBg = androidx.core.content.ContextCompat.getColor(context, R.color.widget_background)
val bgWithAlpha = androidx.core.graphics.ColorUtils.setAlphaComponent(baseBg, (widgetAlpha * 255).toInt().coerceIn(0, 255))
views.setInt(R.id.widget_container, "setBackgroundColor", bgWithAlpha)

// Device image (large preview) and name
val previewRes = DevicePreviewResolver.getPreviewRes(lastDevice)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/rounded_add_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M440,520L240,520Q223,520 211.5,508.5Q200,497 200,480Q200,463 211.5,451.5Q223,440 240,440L440,440L440,240Q440,223 451.5,211.5Q463,200 480,200Q497,200 508.5,211.5Q520,223 520,240L520,440L720,440Q737,440 748.5,451.5Q760,463 760,480Q760,497 748.5,508.5Q737,520 720,520L520,520L520,720Q520,737 508.5,748.5Q497,760 480,760Q463,760 451.5,748.5Q440,737 440,720L440,520Z"/>

</vector>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/rounded_remove_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M240,520Q223,520 211.5,508.5Q200,497 200,480Q200,463 211.5,451.5Q223,440 240,440L720,440Q737,440 748.5,451.5Q760,463 760,480Q760,497 748.5,508.5Q737,520 720,520L240,520Z"/>

</vector>