Compare commits

..

1 Commits

Author SHA1 Message Date
Kavish Devar 6c33970370 android: add material 3 expressive theme
All of the new UI was written by hand!
also fixed upgraded being set to false in FOSS builds when opening App settings. should've been a different commit, but ¯\_(ツ)_/¯
2026-06-20 20:06:24 +05:30
16 changed files with 106 additions and 143 deletions
+9 -17
View File
@@ -1,7 +1,7 @@
> [!WARNING] >[!IMPORTANT]
> librepods.org is not an official website of the LibrePods project. It inaccurately claims to be the official website of the project by claiming copyrights and using the LibrePods logo in the footer. And at the same time, they say that the project is not affiliated with the LibrePods project or its developers. Development paused due to lack of time until June 2026 (JEE Advanced). PRs and issues might not be responded to until then.
>
> Please report any other such websites to [me@kavish.xyz](mailto:me@kavish.xyz) ---
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="./imgs/banner-dark.png" /> <source media="(prefers-color-scheme: dark)" srcset="./imgs/banner-dark.png" />
@@ -224,11 +224,11 @@ A huge thank you to everyone supporting the project!
# Star History # Star History
<a href="https://www.star-history.com/?repos=librepods-org%2Flibrepods&type=date&legend=top-left"> <a href="https://www.star-history.com/#kavishdevar/librepods&type=date&legend=top-left">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=librepods-org/librepods&type=date&theme=dark&legend=top-left&sealed_token=CS9QF2Wcx-_EN6M7lBhwvTa2WBd5eB8FbQa5FWt3SV14v1OcQdy2aCGsXxHoytVaoylP_pTxj1h2U-odoXDV_EwTqMxM-MQwG_WpvV_1g_yH-Jh3ux2BuWCmS98LxLqfox2ibLOlFDHu_-geTLFsTmqTc1SjX6-NYxVHXF5DaqnxFOAyjLPL5_WtXVew" /> <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kavishdevar/librepods&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=librepods-org/librepods&type=date&legend=top-left&sealed_token=CS9QF2Wcx-_EN6M7lBhwvTa2WBd5eB8FbQa5FWt3SV14v1OcQdy2aCGsXxHoytVaoylP_pTxj1h2U-odoXDV_EwTqMxM-MQwG_WpvV_1g_yH-Jh3ux2BuWCmS98LxLqfox2ibLOlFDHu_-geTLFsTmqTc1SjX6-NYxVHXF5DaqnxFOAyjLPL5_WtXVew" /> <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kavishdevar/librepods&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=librepods-org/librepods&type=date&legend=top-left&sealed_token=CS9QF2Wcx-_EN6M7lBhwvTa2WBd5eB8FbQa5FWt3SV14v1OcQdy2aCGsXxHoytVaoylP_pTxj1h2U-odoXDV_EwTqMxM-MQwG_WpvV_1g_yH-Jh3ux2BuWCmS98LxLqfox2ibLOlFDHu_-geTLFsTmqTc1SjX6-NYxVHXF5DaqnxFOAyjLPL5_WtXVew" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=kavishdevar/librepods&type=date&legend=top-left" />
</picture> </picture>
</a> </a>
@@ -250,12 +250,4 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
# Trademark Notice All trademarks, logos, and brand names are the property of their respective owners. Use of them does not imply any affiliation with or endorsement by them. All AirPods images, symbols, and the SF Pro font are the property of Apple Inc.
The GPL does not grant any rights to use the LibrePods name, logo, or branding. The LibrePods name and logo may not be used for software, websites, domains, products, services, or other projects in a manner that suggests affiliation with, endorsement by, or association with the official LibrePods project without prior permission.
If you see any misuse of the LibrePods name or logo, please report it to [me@kavish.xyz](mailto:me@kavish.xyz).
The SF Pro font used in the Android app is the property of Apple Inc.. This will be removed in future versions of the app and replaced with an open alternative soon.
AirPods, AirPods Pro, AirPods Max, and the AirPods logo are trademarks of Apple Inc. The LibrePods project is not affiliated with or endorsed by Apple Inc. in any way.
+2 -2
View File
@@ -1,6 +1,6 @@
import java.util.Properties import java.util.Properties
val appVersionName = "1.0.0-rc2" val appVersionName = "v1.0.0-rc1"
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
@@ -49,7 +49,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "me.kavishdevar.librepods" applicationId = "me.kavishdevar.librepods"
targetSdk = 37 targetSdk = 37
versionCode = 63 versionCode = 61
versionName = appVersionName versionName = appVersionName
} }
buildTypes { buildTypes {
@@ -181,7 +181,7 @@ fun Main() {
val onboardingComplete = sharedPreferences.getBoolean("onboarding_complete", false) val onboardingComplete = sharedPreferences.getBoolean("onboarding_complete", false)
val releaseNotesShownPrefKey = "release_notes_shown_${BuildConfig.VERSION_NAME.removeSuffix("-debug").removeSuffix("-play")}" val releaseNotesShownPrefKey = "release_notes_shown_${BuildConfig.VERSION_NAME.removeSuffix("-debug")}"
val releaseNotesShown = sharedPreferences.getBoolean(releaseNotesShownPrefKey, false) val releaseNotesShown = sharedPreferences.getBoolean(releaseNotesShownPrefKey, false)
fun bindService() { fun bindService() {
@@ -8,7 +8,7 @@ import me.kavishdevar.librepods.presentation.screens.EqualizerScreenPreviewMater
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
val update1_0_0 = listOf( val update0_3_1 = listOf(
UpdateItem( UpdateItem(
titleRes = R.string.material3e, titleRes = R.string.material3e,
descriptionRes = R.string.update_m3e_description, descriptionRes = R.string.update_m3e_description,
@@ -32,4 +32,4 @@ val update1_0_0 = listOf(
), ),
) )
val updates = update1_0_0 val updates = update0_3_1
@@ -46,8 +46,8 @@ fun AboutCard(
DesignSystem.Material -> listOf( DesignSystem.Material -> listOf(
serialNumbers[0], serialNumbers[0],
stringResource(R.string.left) + " " + serialNumbers[1], stringResource(R.string.left) + " " + {serialNumbers[1]},
stringResource(R.string.right) + " " + serialNumbers[2], stringResource(R.string.right) + " " + {serialNumbers[2]},
) )
} }
@@ -131,6 +131,7 @@ fun NoiseControlSettings(
Column { Column {
Box( Box(
modifier = Modifier modifier = Modifier
.background(Color.Transparent)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(top = 4.dp, bottom = 12.dp) .padding(top = 4.dp, bottom = 12.dp)
) { ) {
@@ -736,116 +736,96 @@ fun AirPodsSettingsScreen(
val animatedShapeColor by animateColorAsState(if (reconnecting) primaryContainerColor else secondaryContainerColor) val animatedShapeColor by animateColorAsState(if (reconnecting) primaryContainerColor else secondaryContainerColor)
if (state.connectionSuccessful) { Box(
Box( modifier = Modifier
modifier = Modifier .size(240.dp)
.size(240.dp) .background(
.background( MaterialTheme.colorScheme.surfaceBright,
MaterialTheme.colorScheme.surfaceBright, CircleShape
CircleShape )
) .clickable(
.clickable( interactionSource = null,
interactionSource = null, indication = ripple(
indication = ripple( bounded = false,
bounded = false, radius = 120.dp
radius = 120.dp ),
), enabled = !reconnecting,
enabled = !reconnecting, onClick = {}
onClick = {} )
) .pointerInput(Unit) {
.pointerInput(Unit) { detectTapGestures(
detectTapGestures( onTap = {
onTap = { if (!reconnecting) {
if (!reconnecting) { currentMorphIndex = 1
currentMorphIndex = 1 reconnecting = true
reconnecting = true reconnectFromSavedMac()
reconnectFromSavedMac()
}
},
onPress = {
if (!reconnecting) {
morphProgress.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
tryAwaitRelease()
morphProgress.animateTo(
targetValue = 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
}
} }
) },
} onPress = {
.drawWithContent { if (!reconnecting) {
val activeMorph = morphs[currentMorphIndex] morphProgress.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
tryAwaitRelease()
morphProgress.animateTo(
targetValue = 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
}
}
)
}
.drawWithContent {
val activeMorph = morphs[currentMorphIndex]
val shapePath = activeMorph.toPath( val shapePath = activeMorph.toPath(
progress = morphProgress.value, progress = morphProgress.value,
path = path path = path
) )
val bounds = shapePath.getBounds() val bounds = shapePath.getBounds()
val scale = min( val scale = min(size.width/bounds.width, size.height/bounds.height) * 0.8f
size.width / bounds.width,
size.height / bounds.height
) * 0.8f
scaleMatrix.reset() scaleMatrix.reset()
scaleMatrix.scale(x = scale, y = scale) scaleMatrix.scale(x = scale, y = scale)
shapePath.transform(scaleMatrix) shapePath.transform(scaleMatrix)
shapePath.translate(size.center - shapePath.getBounds().center) shapePath.translate(size.center - shapePath.getBounds().center)
drawPath( drawPath(
path = shapePath, path = shapePath,
color = animatedShapeColor color = animatedShapeColor
) )
drawContent() drawContent()
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = if (reconnecting) MaterialIcons.bluetooth_searching else MaterialIcons.headset_off, imageVector = if (reconnecting) MaterialIcons.bluetooth_searching else MaterialIcons.headset_off,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(84.dp), modifier = Modifier.size(84.dp),
tint = if (reconnecting) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSecondaryContainer tint = if (reconnecting) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSecondaryContainer
)
}
Spacer(Modifier.height(40.dp))
Text(
text = if (reconnecting) stringResource(R.string.reconnecting) else stringResource(R.string.tap_to_reconnect),
style = MaterialTheme.typography.labelSmallEmphasized,
color = MaterialTheme.colorScheme.primary
) )
} else {
Text(
text = stringResource(R.string.airpods_not_connected),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary
)
if (sharedPreferences.getBoolean("bypass_device_check.v2", false)) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.compatibility_check_bypassed),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
}
} }
Spacer(Modifier.height(40.dp))
Text(
text = if (reconnecting) stringResource(R.string.reconnecting) else stringResource(R.string.tap_to_reconnect),
style = MaterialTheme.typography.labelSmallEmphasized,
color = MaterialTheme.colorScheme.primary
)
} }
if (!BuildConfig.PLAY_BUILD) { if (!BuildConfig.PLAY_BUILD) {
@@ -190,7 +190,7 @@ fun AppSettingsScreen(
label = stringResource(R.string.use_material3e), label = stringResource(R.string.use_material3e),
checked = state.m3eEnabled, checked = state.m3eEnabled,
onCheckedChange = viewModel::setm3eEnabled, onCheckedChange = viewModel::setm3eEnabled,
enabled = state.isPremium // enabled = state.isPremium
) )
if (state.connectionSuccessful) { if (state.connectionSuccessful) {
@@ -78,7 +78,7 @@ import androidx.graphics.shapes.Morph
import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.data.updates.UpdateItem import me.kavishdevar.librepods.data.updates.UpdateItem
import me.kavishdevar.librepods.data.updates.updates import me.kavishdevar.librepods.data.updates.update0_3_1
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
@@ -116,7 +116,7 @@ fun ReleaseNotesScreen(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
val versionName = BuildConfig.VERSION_NAME.removeSuffix("-debug").removeSuffix("-play") val versionName = BuildConfig.VERSION_NAME.removeSuffix("-debug")
val url = "https://github.com/kavishdevar/librepods/releases/v$versionName" val url = "https://github.com/kavishdevar/librepods/releases/v$versionName"
val fullText = "${stringResource(R.string.version)} $versionName" val fullText = "${stringResource(R.string.version)} $versionName"
val textColor = MaterialTheme.colorScheme.primary val textColor = MaterialTheme.colorScheme.primary
@@ -375,7 +375,7 @@ fun ReleaseNotesScreenPreview() {
m3eEnabled = false m3eEnabled = false
) { ) {
ReleaseNotesScreen( ReleaseNotesScreen(
updates = updates, updates = update0_3_1,
releaseNotesShown = { } releaseNotesShown = { }
) )
} }
@@ -47,7 +47,6 @@ import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
import me.kavishdevar.librepods.utils.XposedState import me.kavishdevar.librepods.utils.XposedState
import me.kavishdevar.librepods.utils.bypassDeviceCheck import me.kavishdevar.librepods.utils.bypassDeviceCheck
import me.kavishdevar.librepods.utils.isSupported import me.kavishdevar.librepods.utils.isSupported
import me.kavishdevar.librepods.utils.removeDeviceCheckBypass
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -192,9 +191,6 @@ fun OnboardingScreen(
NotSupportedPage( NotSupportedPage(
bypassCompatibilityCheck = { bypassCompatibilityCheck = {
bypassDeviceCheck(sharedPreferences) bypassDeviceCheck(sharedPreferences)
animationScope.launch {
state.animateScrollToItem(3)
}
} }
) )
} }
@@ -203,9 +199,6 @@ fun OnboardingScreen(
onBackward = { onBackward = {
animationScope.launch { animationScope.launch {
if (state.canScrollBackward) state.animateScrollToItem(if (isSupported) 1 else 2) if (state.canScrollBackward) state.animateScrollToItem(if (isSupported) 1 else 2)
if (!isSupported) {
removeDeviceCheckBypass(sharedPreferences)
}
} }
}, },
onForward = onOnboardingComplete onForward = onOnboardingComplete
@@ -105,7 +105,7 @@ data class AirPodsUiState(
) )
val demoInstance = AirPodsInstance( val demoInstance = AirPodsInstance(
name = "[DEMO] AirPods Pro", name = "AirPods Pro",
model = AirPodsModels.getModelByModelNumber("A3064")!!, model = AirPodsModels.getModelByModelNumber("A3064")!!,
actualModelNumber = "A3064", actualModelNumber = "A3064",
serialNumber = "JXF9Q94A40", serialNumber = "JXF9Q94A40",
@@ -160,6 +160,8 @@ val demoState = AirPodsUiState(
dynamicEndOfCharge = true, dynamicEndOfCharge = true,
connectionSuccessful = true,
customEq = CustomEq(state = 2, low = 65, mid = 50, high = 70), customEq = CustomEq(state = 2, low = 65, mid = 50, high = 70),
controlStates = mapOf( controlStates = mapOf(
@@ -42,7 +42,3 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean {
fun bypassDeviceCheck(sharedPreferences: SharedPreferences) { fun bypassDeviceCheck(sharedPreferences: SharedPreferences) {
sharedPreferences.edit{ putBoolean("bypass_device_check.v2", true) } sharedPreferences.edit{ putBoolean("bypass_device_check.v2", true) }
} }
fun removeDeviceCheckBypass(sharedPreferences: SharedPreferences) {
sharedPreferences.edit{ putBoolean("bypass_device_check.v2", false) }
}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_monochrome" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" /> <monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>
@@ -290,5 +290,4 @@
<string name="permissions">Permissions</string> <string name="permissions">Permissions</string>
<string name="privacy_policy">Privacy Policy</string> <string name="privacy_policy">Privacy Policy</string>
<string name="i_agree">I agree</string> <string name="i_agree">I agree</string>
<string name="compatibility_check_bypassed">Compatibility check bypassed. Are you sure your device is compatible?</string>
</resources> </resources>
+1 -1
View File
@@ -1,6 +1,6 @@
[versions] [versions]
accompanistPermissions = "0.37.3" accompanistPermissions = "0.37.3"
agp = "9.2.1" agp = "9.1.1"
kotlin = "2.3.21" kotlin = "2.3.21"
coreKtx = "1.18.0" coreKtx = "1.18.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
+1 -1
View File
@@ -1,6 +1,6 @@
#Mon Oct 07 22:30:36 IST 2024 #Mon Oct 07 22:30:36 IST 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists