Skip to content

Commit

Permalink
DO NOT MERGE: androidApp: Initial support for tickets
Browse files Browse the repository at this point in the history
Signed-off-by: Aayush Gupta <[email protected]>
  • Loading branch information
theimpulson committed Oct 11, 2024
1 parent a567b2b commit c37c0d3
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import app.opass.ccip.android.ui.screens.event.EventScreen
import app.opass.ccip.android.ui.screens.eventpreview.EventPreviewScreen
import app.opass.ccip.android.ui.screens.schedule.ScheduleScreen
import app.opass.ccip.android.ui.screens.session.SessionScreen
import app.opass.ccip.android.ui.screens.ticket.TicketScreen

@Composable
fun SetupNavGraph(navHostController: NavHostController, startDestination: Screen) {
Expand Down Expand Up @@ -51,6 +52,10 @@ fun SetupNavGraph(navHostController: NavHostController, startDestination: Screen
composable<Screen.Session> { backStackEntry ->
backStackEntry.toRoute<Screen.Session>().SessionScreen(navHostController)
}

composable<Screen.Ticket> { backStackEntry ->
backStackEntry.toRoute<Screen.Ticket>().TicketScreen(navHostController)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ sealed class Screen(@StringRes val title: Int, @DrawableRes val icon: Int) {
title = R.string.session,
icon = R.drawable.ic_podium
)

@Serializable
data class Ticket(val eventId: String) : Screen(
title = R.string.ticket,
icon = R.drawable.ic_ticket
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ fun Screen.Event.EventScreen(
FeatureItem(
label = stringResource(id = R.string.ticket),
iconRes = R.drawable.ic_ticket
)
) {
navHostController.navigate(Screen.Ticket(this@EventScreen.id))
}
}

FeatureType.VENUE -> {
Expand Down Expand Up @@ -260,6 +262,7 @@ private fun HeaderImage(logoUrl: String?) {
.padding(horizontal = 32.dp)
.aspectRatio(2.0f)
.heightIn(max = 180.dp)
.clip(RoundedCornerShape(10.dp))
.shimmer(logoUrl == null),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* SPDX-FileCopyrightText: 2024 OPass
* SPDX-License-Identifier: GPL-3.0-only
*/

package app.opass.ccip.android.ui.screens.ticket

import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import app.opass.ccip.android.R
import app.opass.ccip.android.ui.components.TopAppBar
import app.opass.ccip.android.ui.extensions.shimmer
import app.opass.ccip.android.ui.navigation.Screen
import coil.compose.SubcomposeAsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import kotlinx.coroutines.android.awaitFrame

@Composable
fun Screen.Ticket.TicketScreen(
navHostController: NavHostController,
viewModel: TicketViewModel = hiltViewModel()
) {

val context = LocalContext.current
var shouldShowManualEntryDialog by rememberSaveable { mutableStateOf(false) }
val eventConfig by viewModel.eventConfig.collectAsStateWithLifecycle()
val startActivityForResult = rememberLauncherForActivityResult(
contract = PickVisualMedia(),
onResult = { uri ->
// TODO: Process the image
}
)

LaunchedEffect(key1 = Unit) { viewModel.getEventConfig(this@TicketScreen.eventId) }

if (shouldShowManualEntryDialog) {
ManualEntryDialog(onDismiss = { shouldShowManualEntryDialog = false })
}

Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = stringResource(this.title),
navHostController = navHostController,
actions = {
IconButton(onClick = { shouldShowManualEntryDialog = true }) {
Icon(
painter = painterResource(R.drawable.ic_keyboard),
contentDescription = stringResource(R.string.enter_token_manually_title)
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
verticalArrangement = Arrangement.spacedBy(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
HeaderImage(logoUrl = eventConfig?.logoUrl)

// Actions
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally)
) {
if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
Button(onClick = {}) {
Text(text = stringResource(R.string.scan))
}
}
FilledTonalButton(
onClick = {
startActivityForResult.launch(
PickVisualMediaRequest(PickVisualMedia.ImageOnly)
)
}
) {
Icon(
painter = painterResource(R.drawable.ic_gallery),
contentDescription = null,
modifier = Modifier.padding(end = 5.dp)
)
Text(text = stringResource(R.string.upload_from_gallery))
}
}
}
}
}

@Composable
private fun HeaderImage(logoUrl: String?) {
SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(logoUrl)
.placeholder(R.drawable.ic_landscape)
.error(R.drawable.ic_broken_image)
.crossfade(true)
.memoryCacheKey(logoUrl)
.diskCacheKey(logoUrl)
.diskCachePolicy(CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = "",
contentScale = ContentScale.Fit,
modifier = Modifier
.padding(horizontal = 32.dp)
.aspectRatio(2.0f)
.heightIn(max = 180.dp)
.clip(RoundedCornerShape(10.dp))
.shimmer(logoUrl == null),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
)
}

@Composable
private fun ManualEntryDialog(onDismiss: () -> Unit = {}) {
var token by rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }

LaunchedEffect(focusRequester) {
awaitFrame()
focusRequester.requestFocus()
}

AlertDialog(
title = { Text(text = stringResource(R.string.enter_token_manually_title)) },
text = {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(text = stringResource(R.string.enter_token_manually_desc))
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = token,
onValueChange = { token = it },
shape = RoundedCornerShape(10.dp)
)
}
},
onDismissRequest = { onDismiss() },
confirmButton = {
TextButton (
onClick = {
// TODO: Process the image
onDismiss()
},
enabled = token.isNotBlank()
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(text = stringResource(android.R.string.cancel))
}
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2024 OPass
* SPDX-License-Identifier: GPL-3.0-only
*/

package app.opass.ccip.android.ui.screens.ticket

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.opass.ccip.helpers.PortalHelper
import app.opass.ccip.network.models.eventconfig.EventConfig
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class TicketViewModel @Inject constructor(
private val portalHelper: PortalHelper
): ViewModel() {

private val TAG = TicketViewModel::class.java.simpleName

private val _eventConfig: MutableStateFlow<EventConfig?> = MutableStateFlow(null)
val eventConfig = _eventConfig.asStateFlow()

private val _isRefreshing = MutableStateFlow(false)
val isRefreshing = _isRefreshing.asStateFlow()

fun getEventConfig(eventId: String, forceReload: Boolean = false) {
viewModelScope.launch {
try {
_isRefreshing.value = true
_eventConfig.value = portalHelper.getEventConfig(eventId, forceReload)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch event config", exception)
_eventConfig.value = null
} finally {
_isRefreshing.value = false
}
}
}
}
14 changes: 14 additions & 0 deletions androidApp/src/main/res/drawable/ic_gallery.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M360,560L760,560L622,380L530,500L468,420L360,560ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z"/>
</vector>
14 changes: 14 additions & 0 deletions androidApp/src/main/res/drawable/ic_keyboard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M160,760Q127,760 103.5,736.5Q80,713 80,680L80,280Q80,247 103.5,223.5Q127,200 160,200L800,200Q833,200 856.5,223.5Q880,247 880,280L880,680Q880,713 856.5,736.5Q833,760 800,760L160,760ZM160,680L800,680Q800,680 800,680Q800,680 800,680L800,280Q800,280 800,280Q800,280 800,280L160,280Q160,280 160,280Q160,280 160,280L160,680Q160,680 160,680Q160,680 160,680ZM320,640L640,640L640,560L320,560L320,640ZM200,520L280,520L280,440L200,440L200,520ZM320,520L400,520L400,440L320,440L320,520ZM440,520L520,520L520,440L440,440L440,520ZM560,520L640,520L640,440L560,440L560,520ZM680,520L760,520L760,440L680,440L680,520ZM200,400L280,400L280,320L200,320L200,400ZM320,400L400,400L400,320L320,320L320,400ZM440,400L520,400L520,320L440,320L440,400ZM560,400L640,400L640,320L560,320L560,400ZM680,400L760,400L760,320L680,320L680,400ZM160,680Q160,680 160,680Q160,680 160,680L160,280Q160,280 160,280Q160,280 160,280L160,280Q160,280 160,280Q160,280 160,280L160,680Q160,680 160,680Q160,680 160,680Z"/>
</vector>
8 changes: 7 additions & 1 deletion androidApp/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<string name="venue">Venue</string>
<string name="sponsors">Sponsors</string>
<string name="staff">Staff</string>
<string name="ticket">Ticket</string>
<string name="wifi">WiFi</string>
<string name="irc" translatable="false">IRC</string>

Expand All @@ -29,4 +28,11 @@

<!-- SessionScreen -->
<string name="session">Session</string>

<!-- TicketScreen -->
<string name="ticket">Ticket</string>
<string name="scan">Scan QR</string>
<string name="upload_from_gallery">Upload from Gallery</string>
<string name="enter_token_manually_title">Token</string>
<string name="enter_token_manually_desc">Enter the token manually that you may have received via email or organizers.</string>
</resources>

0 comments on commit c37c0d3

Please sign in to comment.