android app almost done, start designing

This commit is contained in:
2023-09-23 10:54:55 +02:00
parent 0019b25244
commit 009cf4aaf0
9 changed files with 335 additions and 108 deletions

View File

@@ -21,6 +21,10 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ScannerActivity"
android:exported="true">
</activity>
</application> </application>
<uses-feature android:name="android.hardware.camera" android:required="true" /> <uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="true" /> <uses-feature android:name="android.hardware.camera.autofocus" android:required="true" />

View File

@@ -0,0 +1,108 @@
package com.janishutz.libreevent
import java.io.BufferedReader
import java.io.DataOutputStream
import java.io.InputStreamReader
import java.lang.Exception
import java.net.HttpURLConnection
import java.net.URL
class ApiClient {
fun authenticateUser(apiUrl: String, username: String, password: String): String {
try {
val url = URL("$apiUrl/app/authenticate")
println(url)
val connection = url.openConnection() as HttpURLConnection
// Set the request method to POST
connection.requestMethod = "POST"
// Set request headers (if needed)
connection.setRequestProperty("Content-Type", "application/json")
// Add other headers as needed
// Enable input and output streams for the connection
connection.doInput = true
connection.doOutput = true
// Create the JSON request body
val jsonRequest = "{\"username\":\"$username\",\"password\":\"$password\"}"
// Write the JSON data to the output stream
val outputStream = DataOutputStream(connection.outputStream)
outputStream.write(jsonRequest.toByteArray(Charsets.UTF_8))
outputStream.flush()
outputStream.close()
// Get the response code from the server
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
// Read and handle the response from the server
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = StringBuilder()
var line: String?
while (reader.readLine().also { line = it } != null) {
response.append(line)
}
reader.close()
// Return the response as a String
return response.toString()
} else {
// Handle the error (e.g., authentication failed)
// You can also throw an exception here if needed
return "status-code-non-ok"
}
} catch (e: Exception) {
e.printStackTrace()
return "error"
}
}
fun checkTicket(apiUrl: String, username: String, password: String, ticket: String): String {
val url = URL("$apiUrl/app/ticketLookup")
val connection = url.openConnection() as HttpURLConnection
// Set the request method to POST
connection.requestMethod = "POST"
// Set request headers (if needed)
connection.setRequestProperty("Content-Type", "application/json")
// Add other headers as needed
// Enable input and output streams for the connection
connection.doInput = true
connection.doOutput = true
// Create the JSON request body
val jsonRequest = "{\"username\":\"$username\",\"password\":\"$password\",\"ticketID\":$ticket}"
// Write the JSON data to the output stream
val outputStream = DataOutputStream(connection.outputStream)
outputStream.write(jsonRequest.toByteArray(Charsets.UTF_8))
outputStream.flush()
outputStream.close()
// Get the response code from the server
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
// Read and handle the response from the server
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = StringBuilder()
var line: String?
while (reader.readLine().also { line = it } != null) {
response.append(line)
}
reader.close()
// Return the response as a String
return response.toString()
} else {
// Handle the error (e.g., authentication failed)
// You can also throw an exception here if needed
return ""
}
}
}

View File

@@ -4,6 +4,9 @@ import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.widget.Button import android.widget.Button
import android.widget.EditText
import android.app.AlertDialog
import com.janishutz.libreevent.ApiClient
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -11,13 +14,54 @@ class MainActivity : AppCompatActivity() {
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
val loginButton = findViewById<Button>(R.id.loginButton) val loginButton = findViewById<Button>(R.id.loginButton)
val urlEditText = findViewById<EditText>(R.id.url)
val usernameEditText = findViewById<EditText>(R.id.username)
val passwordEditText = findViewById<EditText>(R.id.password)
loginButton.setOnClickListener { loginButton.setOnClickListener {
login() val url = urlEditText.text.toString()
val username = usernameEditText.text.toString()
val password = passwordEditText.text.toString()
login( url, username, password )
} }
} }
private fun login() { private fun login( url: String, username: String, password: String ) {
val switchIntent = Intent(this, ScanActivity::class.java) val res = ApiClient().authenticateUser( url, username, password )
startActivity(switchIntent) println( res )
if ( res == "authOk" ) {
val switchIntent = Intent(this, ScannerActivity::class.java)
startActivity(switchIntent)
} else if ( res == "status-code-non-ok" ) {
val alertDialogBuilder = AlertDialog.Builder(this)
alertDialogBuilder.setTitle("Username or password incorrect")
alertDialogBuilder.setMessage("Please ensure that the values entered are correct and try again")
alertDialogBuilder.setIcon(android.R.drawable.ic_dialog_alert)
alertDialogBuilder.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
alertDialogBuilder.show()
} else if ( res == "error") {
val alertDialogBuilder = AlertDialog.Builder(this)
alertDialogBuilder.setTitle("Unable to connect")
alertDialogBuilder.setMessage("Please ensure that the url specified is correct.")
alertDialogBuilder.setIcon(android.R.drawable.ic_dialog_alert)
alertDialogBuilder.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
alertDialogBuilder.show()
} else if ( res == "wrong") {
val alertDialogBuilder = AlertDialog.Builder(this)
alertDialogBuilder.setTitle("Username or password incorrect")
alertDialogBuilder.setMessage("Please ensure that the values entered are correct and try again")
alertDialogBuilder.setIcon(android.R.drawable.ic_dialog_alert)
alertDialogBuilder.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
alertDialogBuilder.show()
}
} }
} }

