new meta, holy butts we've got ourselves some Jetpack Compose

This commit is contained in:
Luke Wass 2025-05-12 22:22:13 -05:00
parent 656c7cdbea
commit 8040d22fca
3 changed files with 248 additions and 124 deletions

View file

@ -88,27 +88,22 @@ dependencies {
implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.gridlayout) implementation(libs.androidx.gridlayout)
// implementation(libs.androidx.lifecycle.runtime.ktx)
// implementation(libs.androidx.material3)
implementation(libs.androidx.palette.ktx) implementation(libs.androidx.palette.ktx)
implementation(libs.androidx.preference.ktx) implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerview)
// implementation(libs.androidx.ui)
// implementation(libs.androidx.ui.graphics)
// implementation(libs.androidx.ui.tooling.preview)
implementation(libs.google.material) implementation(libs.google.material)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.jonahbauer.android.preference.annotations) implementation(libs.jonahbauer.android.preference.annotations)
implementation(libs.androidx.ui.tooling.preview.android)
kapt(libs.jonahbauer.android.preference.annotations) kapt(libs.jonahbauer.android.preference.annotations)
testImplementation(libs.junit) testImplementation(libs.junit)
// androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
// androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.androidx.test.ext.junit)
// androidTestImplementation(libs.androidx.ui.test.junit4)
// debugImplementation(libs.androidx.ui.test.manifest)
// debugImplementation(libs.androidx.ui.tooling)
} }

View file

