Merge pull request #13 from Faraphel/menu-v1

Liste de cartes
This commit is contained in:
Faraphel 2024-01-02 14:49:36 +01:00 committed by GitHub
commit 46de8a3db5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1122 additions and 88 deletions

12
.gitignore vendored
View file

@ -1,15 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
local.properties
.idea
.DS_Store
/build
/captures
build
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore vendored
View file

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View file

@ -1 +0,0 @@
Palto

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
</component>
</project>

View file

@ -1,6 +0,0 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -30,14 +30,23 @@ android {
jvmTarget = "1.8"
}
buildToolsVersion = "33.0.1"
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
implementation("androidx.annotation:annotation:1.7.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.recyclerview:recyclerview:1.3.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.NFC" />
@ -16,14 +17,20 @@
android:theme="@style/Theme.Palto"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true" >
android:name=".PaltoActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>

View file

@ -1,48 +1,50 @@
package com.example.palto
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.nfc.tech.NdefFormatable
import android.nfc.tech.NfcA
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.viewModels
class MainActivity : AppCompatActivity() {
class PaltoActivity : AppCompatActivity() {
private var nfcAdapter: NfcAdapter? = null
private val paltoViewModel: PaltoViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// get the NFC Adapter
this.nfcAdapter = NfcAdapter.getDefaultAdapter(this)
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
// check if NFC is supported
if (this.nfcAdapter == null) {
if (nfcAdapter == null) {
Log.e("NFC", "NFC is not supported")
return
}
// check if NFC is disabled
if (!(this.nfcAdapter!!.isEnabled)) {
if (nfcAdapter?.isEnabled == false) {
Log.w("NFC", "NFC is not enabled")
}
setContentView(R.layout.activity_palto)
/*
val url = URL("https://www.faraphel.fr/palto/api/auth/token/")
val connection = url.openConnection()
val auth_data = Json.decodeFromString(connection.content)
*/
}
override fun onResume() {
super.onResume()
nfcAdapter!!.enableReaderMode(
nfcAdapter?.enableReaderMode(
this,
this::processTag,
paltoViewModel.tagLiveData::postValue,
NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
null
)
@ -52,11 +54,11 @@ class MainActivity : AppCompatActivity() {
super.onPause()
// disable the NFC discovery
this.nfcAdapter!!.disableReaderMode(this)
nfcAdapter?.disableReaderMode(this)
}
@OptIn(ExperimentalStdlibApi::class)
fun processTag(tag: Tag) {
Log.d("NFC", "Tag ID : ${tag.id.toHexString()}")
Log.d("NFC", "Tag ID : " + tag.id.toHexString())
}
}

View file

@ -0,0 +1,9 @@
package com.example.palto
import android.nfc.Tag
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class PaltoViewModel: ViewModel() {
val tagLiveData = MutableLiveData<Tag>()
}

View file

@ -0,0 +1,18 @@
package com.example.palto.data
/**
* A generic class that holds a value with its loading status.
* @param <T>
*/
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
}
}
}

View file

@ -0,0 +1,24 @@
package com.example.palto.data.local
/**
* Class that handles authentication w/ login credentials and retrieves user information.
*/
class LocalDataSource {
/*
fun login(username: String, password: String): Result<LoggedInUser> {
try {
val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")
return Result.Success(fakeUser)
} catch (e: Throwable) {
return Result.Error(IOException("Error logging in", e))
}
}
fun logout() {
// TODO: revoke authentication
}
*/
}

View file

@ -0,0 +1,60 @@
package com.example.palto.data.network
import com.example.palto.data.Result
import com.example.palto.model.LoggedInUser
import com.example.palto.model.Tokens
import java.io.IOException
import java.util.UUID
/**
* Class that handles API calls.
*/
class ServerDataSource {
private var hostname: String? = null
fun requestToken(
hostname: String,
username: String,
password: String
): Result<Tokens> {
try {
val tokens = Tokens(
refresh = "aa",
access = "bb"
)
return Result.Success(tokens)
} catch (e: Throwable) {
return Result.Error(IOException("Error logging in", e))
}
}
fun refreshToken(current_tokens: Tokens): Result<Tokens> {
return Result.Success(current_tokens)
}
fun verifyToken(): Boolean {
return true
}
fun login(
hostname: String,
username: String,
password: String
): Result<LoggedInUser> {
try {
val fakeUser = LoggedInUser(
UUID.randomUUID().toString(),
"dede",
"Lucie",
"Doe",
"aa@free.fr",
)
return Result.Success(fakeUser)
} catch (e: Throwable) {
return Result.Error(IOException("Error logging in", e))
}
}
fun logout() { }
}

View file

@ -0,0 +1,10 @@
package com.example.palto.data.repository
import com.example.palto.data.network.ServerDataSource
/**
*
*/
class AttendanceRepository(val dataSource: ServerDataSource) {
// private val cards
}

View file

@ -0,0 +1,47 @@
package com.example.palto.data.repository
import com.example.palto.data.Result
import com.example.palto.data.network.ServerDataSource
import com.example.palto.model.LoggedInUser
/**
* Class that requests authentication and user information from the remote data source and
* maintains an in-memory cache of login status and user credentials information.
*/
class LoginRepository(val dataSource: ServerDataSource) {
var user: LoggedInUser? = null
private set
val isLoggedIn: Boolean
get() = user != null
init {
user = null
}
fun logout() {
user = null
dataSource.logout()
}
fun login(
hostname: String,
username: String,
password: String
): Result<LoggedInUser> {
// handle login
val result = dataSource.login(hostname, username, password)
if (result is Result.Success) {
setLoggedInUser(result.data)
}
return result
}
private fun setLoggedInUser(loggedInUser: LoggedInUser) {
this.user = loggedInUser
}
}

View file

@ -0,0 +1,10 @@
package com.example.palto.data.repository
import com.example.palto.data.network.ServerDataSource
/**
*
*/
class SessionRepository(val dataSource: ServerDataSource) {
// private val cards
}

View file

@ -0,0 +1,43 @@
package com.example.palto.data.repository
import com.example.palto.data.network.ServerDataSource
import com.example.palto.model.Tokens
/**
* Class that requests authentication and user information from the remote data source and
* maintains an in-memory cache of login status and user credentials information.
*/
class TokensRepository(val dataSource: ServerDataSource) {
var tokens: Tokens? = null
private set
/*
val isLoggedIn: Boolean
get() = user != null
*/
init {
// If user credentials will be cached in local storage, it is recommended it be encrypted
// @see https://developer.android.com/training/articles/keystore
tokens = null
}
/*
fun login(username: String, password: String): Result<LoggedInUser> {
// handle login
val result = dataSource.login(username, password)
if (result is Result.Success) {
setLoggedInUser(result.data)
}
return result
}
*/
private fun setTokens(tokens: Tokens) {
this.tokens = tokens
}
}

View file

@ -0,0 +1,10 @@
package com.example.palto.model
import java.io.Serializable
/**
* Data class that captures tokens for logged in users retrieved from LoginRepository
*/
data class Attendance(
val date: String,
val access: String
) : Serializable

View file

@ -0,0 +1,23 @@
package com.example.palto.model
import java.io.Serializable
data class Card(
val id: String,
val uid: ByteArray,
val department: String,
val owner: String
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Card
return uid.contentEquals(other.uid)
}
override fun hashCode(): Int {
return uid.contentHashCode()
}
}

View file

@ -0,0 +1,14 @@
package com.example.palto.model
import java.io.Serializable
/**
* Data class that captures user information for logged in users retrieved from LoginRepository
*/
data class LoggedInUser(
val id: String,
val username: String,
val first_name: String,
val last_name: String,
val email: String
) : Serializable

View file

@ -0,0 +1,9 @@
package com.example.palto.model
import java.io.Serializable
/**
* Data class that captures tokens for logged in users retrieved from LoginRepository
*/
data class Session(
val id: String
) : Serializable

View file

@ -0,0 +1,10 @@
package com.example.palto.model
import java.io.Serializable
/**
* Data class that captures tokens for logged in users retrieved from LoginRepository
*/
data class Tokens(
val refresh: String,
val access: String
) : Serializable

View file

@ -0,0 +1,58 @@
package com.example.palto.ui.attendanceList
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.palto.databinding.FragmentAttendanceItemBinding
import com.example.palto.model.Card
/**
*
*/
class AttendanceListAdapter :
ListAdapter<Card, AttendanceListAdapter.ViewHolder>(CardDiffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
return ViewHolder(
FragmentAttendanceItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
@OptIn(ExperimentalStdlibApi::class)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.cardId.text = item.uid.toHexString()
//holder.contentView.text = item.content
}
inner class ViewHolder(
binding: FragmentAttendanceItemBinding
) : RecyclerView.ViewHolder(binding.root) {
val cardId: TextView = binding.cardId
override fun toString(): String {
return super.toString() + " '" + cardId.text + "'"
}
}
}
object CardDiffCallback : DiffUtil.ItemCallback<Card>() {
override fun areItemsTheSame(oldItem: Card, newItem: Card): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Card, newItem: Card): Boolean {
return oldItem.id == newItem.id
}
}