View File

@@ -5,14 +5,18 @@ import android.content.pm.PackageManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import com.journeyapps.barcodescanner.BarcodeCallback
import com.journeyapps.barcodescanner.BarcodeResult
import com.journeyapps.barcodescanner.CaptureActivity
import com.journeyapps.barcodescanner.CaptureManager import com.journeyapps.barcodescanner.CaptureManager
import com.journeyapps.barcodescanner.DecoratedBarcodeView import com.journeyapps.barcodescanner.DecoratedBarcodeView
class ScanActivity : AppCompatActivity() { class ScannerActivity : CaptureActivity() {
private lateinit var barcodeView: DecoratedBarcodeView private lateinit var barcodeView: DecoratedBarcodeView
private lateinit var captureManager: CaptureManager private lateinit var captureManager: CaptureManager
private var lastScanned: String = ""
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scanner) setContentView(R.layout.activity_scanner)
@@ -32,6 +36,25 @@ class ScanActivity : AppCompatActivity() {
captureManager.initializeFromIntent(intent, null) captureManager.initializeFromIntent(intent, null)
captureManager.decode() captureManager.decode()
barcodeView.decodeContinuous(object : BarcodeCallback {
override fun barcodeResult(result: BarcodeResult?) {
if (result != null) {
val scannedData = result.text // This is the scanned data (e.g., QR code content)
handleScanResult(scannedData)
}
}
override fun possibleResultPoints(resultPoints: List<com.google.zxing.ResultPoint>?) {
// Optional: Handle possible result points
}
})
}
private fun handleScanResult(result: String) {
if ( lastScanned != result ) {
println(result)
lastScanned = result
}
} }
override fun onResume() { override fun onResume() {

View File

@@ -0,0 +1,73 @@
package com.janishutz.libreevent
import android.Manifest
import android.content.pm.PackageManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.core.app.ActivityCompat
import com.journeyapps.barcodescanner.BarcodeCallback
import com.journeyapps.barcodescanner.CaptureManager
import com.journeyapps.barcodescanner.DecoratedBarcodeView
class ScannerActivity : AppCompatActivity() {
private lateinit var barcodeView: DecoratedBarcodeView
private lateinit var captureManager: CaptureManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scanner)
barcodeView = findViewById(R.id.barcodeScannerView)
// Check for camera permission and request if not granted
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST)
} else {
setupScanner()
}
}
private fun setupScanner() {
captureManager = CaptureManager(this, barcodeView)
captureManager.initializeFromIntent(intent, null)
captureManager.decode()
}
private fun handleScanResult(result: String) {
// The `result` parameter contains the scanned data (e.g., QR code content)
// You can process it or send it as needed
}
override fun onResume() {
super.onResume()
captureManager.onResume()
}
override fun onPause() {
super.onPause()
captureManager.onPause()
}
// Pass savedInstanceState to onSaveInstanceState
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
captureManager.onSaveInstanceState(outState)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == CAMERA_PERMISSION_REQUEST) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
setupScanner()
} else {
// Handle permission denied
}
}
}
companion object {
private const val CAMERA_PERMISSION_REQUEST = 1
}
}

View File