@ -1,18 +1,35 @@
package de.jrpie.android.launcher.ui.settings.meta package de.jrpie.android.launcher.ui.settings.meta
import android.app.AlertDialog import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import androidx.compose.foundation.clickable
import android.widget.TextView import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.copyToClipboard import de.jrpie.android.launcher.copyToClipboard
import de.jrpie.android.launcher.databinding.SettingsMetaBinding
import de.jrpie.android.launcher.getDeviceInfo import de.jrpie.android.launcher.getDeviceInfo
import de.jrpie.android.launcher.openInBrowser import de.jrpie.android.launcher.openInBrowser
import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.openTutorial
@ -30,13 +47,21 @@ import de.jrpie.android.launcher.ui.UIObject
*/ */
class SettingsFragmentMeta : Fragment(), UIObject { class SettingsFragmentMeta : Fragment(), UIObject {
private lateinit var binding: SettingsMetaBinding
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = SettingsMetaBinding.inflate(inflater, container, false) return ComposeView(requireContext()).apply {
return binding.root setContent {
MaterialTheme {
SettingsMetaScreen(
context = requireContext(),
onResetConfirmed = { requireActivity().finish() }
)
}
}
}
} }
override fun onStart() { override fun onStart() {
@ -45,99 +70,204 @@ class SettingsFragmentMeta : Fragment(), UIObject {
} }
override fun setOnClicks() { override fun setOnClicks() {
// No longer needed as click handlers are defined in Compose
fun bindURL(view: View, urlRes: Int) {
view.setOnClickListener {
openInBrowser(
getString(urlRes),
requireContext()
)
}
}
binding.settingsMetaButtonViewTutorial.setOnClickListener {
openTutorial(requireContext())
}
// prompting for settings-reset confirmation
binding.settingsMetaButtonResetSettings.setOnClickListener {
AlertDialog.Builder(this.requireContext(), R.style.AlertDialogCustom)
.setTitle(getString(R.string.settings_meta_reset))
.setMessage(getString(R.string.settings_meta_reset_confirm))
.setPositiveButton(
android.R.string.ok
) { _, _ ->
resetPreferences(this.requireContext())
requireActivity().finish()
}
.setNegativeButton(android.R.string.cancel, null)
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
// view code
bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
// report a bug
binding.settingsMetaButtonReportBug.setOnClickListener {
val deviceInfo = getDeviceInfo()
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
setView(R.layout.dialog_report_bug)
setTitle(R.string.dialog_report_bug_title)
setPositiveButton(R.string.dialog_report_bug_create_report) { _, _ ->
openInBrowser(
getString(R.string.settings_meta_report_bug_link),
requireContext()
)
}
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
}.create().also { it.show() }.apply {
val info = findViewById<TextView>(R.id.dialog_report_bug_device_info)
val buttonClipboard = findViewById<Button>(R.id.dialog_report_bug_button_clipboard)
val buttonSecurity = findViewById<Button>(R.id.dialog_report_bug_button_security)
info.text = deviceInfo
buttonClipboard.setOnClickListener {
copyToClipboard(requireContext(), deviceInfo)
}
info.setOnClickListener {
copyToClipboard(requireContext(), deviceInfo)
}
buttonSecurity.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_report_vulnerability_link),
requireContext()
)
}
}
}
// join chat
bindURL(binding.settingsMetaButtonJoinChat, R.string.settings_meta_chat_url)
// contact developer
// bindURL(binding.settingsMetaButtonContact, R.string.settings_meta_contact_url)
// contact fork developer
bindURL(binding.settingsMetaButtonForkContact, R.string.settings_meta_fork_contact_url)
// donate
bindURL(binding.settingsMetaButtonDonate, R.string.settings_meta_donate_url)
// privacy policy
bindURL(binding.settingsMetaButtonPrivacy, R.string.settings_meta_privacy_url)
// legal info
binding.settingsMetaButtonLicenses.setOnClickListener {
startActivity(Intent(this.context, LegalInfoActivity::class.java))
}
// version
binding.settingsMetaTextVersion.text = BuildConfig.VERSION_NAME
binding.settingsMetaTextVersion.setOnClickListener {
val deviceInfo = getDeviceInfo()
copyToClipboard(requireContext(), deviceInfo)
}
} }
} }
// Here we define what the settings meta screen looks like
@Composable
fun SettingsMetaScreen(
context: Context,
onResetConfirmed: () -> Unit
) {
val openAlertDialog = remember { mutableStateOf(false) }
// Here we tell the screen to lay out the settings buttons in a column.
// This could also be a row, but that's hella ugly in this use case.
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// View Tutorial
SettingsButton(
text = stringResource(R.string.settings_meta_show_tutorial),
onClick = { openTutorial(context) }
)
// Reset Settings
SettingsButton(
text = stringResource(R.string.settings_meta_reset),
onClick = { openAlertDialog.value = true }
)
SettingsButtonSpacer()
// View Code
SettingsButton(
text = stringResource(R.string.settings_meta_view_code),
onClick = { openInBrowser(context.getString(R.string.settings_meta_link_github), context) }
)
// Report a Bug (Placeholder for dialog, simplified for now)
SettingsButton(
text = stringResource(R.string.settings_meta_report_bug),
onClick = {
// Simplified: Directly open bug report link
// TODO: Implement Compose dialog for bug reporting if needed
openInBrowser(context.getString(R.string.settings_meta_report_bug_link), context)
}
)
SettingsButtonSpacer()
// Join Chat
SettingsButton(
text = stringResource(R.string.settings_meta_join_chat),
onClick = { openInBrowser(context.getString(R.string.settings_meta_chat_url), context) }
)
// Contact Fork Developer
SettingsButton(
text = stringResource(R.string.settings_meta_fork_contact),
onClick = { openInBrowser(context.getString(R.string.settings_meta_fork_contact_url), context) }
)
// Donate
SettingsButton(
text = stringResource(R.string.settings_meta_donate),
onClick = { openInBrowser(context.getString(R.string.settings_meta_donate_url), context) }
)
SettingsButtonSpacer()
// Privacy Policy
SettingsButton(
text = stringResource(R.string.settings_meta_privacy),
onClick = { openInBrowser(context.getString(R.string.settings_meta_privacy_url), context) }
)
// Legal Info
SettingsButton(
text = stringResource(R.string.settings_meta_licenses),
onClick = {
context.startActivity(Intent(context, LegalInfoActivity::class.java))
}
)
// Version
Text(
text = BuildConfig.VERSION_NAME,
// TODO: Implement matching font to Launcher Appearance font
textAlign = TextAlign.Right,
color = colorResource(R.color.finnmglasTheme_text_color),
modifier = Modifier
.fillMaxWidth(1f)
.padding(
top = 16.dp,
bottom = 8.dp,
end = 8.dp
)
.clickable {
val deviceInfo = getDeviceInfo()
copyToClipboard(context, deviceInfo)
}
)
// Reset Settings Dialog
if (openAlertDialog.value) {
AlertDialogResetSettings(
onDismissRequest = { openAlertDialog.value = false },
onConfirmation = {
openAlertDialog.value = false
resetPreferences(context)
onResetConfirmed()
},
dialogTitle = stringResource(R.string.settings_meta_reset),
dialogText = stringResource(R.string.settings_meta_reset_confirm),
icon = Icons.Default.Warning
)
}
}
}
// Here we define what a settings button looks like
@Composable
fun SettingsButton(
text: String,
onClick: () -> Unit
) {
Button(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(8.dp),
shape = RoundedCornerShape(4.dp),
// TODO: colors can be changed to match the dynamic system theme
// https://developer.android.com/codelabs/jetpack-compose-theming#0
colors = ButtonDefaults.buttonColors(
containerColor = colorResource(R.color.finnmglasTheme_accent_color),
contentColor = colorResource(R.color.finnmglasTheme_text_color)
)
) {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.align(Alignment.CenterVertically),
color = colorResource(R.color.finnmglasTheme_text_color)
)
}
}
// Here we define some space to put between the buttons
@Composable
fun SettingsButtonSpacer() {
Spacer(modifier = Modifier.height(48.dp))
}
// Here we define what an alert dialog looks like
@Composable
fun AlertDialogResetSettings(
onDismissRequest: () -> Unit,
onConfirmation: () -> Unit,
dialogTitle: String,
dialogText: String,
icon: ImageVector
) {
AlertDialog(
icon = {
Icon(
imageVector = icon,
contentDescription = stringResource(android.R.string.dialog_alert_title),
tint = MaterialTheme.colorScheme.error
)
},
title = {
Text(text = dialogTitle)
},
text = {
Text(text = dialogText)
},
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
onClick = onConfirmation
)
{
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(
onClick = onDismissRequest
) {
Text(stringResource(android.R.string.cancel))
}
},
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
)
}