View file

@ -0,0 +1,69 @@
package com.example.palto.ui.attendanceList
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.navigation.navGraphViewModels
import com.example.palto.PaltoViewModel
import com.example.palto.R
import com.example.palto.databinding.FragmentAttendanceListBinding
/**
* A fragment representing a list of attendances.
*/
class AttendanceListFragment : Fragment() {
private val attendanceListViewModel: AttendanceListViewModel by
navGraphViewModels(R.id.nav_graph) { AttendanceListViewModel.Factory }
private val paltoViewModel: PaltoViewModel by
activityViewModels()
private var _binding: FragmentAttendanceListBinding? = null
// This property is only valid between onCreateView and onDestroyView
private val binding get() = _binding!!
/**
* Only inflate the view.
*/
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAttendanceListBinding.inflate(inflater, container, false)
return binding.root
}
/**
* Logic on the returned view of onCreateView.
*/
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
// Set the adapter of the view for managing automatically the list of items on the screen.
val adapter = AttendanceListAdapter()
binding.list.adapter = adapter
attendanceListViewModel.cardsLiveData.observe(viewLifecycleOwner) {
Log.d("NFC", "A card has been had to the list")
adapter.submitList(it)
}
// Set the listener for a new NFC tag.
paltoViewModel.tagLiveData.observe(viewLifecycleOwner) {
Log.d("NFC", "tag observer has been notified")
attendanceListViewModel.insertCard(it)
}
}
}