@@ -20,7 +20,7 @@
app:layout_constraintVertical_bias="0.157" /> app:layout_constraintVertical_bias="0.157" />
<EditText <EditText
android:id="@+id/editTextTextEmailAddress" android:id="@+id/username"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="52dp" android:layout_marginTop="52dp"
@@ -30,10 +30,10 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497" app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextText" /> app:layout_constraintTop_toBottomOf="@+id/url" />
<EditText <EditText
android:id="@+id/editTextText" android:id="@+id/url"
android:layout_width="302dp" android:layout_width="302dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
@@ -46,7 +46,7 @@
app:layout_constraintTop_toBottomOf="@+id/textView2" /> app:layout_constraintTop_toBottomOf="@+id/textView2" />
<EditText <EditText
android:id="@+id/editTextTextPassword" android:id="@+id/password"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
@@ -56,7 +56,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.502" app:layout_constraintHorizontal_bias="0.502"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextTextEmailAddress" /> app:layout_constraintTop_toBottomOf="@+id/username" />
<Button <Button
android:id="@+id/loginButton" android:id="@+id/loginButton"
@@ -66,7 +66,7 @@
android:text="Log in" android:text="Log in"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextTextPassword" /> app:layout_constraintTop_toBottomOf="@+id/password" />
<TextView <TextView
android:id="@+id/textView2" android:id="@+id/textView2"

View File

@@ -64,7 +64,7 @@
} }
.cart-list { .cart-list {
width: 50%; width: 90%;
} }
ul { ul {
@@ -89,6 +89,12 @@
.tickets-table { .tickets-table {
margin-left: 3%; margin-left: 3%;
} }
@media only screen and (min-width: 999px) {
.cart-list {
width: 50%;
}
}
</style> </style>
<script> <script>

View File

@@ -14,6 +14,7 @@
<ul> <ul>
<li v-for="event in orderedEvents"> <li v-for="event in orderedEvents">
<router-link to="/tickets/details" class="ticket" @click="setActiveTicket( event.eventID );"> <router-link to="/tickets/details" class="ticket" @click="setActiveTicket( event.eventID );">
<img :src="event.logo" alt="event logo" class="ticket-logo">
<div class="ticket-name"> <div class="ticket-name">
<h3>{{ event.name }}</h3> <h3>{{ event.name }}</h3>
<p>{{ event.description }}</p> <p>{{ event.description }}</p>
@@ -23,7 +24,6 @@
<p>{{ event.locationName }}, {{ event.dateString }}</p> <p>{{ event.locationName }}, {{ event.dateString }}</p>
<h4>Starting at {{ event.currency }} {{ event.startingPrice }}</h4> <h4>Starting at {{ event.currency }} {{ event.startingPrice }}</h4>
</div> </div>
<img :src="event.logo" alt="event logo" class="ticket-logo">
</router-link> </router-link>
</li> </li>
</ul> </ul>
@@ -52,6 +52,7 @@
.ticket { .ticket {
display: flex; display: flex;
align-items: center; align-items: center;
flex-shrink: 0;
justify-content: center; justify-content: center;
text-decoration: none; text-decoration: none;
color: var( --primary-color ); color: var( --primary-color );
@@ -61,6 +62,14 @@
border-style: solid; border-style: solid;
padding: 10px; padding: 10px;
transition: 0.4s; transition: 0.4s;
flex-direction: column;
}
.ticket-logo {
height: 20vh;
width: auto;
min-width: 20vh;
margin-right: 3%;
} }
.ticket:hover { .ticket:hover {
@@ -68,20 +77,38 @@
transition: 0.4s; transition: 0.4s;
} }
.ticket-logo {
height: 20vh;
width: auto;
margin-left: auto;
}
.ticket-name { .ticket-name {
margin-right: auto; text-align: center;
max-width: 35%;
} }
.ticket-info { .ticket-info {
margin-left: auto; text-align: center;
margin-right: auto }
@media only screen and (min-width: 999px) {
.ticket {
flex-direction: row;
}
.ticket-logo {
height: 20vh;
width: auto;
min-width: 20vh;
margin-left: 3%;
}
.ticket-name {
flex-shrink: 0;
margin-right: 3%;
width: 40%;
text-align: justify;
}
.ticket-info {
margin-right: auto;
text-align: justify;
}
} }
</style> </style>

View File

@@ -14,59 +14,31 @@
<div v-if="!isAuthenticated" class="wrapper-buttons"> <div v-if="!isAuthenticated" class="wrapper-buttons">
<router-link to="/login" class="option-button" @click="setRedirect()">Log in with an existing account</router-link><br> <router-link to="/login" class="option-button" @click="setRedirect()">Log in with an existing account</router-link><br>
<router-link to="/signup" class="option-button" @click="setRedirect()">Create new account</router-link><br> <router-link to="/signup" class="option-button" @click="setRedirect()">Create new account</router-link><br>
<router-link to="/guest" v-if="!settings.accountRequired" class="option-button" @click="setRedirect()">Purchase as guest</router-link> <!-- <router-link to="/guest" v-if="!settings.accountRequired" class="option-button" @click="setRedirect()">Purchase as guest</router-link> -->
</div> </div>
<div v-else class="wrapper"> <div v-else class="wrapper">
<div class="data"> <p>Ready to buy? Please once again check that all the right items are in your cart.</p>
<h2>Purchase</h2> <div class="cart-list">
<p>Ready to buy? Please once again check that all the right items are in your cart.</p> <h2>Order summary</h2>
<!--<h2>Billing</h2> <h3>Your tickets</h3>
<table class="billing-info-table"> <div v-for="event in cart">
<tr v-if="settings.requiresAddress"> <h3>{{ event.displayName }}</h3>
<td>Street and house number</td> <table class="tickets-table">
<td><input type="text" name="street" id="street" v-bind="userData.street" placeholder="Street"> <input type="text" name="houseNumber" id="houseNumber" v-bind="userData.houseNumber" placeholder="House number"></td> <tr v-for="ticket in event.tickets">
</tr> <td>
<tr v-if="settings.requiresAddress"> <h4 class="price"><div style="display: inline;" v-if="ticket.count">{{ ticket.count }}x</div> {{ ticket.displayName }}: </h4>
<td>Zip Code and City</td> </td>
<td><input type="text" name="zip" id="zip" v-bind="userData.zip" placeholder="Zip Code"> <input type="text" name="city" id="city" v-bind="userData.city" placeholder="City"></td> <td>
</tr> {{ backend.currency }} {{ ticket.price }}
<tr v-if="settings.requiresAddress"> </td>
<td>Country</td> </tr>
<td><input type="text" name="country" id="country" v-bind="userData.zip" placeholder="Country"></td> </table>
</tr> </div>
<tr v-if="settings.requiresAge"> <div class="tool-wrapper wrapper-loggedIn">
<td>Birth date</td> <h4>Total: {{ backend.currency }} {{ backend.total }}</h4>
<td><input type="date" name="bday" id="bday" v-bind="userData.bday"></td>
</tr>
</table>
<div v-if="settings.requiresSpecialToken">
TODO: FUTURE: Implement
</div> -->
<button id="buy-button" @click="preparePayment();">Buy now</button>
</div>
<div class="cart">
<div class="cart-list">
<h2>Order summary</h2>
<h3>Your tickets</h3>
<div v-for="event in cart">
<h3>{{ event.displayName }}</h3>
<table class="tickets-table">
<tr v-for="ticket in event.tickets">
<td>
<h4 class="price"><div style="display: inline;" v-if="ticket.count">{{ ticket.count }}x</div> {{ ticket.displayName }}: </h4>
</td>
<td>
{{ backend.currency }} {{ ticket.price }}
</td>
</tr>
</table>
</div>
<div class="tool-wrapper wrapper-loggedIn">
<h4>Total: {{ backend.currency }} {{ backend.total }}</h4>
</div>
</div> </div>
</div> </div>
<button id="buy-button" @click="preparePayment();">Buy now</button>
</div> </div>
</div> </div>
<div v-else> <div v-else>
@@ -111,10 +83,6 @@
.purchase-app { .purchase-app {
text-align: justify; text-align: justify;
width: 100%; width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-grow: 1;
} }
@@ -124,36 +92,20 @@
border-radius: 20px; border-radius: 20px;
padding: 6% 7%; padding: 6% 7%;
display: block; display: block;
width: 100%; width: 60%;
text-align: center; text-align: center;
margin: 0.5%; margin: 0.5%;
color: var( --primary-color ); color: var( --primary-color );
text-decoration: none; text-decoration: none;
} }
.data {
grid-area: main;
display: flex;
justify-content: justify;
align-items: center;
flex-direction: column;
flex-grow: 1;
}
.option-button:hover { .option-button:hover {
background-color: var( --hover-color ); background-color: var( --hover-color );
color: var( --secondary-color ) color: var( --secondary-color )
} }
.cart {
grid-area: sidebar;
background-color: var( --accent-background );
color: var( --secondary-color );
overflow: scroll;
}
.wrapper-buttons { .wrapper-buttons {
width: 40%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
@@ -163,25 +115,15 @@
.wrapper { .wrapper {
width: 100%; width: 100%;
display: grid; display: flex;
height: 100%; flex-direction: column;
grid-template-areas: align-items: center;
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar';
} }
.cart-list { .cart-list {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center; align-items: center;
} }