View file

@ -1,26 +1,27 @@
[versions] [versions]
activity = "1.10.1" activity = "1.10.1"
activity-ktx = "1.8.0" activity-ktx = "1.10.1"
activityCompose = "1.10.1" activityCompose = "1.10.1"
agp = "8.10.0" agp = "8.10.0"
android-preference-annotations = "1.1.2" android-preference-annotations = "1.1.2"
appcompat = "1.7.0" appcompat = "1.7.0"
composeBom = "2025.05.00" composeBom = "2025.05.00"
constraintlayout = "2.2.0" compose-latest = "1.8.1"
core-ktx = "1.15.0" constraintlayout = "2.2.1"
core-ktx = "1.16.0"
espresso-core = "3.6.1" espresso-core = "3.6.1"
gridlayout = "1.0.0" gridlayout = "1.1.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" junitVersion = "1.2.1"
kapt = "1.8.10" kapt = "1.8.10"
kotlin = "2.1.0" kotlin = "2.1.0"
kotlinx-serialization-json = "1.7.3" kotlinx-serialization-json = "1.7.3"
# lifecycleRuntimeKtx = "2.9.0"
material = "1.12.0" material = "1.12.0"
palette-ktx = "1.0.0" palette-ktx = "1.0.0"
preference-ktx = "1.2.1" preference-ktx = "1.2.1"
recyclerview = "1.4.0" recyclerview = "1.4.0"
serialization = "1.8.10" serialization = "1.8.10"
uiToolingPreviewAndroid = "1.8.1"
[libraries] [libraries]
# AndroidX dependencies # AndroidX dependencies
@ -29,18 +30,16 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity-ktx" } androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity-ktx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose-latest" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
androidx-gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" } androidx-gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" }
androidx-palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "palette-ktx" } androidx-palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "palette-ktx" }
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference-ktx" } androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference-ktx" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
# androidx-ui = { group = "androidx.compose.ui", name = "ui" }
# androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
# androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
# androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
# androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
# androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
# Google Material # Google Material
google-material = { group = "com.google.android.material", name = "material", version.ref = "material" } google-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
@ -53,9 +52,9 @@ jonahbauer-android-preference-annotations = { group = "eu.jonahbauer", name = "a
# Testing dependencies # Testing dependencies
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
# androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }