1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 12:02:33 +01:00

Merge pull request #1951 from AllanWang/settings

This commit is contained in:
Allan Wang 2023-06-22 19:02:27 -07:00 committed by GitHub
commit f0b833e741
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 670 additions and 0 deletions

View File

@ -190,6 +190,7 @@ dependencies {
implementation("androidx.compose.ui:ui:${composeVersion}")
implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
implementation("androidx.navigation:navigation-compose:2.6.0")
// Tooling support (Previews, etc.)
implementation("androidx.compose.ui:ui-tooling:${composeVersion}")
// Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)

View File

@ -0,0 +1,42 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.compose
import android.app.Activity
import android.graphics.Color
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.core.view.WindowCompat
/** Wrapper used for compose previews */
@Composable
fun FrostPreview(content: @Composable () -> Unit) {
val isPreview = LocalInspectionMode.current
val activity = LocalContext.current as Activity
LaunchedEffect(Unit) {
if (!isPreview) {
println("FrostPreview is in use")
}
val window = activity.window
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
}
FrostTheme(isDarkTheme = true, content = content)
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.compose.settings
/**
* Basic state container for settings.
*
* Allows for getting and assigning values. It is expected that assigning values will cause
* recompositions as needed.
*/
interface SettingState<T> {
val enabled: Boolean
var value: T
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.compose.settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
@DslMarker annotation class SettingsDslMarker
@SettingsDslMarker
interface SettingsDsl {
/** Entry point to avoid cluttering the global namespace. */
companion object : SettingsDsl
}
/** Dsl for creating individual entries in a list */
@SettingsDslMarker
interface SettingsListDsl {
/**
* Sub list with group title
*
* TODO support collapsed and/or shown?
*/
// fun group(title: String, enabled: Boolean = true, action: SettingsListDsl.() -> Unit)
/** Generic item without content */
fun item(
title: String,
enabled: Boolean = true,
icon: ImageVector? = null,
description: String? = null,
onClick: (() -> Unit)? = null
)
/** Long, non clickable content */
fun description(text: String, icon: ImageVector? = null)
fun checkbox(
title: String,
enabled: Boolean = true,
icon: ImageVector? = null,
description: String? = null,
checked: Boolean,
onCheckedChanged: (Boolean) -> Unit,
)
fun switch(
title: String,
enabled: Boolean = true,
icon: ImageVector? = null,
description: String? = null,
checked: Boolean,
onCheckedChanged: (Boolean) -> Unit,
)
fun custom(
title: String,
enabled: Boolean = true,
icon: ImageVector? = null,
description: String? = null,
onClick: (() -> Unit)? = null,
content: @Composable () -> Unit,
)
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.compose.settings
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.pitchedapps.frost.compose.FrostPreview
@Composable
fun SettingsListDsl(
modifier: Modifier = Modifier,
content: @Composable SettingsListDsl.() -> Unit
) {
val items = SettingsDsl.settingsListDsl(content)
LazyColumn(modifier = modifier) { items(items) { compose -> compose() } }
}
@Preview
@Composable
fun SettingsListDslPreview() {
data class Model(
val check1: Boolean = false,
val switch1: Boolean = false,
val switch2: Boolean = false,
)
var state by remember { mutableStateOf(Model()) }
FrostPreview {
SettingsListDsl {
checkbox(
title = "Check 1",
checked = state.check1,
onCheckedChanged = { state = state.copy(check1 = it) },
)
checkbox(
title = "Check 1",
description = "Linked again",
checked = state.check1,
onCheckedChanged = { state = state.copy(check1 = it) },
)
switch(
title = "Switch 1",
checked = state.switch1,
onCheckedChanged = { state = state.copy(switch1 = it) },
)
switch(
title = "Switch 2",
enabled = state.switch1,
description = "Enabled by switch 1",
checked = state.switch2,
onCheckedChanged = { state = state.copy(switch2 = it) },
)
}
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.compose.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.size
import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pitchedapps.frost.compose.FrostPreview
import com.pitchedapps.frost.ext.optionalCompose
import com.pitchedapps.frost.ext.thenIf
/** Basic building block for settings */
@Composable
fun SettingsListItem(
modifier: Modifier = Modifier,
title: String,
enabled: Boolean = true,
icon: ImageVector? = null,
description: String? = null,
onClick: (() -> Unit)? = null,
content: (@Composable () -> Unit)? = null
) {
val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
ListItem(
modifier =
modifier.thenIf(onClick != null) {
Modifier.clickable(enabled = enabled) { onClick?.invoke() }
},
leadingContent =
icon.optionalCompose {
Icon(
modifier = Modifier.size(24.dp).alpha(alpha),
imageVector = it,
contentDescription = null,
)
},
headlineContent = { Text(modifier = Modifier.alpha(alpha), text = title) },
supportingContent =
description.optionalCompose {
Text(
modifier = Modifier.alpha(alpha),
text = it,
)
},
trailingContent = content,
)
}
@Preview
@Composable
private fun SettingsListItemPreview() {
var state by remember { mutableStateOf(false) }
FrostPreview {
SettingsListItem(
icon = Icons.Outlined.Person,
title = "Test Title",
description = "Test Description",
) {
Checkbox(
checked = state,
onCheckedChange = { state = it },
)
}
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.compose.settings
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun LazyListScope.settingsListDsl(content: @Composable SettingsListDsl.() -> Unit) {
val items = SettingsDsl.settingsListDsl(content)
items(items) { compose -> compose() }
}
@Composable
fun SettingsDsl.settingsListDsl(
content: @Composable SettingsListDsl.() -> Unit
): List<@Composable () -> Unit> {
val data = SettingsListDslData()
data.content()
return data.items
}
private class SettingsListDslData : SettingsListDsl {
val items: MutableList<@Composable () -> Unit> = mutableListOf()
private fun addCompose(content: @Composable () -> Unit) {
items.add(content)
}
override fun item(
title: String,
enabled: Boolean,
icon: ImageVector?,
description: String?,
onClick: (() -> Unit)?
) {
addCompose {
SettingsListItem(
icon = icon,
title = title,
enabled = enabled,
description = description,
onClick = onClick,
content = null,
)
}
}
override fun description(text: String, icon: ImageVector?) {}
override fun checkbox(
title: String,
enabled: Boolean,
icon: ImageVector?,
description: String?,
checked: Boolean,
onCheckedChanged: (Boolean) -> Unit
) {
custom(
icon = icon,
title = title,
enabled = enabled,
description = description,
onClick = { onCheckedChanged(!checked) },
) {
Checkbox(
enabled = enabled,
checked = checked,
onCheckedChange = onCheckedChanged,
)
}
}
override fun switch(
title: String,
enabled: Boolean,
icon: ImageVector?,
description: String?,
checked: Boolean,
onCheckedChanged: (Boolean) -> Unit
) {
custom(
icon = icon,
title = title,
enabled = enabled,
description = description,
onClick = { onCheckedChanged(!checked) },
) {
Switch(
enabled = enabled,
checked = checked,
onCheckedChange = onCheckedChanged,
)
}
}
override fun custom(
title: String,
enabled: Boolean,
icon: ImageVector?,
description: String?,
onClick: (() -> Unit)?,
content: @Composable () -> Unit
) {
addCompose {
SettingsListItem(
icon = icon,
title = title,
enabled = enabled,
description = description,
onClick = onClick,
content = content,
)
}
}
}

View File

@ -34,3 +34,13 @@ fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
fun IntSize.toDpSize(density: Density): DpSize {
return with(density) { DpSize(width.toDp(), height.toDp()) }
}
/**
* Helper for functions that take in nullable compose lambdas.
*
* If the input is null, return null. Otherwise, return the provided composable lambda.
*/
fun <T> T?.optionalCompose(action: @Composable (T) -> Unit): (@Composable () -> Unit)? {
if (this == null) return null
return { action(this) }
}

View File

@ -0,0 +1,110 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.settings
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.pitchedapps.frost.R
import com.pitchedapps.frost.compose.FrostPreview
import com.pitchedapps.frost.settings.screens.MainSettingsScreen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen() {
val navController = rememberNavController()
val topBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
topBar = {
MediumTopAppBar(
scrollBehavior = topBarScrollBehavior,
navigationIcon = {
IconButton(onClick = {}) {
Icon(imageVector = Icons.Outlined.ArrowBack, contentDescription = null)
}
},
title = {
val entry by navController.currentBackStackEntryAsState()
val title =
entry
?.destination
?.route
?.let { SettingsPages.valueOf(it) }
?.titleId
?.let { stringResource(id = it) }
if (title != null) {
Text(text = title)
}
},
actions = { IconButton(onClick = {}) { Icon(Icons.Outlined.Info, null) } },
)
},
) { paddingValue ->
NavHost(
modifier =
Modifier.fillMaxSize()
.nestedScroll(topBarScrollBehavior.nestedScrollConnection)
.padding(paddingValue),
navController = navController,
startDestination = SettingsPages.Main.name,
) {
composable(SettingsPages.Main) { MainSettingsScreen() }
/*...*/
}
}
}
private fun NavGraphBuilder.composable(
route: SettingsPages,
content: @Composable (NavBackStackEntry) -> Unit
) = composable(route = route.name, content = content)
private enum class SettingsPages(val titleId: Int) {
Main(R.string.settings),
Appearance(R.string.appearance)
}
@Preview
@Composable
fun SettingsScreenPreview() {
FrostPreview { SettingsScreen() }
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.settings.screens
import androidx.compose.material.icons.Icons.Outlined as MaterialIcons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Newspaper
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Replay
import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material.icons.outlined.TrendingUp
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.pitchedapps.frost.R
import com.pitchedapps.frost.compose.FrostPreview
import com.pitchedapps.frost.compose.settings.SettingsListDsl
@Composable
fun MainSettingsScreen(modifier: Modifier = Modifier) {
SettingsListDsl(modifier = modifier) {
item(
icon = MaterialIcons.Palette,
title = stringResource(id = R.string.appearance),
description = stringResource(id = R.string.appearance_desc),
)
item(
icon = MaterialIcons.TrendingUp,
title = stringResource(id = R.string.behaviour),
description = stringResource(id = R.string.behaviour_desc),
)
item(
icon = MaterialIcons.Newspaper,
title = stringResource(id = R.string.newsfeed),
description = stringResource(id = R.string.newsfeed_desc),
)
item(
icon = MaterialIcons.Notifications,
title = stringResource(id = R.string.notifications),
description = stringResource(id = R.string.notifications_desc),
)
item(
icon = MaterialIcons.Lock,
title = stringResource(id = R.string.security),
description = stringResource(id = R.string.security_desc),
)
item(
icon = MaterialIcons.Info,
title = stringResource(id = R.string.about_frost),
description = stringResource(id = R.string.about_frost_desc),
)
item(
icon = MaterialIcons.Translate,
title = stringResource(id = R.string.help_translate),
description = stringResource(id = R.string.help_translate_desc),
)
item(
icon = MaterialIcons.Replay,
title = stringResource(id = R.string.replay_intro),
)
// item(
// icon = MaterialIcons.Science,
// title = stringResource(id = R.string.experimental),
// description = stringResource(id = R.string.experimental_desc),
// )
}
}
@Preview
@Composable
fun MainSettingsScreenPreview() {
FrostPreview { MainSettingsScreen() }
}