Skip to content

Commit

Permalink
Add UI for icon requesting
Browse files Browse the repository at this point in the history
This commits adds a simple UI to view all unthemed icons, alongside a way to either copy them to clipboard and open the form (either prefilled or empty).

It also adds a simple long-click gesture to directly visit the icon request form with prefilled information.

If the URL length is greater than DIRECT_LINK_MAX_LENGTH however, it removes the direct link and offers the user to copy the contents to the clipboard.

Fixes #2162
  • Loading branch information
SuperDragonXD committed Jun 16, 2024
1 parent cf84305 commit 6115328
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package app.lawnchair.lawnicons.ui.components.home

import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.horizontalScroll
Expand Down Expand Up @@ -56,6 +53,7 @@ import app.lawnchair.lawnicons.ui.components.core.Card
import app.lawnchair.lawnicons.ui.components.core.ListRow
import app.lawnchair.lawnicons.ui.components.core.SimpleListRow
import app.lawnchair.lawnicons.ui.theme.LawniconsTheme
import app.lawnchair.lawnicons.ui.util.Constants
import app.lawnchair.lawnicons.ui.util.PreviewLawnicons
import app.lawnchair.lawnicons.ui.util.SampleData

Expand Down Expand Up @@ -125,7 +123,7 @@ fun IconInfoSheet(
IconLink(
iconResId = R.drawable.github_foreground,
label = stringResource(id = R.string.view_on_github),
url = "https://github.com/LawnchairLauncher/lawnicons/blob/develop/svgs/$githubName.svg",
url = "${Constants.GITHUB}/blob/develop/svgs/$githubName.svg",
)
}
}
Expand Down Expand Up @@ -275,12 +273,6 @@ fun IconInfoSheet(
}
}

private fun copyTextToClipboard(context: Context, text: String) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(context.getString(R.string.copied_text), text)
clipboard.setPrimaryClip(clip)
}