View file

@ -0,0 +1,49 @@
package com.example.palto.ui.attendanceList
import android.nfc.Tag
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.palto.data.network.ServerDataSource
import com.example.palto.data.repository.AttendanceRepository
import com.example.palto.model.Card
/**
* ViewModel of a session which has a list of attendances.
*/
class AttendanceListViewModel(
private val attendanceRepository: AttendanceRepository
) : ViewModel() {
val cardsLiveData: MutableLiveData<List<Card>> = MutableLiveData(emptyList())
fun insertCard(tag: Tag) {
val card = Card(
"0",
tag.id,
"tmp",
"tmp"
)
cardsLiveData.value = (cardsLiveData.value ?: emptyList()) + card
Log.d("NFC", "view model: A card has been had to the list")
}
/**
* ViewModel Factory.
*/
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>
): T {
return AttendanceListViewModel(
AttendanceRepository(ServerDataSource())
) as T
}
}
}
}

View file

@ -0,0 +1,13 @@
package com.example.palto.ui.login
/* Est-ce que cest util ?
* Updater la vue dans le fragment
*/
/**
* User details post authentication that is exposed to the UI
*/
data class LoggedInUserView(
val displayName: String
//... other data fields that may be accessible to the UI
)

View file

@ -0,0 +1,11 @@
package com.example.palto.ui.login
/**
* Data validation state of the login form.
*/
data class LoginFormState(
val hostnameError: Int? = null,
val usernameError: Int? = null,
val passwordError: Int? = null,
val isDataValid: Boolean = false
)