@PreviewLawnicons
@Composable
private fun IconInfoPopupPreview() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.lawnchair.lawnicons.ui.components.home

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
Expand Down Expand Up @@ -101,7 +102,7 @@ fun IconPreview(
)
}
}
if (isIconInfoShown.value) {
AnimatedVisibility(isIconInfoShown.value) {
IconInfoSheet(
iconInfo = iconInfo,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,265 @@
package app.lawnchair.lawnicons.ui.components.home

import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import app.lawnchair.lawnicons.R
import app.lawnchair.lawnicons.model.IconRequest
import app.lawnchair.lawnicons.model.IconRequestModel
import app.lawnchair.lawnicons.ui.components.core.Card
import app.lawnchair.lawnicons.ui.util.Constants
import app.lawnchair.lawnicons.ui.util.isScrollingUp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

@Composable
fun IconRequestFAB(
iconRequestModel: IconRequestModel?,
lazyGridState: LazyGridState,
modifier: Modifier = Modifier,
) {
Log.d("IconRequestFAB", "iconRequestModel: $iconRequestModel")
if (iconRequestModel != null) {
if (iconRequestModel.iconCount > 0) {
IconRequestFAB(
iconRequestList = iconRequestModel.list,
iconCount = iconRequestModel.iconCount,
lazyGridState = lazyGridState,
modifier = modifier,
)
}
}
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun IconRequestFAB(
iconRequestList: List<IconRequest>,
iconCount: Int,
lazyGridState: LazyGridState,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val request = buildForm(iconRequestList.joinToString("%0A") { "${it.name}%0A${it.componentName}" })

val list = iconRequestList.joinToString("\n") { "${it.name}\n${it.componentName}" }
val request = buildForm(list.replace("\n", "%20"))

val sheetExpanded = remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)

val coroutineScope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }
val viewConfiguration = LocalViewConfiguration.current

val directLinkEnabled = request.length < Constants.DIRECT_LINK_MAX_LENGTH

LaunchedEffect(interactionSource) {
var isLongClick = false

interactionSource.interactions.collectLatest { interaction ->
when (interaction) {
is PressInteraction.Press -> {
isLongClick = false
delay(viewConfiguration.longPressTimeoutMillis)
isLongClick = true
if (directLinkEnabled) {
openLink(context, request)
} else {
coroutineScope.launch {
sheetExpanded.value = true
sheetState.show()
}
}
}
is PressInteraction.Release -> {
if (!isLongClick) {
coroutineScope.launch {
sheetExpanded.value = true
sheetState.show()
}
}
}
is PressInteraction.Cancel -> {
isLongClick = false
}
}
}
}

ExtendedFloatingActionButton(
text = {
Text(stringResource(R.string.unthemed_icons_info))
Text(stringResource(R.string.request_icons))
},
icon = { Icon(painter = painterResource(id = R.drawable.icon_request_app), contentDescription = null) },
onClick = {
val website = Uri.parse(request)
val intent = Intent(Intent.ACTION_VIEW, website)
context.startActivity(intent)
icon = {
Icon(
painter = painterResource(id = R.drawable.icon_request_app),
contentDescription = null,
)
},
modifier = modifier,
onClick = {},
expanded = lazyGridState.isScrollingUp(),
interactionSource = interactionSource,
modifier = modifier,
)
AnimatedVisibility(visible = sheetExpanded.value) {
ModalBottomSheet(
onDismissRequest = { sheetExpanded.value = false },
sheetState = sheetState,
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val detailsExpanded = remember { mutableStateOf(false) }

Text(
text = stringResource(R.string.unthemed_icons_info_title, iconCount),
style = MaterialTheme.typography.headlineSmall,
)
if (!directLinkEnabled) {
detailsExpanded.value = true
Card {
Row(
modifier = Modifier
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Warning,
contentDescription = null,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.unthemed_icons_info_text),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
} else {
Text(
text = stringResource(R.string.icon_request_hold_tip),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Row {
Button(
onClick = {
openLink(context, request)
},
) {
Text(stringResource(R.string.request_all_unthemed_icons))
}
Spacer(modifier = Modifier.width(8.dp))
FilledTonalButton(
onClick = {
detailsExpanded.value = !detailsExpanded.value
},
) {
Text(stringResource(R.string.more_information))
}
}
}

Spacer(modifier = Modifier.height(16.dp))
AnimatedVisibility(visible = detailsExpanded.value) {
Card {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(16.dp),
) {
Text(
text = list,
fontFamily = FontFamily.Monospace,
modifier = Modifier
.horizontalScroll(rememberScrollState()),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
onClick = {
copyTextToClipboard(context, list)
},
) {
Text(stringResource(R.string.copy_to_clipboard))
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(
onClick = {
openLink(context, Constants.ICON_REQUEST_FORM)
},
) {
Text(stringResource(R.string.open_request_form))
}
}
}
}
}
}
}
}
}

private fun copyTextToClipboard(context: Context, text: String) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(context.getString(R.string.copied_text), text)
clipboard.setPrimaryClip(clip)
}

private fun openLink(context: Context, link: String) {
val website = Uri.parse(link)
val intent = Intent(Intent.ACTION_VIEW, website)
context.startActivity(intent)
}

private fun buildForm(string: String): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ private fun IconInfoListItem(iconInfo: ImmutableList<IconInfo>) {
.clip(RoundedCornerShape(16.dp))
.clickable(onClick = { isIconInfoAppfilterShown.value = true }),
)
if (isIconInfoAppfilterShown.value) {
AnimatedVisibility(isIconInfoAppfilterShown.value) {
IconInfoSheet(
iconInfo = it,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import app.lawnchair.lawnicons.ui.components.core.Card
import app.lawnchair.lawnicons.ui.components.core.LawniconsScaffold
import app.lawnchair.lawnicons.ui.components.core.SimpleListRow
import app.lawnchair.lawnicons.ui.theme.LawniconsTheme
import app.lawnchair.lawnicons.ui.util.Constants
import app.lawnchair.lawnicons.ui.util.Contributor
import app.lawnchair.lawnicons.ui.util.Destinations
import app.lawnchair.lawnicons.ui.util.ExternalLink
Expand All @@ -43,12 +44,12 @@ private val externalLinks = listOf(
ExternalLink(
iconResId = R.drawable.github_foreground,
name = R.string.github,
url = "https://github.com/LawnchairLauncher/lawnicons",
url = "${Constants.GITHUB}",
),
ExternalLink(
iconResId = R.drawable.icon_request_app,
name = R.string.request_form,
url = "https://forms.gle/xt7sJhgWEasuo9TR9",
url = Constants.ICON_REQUEST_FORM,
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ import app.lawnchair.lawnicons.ui.components.ContributorRowPlaceholder
import app.lawnchair.lawnicons.ui.components.ExternalLinkRow
import app.lawnchair.lawnicons.ui.components.core.LawniconsScaffold
import app.lawnchair.lawnicons.ui.theme.LawniconsTheme
import app.lawnchair.lawnicons.ui.util.Constants
import app.lawnchair.lawnicons.ui.util.PreviewLawnicons
import app.lawnchair.lawnicons.viewmodel.ContributorsUiState
import app.lawnchair.lawnicons.viewmodel.ContributorsViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

const val CONTRIBUTOR_URL = "https://github.com/LawnchairLauncher/lawnicons/graphs/contributors"
const val CONTRIBUTOR_URL = "${Constants.GITHUB}/graphs/contributors"

@Composable
fun Contributors(
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/kotlin/app/lawnchair/lawnicons/ui/util/Constants.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package app.lawnchair.lawnicons.ui.util

object Constants {
const val ICON_REQUEST_FORM = "https://forms.gle/xt7sJhgWEasuo9TR9"
const val GITHUB = "https://github.com/LawnchairLauncher/lawnicons"

/**
* Maximum length of a direct Google forms link. The limit is around 2300-2400 characters, so we use the lower limit to prevent any issues.
*/
const val DIRECT_LINK_MAX_LENGTH = 2300
}

This file was deleted.

Loading

0 comments on commit 6115328

Please sign in to comment.