View file

@ -0,0 +1,136 @@
package com.example.palto.ui.login
import androidx.lifecycle.Observer
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.example.palto.databinding.FragmentLoginBinding
import com.example.palto.R
class LoginFragment : Fragment() {
private val loginViewModel: LoginViewModel by
navGraphViewModels(R.id.nav_graph) { LoginViewModelFactory() }
private var _binding: FragmentLoginBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val hostnameEditText = binding.hostname
val usernameEditText = binding.username
val passwordEditText = binding.password
val loginButton = binding.login
val loadingProgressBar = binding.loading
//
loginViewModel.loginFormState.observe(viewLifecycleOwner,
Observer { loginFormState ->
if (loginFormState == null) {
return@Observer
}
loginButton.isEnabled = loginFormState.isDataValid
loginFormState.hostnameError?.let {
hostnameEditText.error = getString(it)
}
loginFormState.usernameError?.let {
usernameEditText.error = getString(it)
}
loginFormState.passwordError?.let {
passwordEditText.error = getString(it)
}
})
loginViewModel.loginResult.observe(viewLifecycleOwner,
Observer { loginResult ->
loginResult ?: return@Observer
loadingProgressBar.visibility = View.GONE
loginResult.error?.let {
showLoginFailed(it)
}
loginResult.success?.let {
//findNavController().navigate(R.id.action_loginFragment_to_attendanceFragment)
//updateUiWithUser(it)
}
})
val afterTextChangedListener = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(s: Editable) {
loginViewModel.loginDataChanged(
hostnameEditText.text.toString(),
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
}
}
hostnameEditText.addTextChangedListener(afterTextChangedListener)
usernameEditText.addTextChangedListener(afterTextChangedListener)
passwordEditText.addTextChangedListener(afterTextChangedListener)
passwordEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
loginViewModel.login(
hostnameEditText.text.toString(),
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
}
false
}
// Damien : Le setOnClickListener est là !
loginButton.setOnClickListener {
loadingProgressBar.visibility = View.VISIBLE
loginViewModel.login(
hostnameEditText.text.toString(),
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
}
}
/*
private fun updateUiWithUser(model: LoggedInUserView) {
val welcome = getString(R.string.welcome) + model.displayName
// TODO : initiate successful logged in experience
val appContext = context?.applicationContext ?: return
Toast.makeText(appContext, welcome, Toast.LENGTH_LONG).show()
}
*/
private fun showLoginFailed(@StringRes errorString: Int) {
val appContext = context?.applicationContext ?: return
Toast.makeText(appContext, errorString, Toast.LENGTH_LONG).show()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,9 @@
package com.example.palto.ui.login
/**
* Authentication result : success (user details) or error message.
*/
data class LoginResult(
val success: LoggedInUserView? = null,
val error: Int? = null
)

View file

@ -0,0 +1,84 @@
package com.example.palto.ui.login
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import android.util.Patterns
import androidx.lifecycle.ViewModelProvider
import com.example.palto.data.repository.LoginRepository
import com.example.palto.R
import com.example.palto.data.network.ServerDataSource
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
private val _loginForm = MutableLiveData<LoginFormState>()
val loginFormState: LiveData<LoginFormState> = _loginForm
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult
fun login(
hostname: String,
username: String,
password: String) {
// can be launched in a separate asynchronous job
val result = loginRepository.login(hostname, username, password)
/*
if (result is Result.Success) {
_loginResult.value =
LoginResult(success = LoggedInUserView(
displayName = result.data.displayName))
} else {
_loginResult.value = LoginResult(error = R.string.login_failed)
}
*/
}
fun loginDataChanged(
hostname: String,
username: String,
password: String) {
if (!isHostNameValid(hostname)) {
_loginForm.value = LoginFormState(hostnameError = R.string.invalid_hostname)
} else if (!isUserNameValid(username)) {
_loginForm.value = LoginFormState(usernameError = R.string.invalid_username)
} else if (!isPasswordValid(password)) {
_loginForm.value = LoginFormState(passwordError = R.string.invalid_password)
} else {
_loginForm.value = LoginFormState(isDataValid = true)
}
}
private fun isHostNameValid(hostname: String): Boolean {
return hostname.isNotBlank()
}
// A placeholder username validation check
private fun isUserNameValid(username: String): Boolean {
return if (username.contains("@")) {
Patterns.EMAIL_ADDRESS.matcher(username).matches()
} else {
username.isNotBlank()
}
}
// A placeholder password validation check
private fun isPasswordValid(password: String): Boolean {
return password.length > 5
}
}
class LoginViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
return LoginViewModel(
loginRepository = LoginRepository(
dataSource = ServerDataSource()
)
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -0,0 +1,44 @@
package com.example.palto.ui.sessionList
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import com.example.palto.ui.sessionList.placeholder.PlaceholderContent.PlaceholderItem
import com.example.palto.databinding.FragmentSessionItemBinding
/**
* [RecyclerView.Adapter] that can display a [PlaceholderItem].
*/
class SessionListAdapter(private val values: List<PlaceholderItem>) :
RecyclerView.Adapter<SessionListAdapter.ViewHolder>() {
class ViewHolder(binding: FragmentSessionItemBinding) :
RecyclerView.ViewHolder(binding.root) {
val idView: TextView = binding.itemNumber
val contentView: TextView = binding.content
override fun toString(): String {
return super.toString() + " '" + contentView.text + "'"
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = FragmentSessionItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = values[position]
holder.idView.text = item.id
holder.contentView.text = item.content
}
override fun getItemCount(): Int = values.size
}

View file

@ -0,0 +1,31 @@
package com.example.palto.ui.sessionList
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.palto.R
import com.example.palto.ui.sessionList.placeholder.PlaceholderContent
/**
* A fragment representing a list of Sessions.
*/
class SessionListFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_session_list, container, false)
if (view is RecyclerView) {
view.layoutManager = LinearLayoutManager(context)
view.adapter = SessionListAdapter(PlaceholderContent.ITEMS)
}
return view
}
}

View file

@ -0,0 +1,14 @@
package com.example.palto.ui.sessionList
import androidx.lifecycle.ViewModel
import com.example.palto.data.repository.LoginRepository
class SessionListViewModel(private val loginRepository: LoginRepository) : ViewModel() {
/*
private val _loginForm = MutableLiveData<LoginFormState>()
val loginFormState: LiveData<LoginFormState> = _loginForm
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult
*/
}

View file

@ -0,0 +1,57 @@
package com.example.palto.ui.sessionList.placeholder
import java.util.ArrayList
import java.util.HashMap
/**
* Helper class for providing sample content for user interfaces created by
* Android template wizards.
*
* TODO: Replace all uses of this class before publishing your app.
*/
object PlaceholderContent {
/**
* An array of sample (placeholder) items.
*/
val ITEMS: MutableList<PlaceholderItem> = ArrayList()
/**
* A map of sample (placeholder) items, by ID.
*/
val ITEM_MAP: MutableMap<String, PlaceholderItem> = HashMap()
private val COUNT = 25
init {
// Add some sample items.
for (i in 1..COUNT) {
addItem(createPlaceholderItem(i))
}
}
private fun addItem(item: PlaceholderItem) {
ITEMS.add(item)
ITEM_MAP.put(item.id, item)
}
private fun createPlaceholderItem(position: Int): PlaceholderItem {
return PlaceholderItem(position.toString(), "Item " + position, makeDetails(position))
}
private fun makeDetails(position: Int): String {
val builder = StringBuilder()
builder.append("Details about Item: ").append(position)
for (i in 0..position - 1) {
builder.append("\nMore details information here.")
}
return builder.toString()
}
/**
* A placeholder item representing a piece of content.
*/
data class PlaceholderItem(val id: String, val content: String, val details: String) {
override fun toString(): String = content
}
}

View file

@ -3,17 +3,17 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
android:layout_height="match_parent">
<TextView
android:id="@+id/test_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/card_̤id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list"
android:name="com.example.palto.ui.attendanceList.AttendanceFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".ui.attendanceList.AttendanceListFragment"
tools:listitem="@layout/fragment_attendance_item" />

View file

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/fragment_horizontal_margin"
android:paddingTop="@dimen/fragment_vertical_margin"
android:paddingRight="@dimen/fragment_horizontal_margin"
android:paddingBottom="@dimen/fragment_vertical_margin"
tools:context=".ui.login.LoginFragment">
<EditText
android:id="@+id/hostname"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="64dp"
android:layout_marginEnd="24dp"
android:autofillHints="@string/prompt_hostname"
android:hint="@string/prompt_hostname"
android:inputType="text"
android:selectAllOnFocus="true"
app:layout_constraintBottom_toTopOf="@+id/username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:autofillHints="@string/prompt_username"
android:hint="@string/prompt_username"
android:inputType="text"
android:selectAllOnFocus="true"
app:layout_constraintBottom_toTopOf="@+id/password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/hostname" />
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:autofillHints="@string/prompt_password"
android:hint="@string/prompt_password"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:selectAllOnFocus="true"
app:layout_constraintBottom_toTopOf="@+id/help_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" />
<TextView
android:id="@+id/help_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/help_message"
app:layout_constraintBottom_toTopOf="@+id/login"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password" />
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="48dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="48dp"
android:layout_marginBottom="64dp"
android:enabled="false"
android:text="@string/action_sign_in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/help_message"
app:layout_constraintVertical_bias="0.2" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginTop="64dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="64dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/password"
app:layout_constraintStart_toStartOf="@+id/password"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/item_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
<TextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list"
android:name="com.example.palto.ui.session.SessionListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".ui.sessionList.SessionListFragment"
tools:listitem="@layout/fragment_session_item" />

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/attendanceListFragment">
<fragment
android:id="@+id/loginFragment"
android:name="com.example.palto.ui.login.LoginFragment"
android:label="fragment._login"
tools:layout="@layout/fragment_login" />
<fragment
android:id="@+id/sessionListFragment"
android:name="com.example.palto.ui.sessionList.SessionListFragment"
android:label="fragment_session_list"
tools:layout="@layout/fragment_session_list" />
<fragment
android:id="@+id/attendanceListFragment"
android:name="com.example.palto.ui.attendanceList.AttendanceListFragment"
android:label="fragment_attendance_list"
tools:layout="@layout/fragment_attendance_list" />
</navigation>

View file

@ -0,0 +1,6 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="fragment_horizontal_margin">16dp</dimen>
<dimen name="fragment_vertical_margin">16dp</dimen>
<dimen name="text_margin">16dp</dimen>
</resources>

View file

@ -1,3 +1,15 @@
<resources>
<string name="app_name">Palto</string>
<!-- Strings related to login -->
<string name="prompt_hostname">Serveur</string>
<string name="prompt_username">Nom dutilisateur</string>
<string name="prompt_password">Mot de passe</string>
<string name="action_sign_in">Connexion</string>
<string name="action_sign_in_short">Sign in</string>
<string name="welcome">"Bienvenue !"</string>
<string name="invalid_hostname">Serveur inaccessible</string>
<string name="invalid_username">Nom dutilisateur non valide</string>
<string name="invalid_password">Mot de passe invalide</string>
<string name="login_failed">"Erreur de connexion !"</string>
<string name="help_message">Identifiants Invalides</string>
</resources>

View file

@ -1,5 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.1.2" apply false
id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
}