Merge pull request #14 from Faraphel/menu-v1

Menu v1
This commit is contained in:
biloute02 2024-01-15 21:21:14 +01:00 committed by GitHub
commit 3b88177aa7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1283 additions and 837 deletions

View file

@ -1 +1,24 @@
# M1-Projet
Cette application est la partie cliente du projet Palto.
## Fonctionnalités
- Se connecter sur un serveur Palto avec ses identifiants, ou bien se connecter en anonyme.
- Sur le menu :
- Voir la liste des sessions créées.
- Créer une nouvelle session en lui donnant un nom.
- Ouvrir une session en cliquant sur litem.
- Lorsquune session est ouverte :
- Voir la list des présences.
- Scanner une carte NFC et créer un nouvel étudiant associé à cette carte.
- Ajouter manuellement un étudiant à la liste.
## Prérequis
- Android 8.1 SDK 27.
- Support du NFC
## Utilisation
Importer le projet dans Android Studio.

View file

@ -29,7 +29,6 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
buildToolsVersion = "33.0.1"
buildFeatures {
viewBinding = true
}
@ -38,16 +37,21 @@ android {
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("com.google.android.material:material:1.11.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.navigation:navigation-fragment-ktx:2.7.6")
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
implementation("androidx.annotation:annotation:1.7.1")
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")
implementation("androidx.recyclerview:recyclerview:1.3.2")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
// Retrofit and Moshi for API requests.
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.moshi:moshi-kotlin:1.13.0")
}

View file

@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
@ -15,6 +16,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Palto"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".PaltoActivity"

View file

@ -0,0 +1,22 @@
package com.example.palto
import android.nfc.Tag
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.palto.domain.Card
import com.example.palto.domain.Event
import com.example.palto.domain.User
class NfcViewModel: ViewModel() {
private var _tagId = MutableLiveData<Event<String>>()
val tagId: LiveData<Event<String>> = _tagId
@OptIn(ExperimentalStdlibApi::class)
fun setTag(tag: Tag) {
Log.d("Nfc", "A new tag has been set.")
_tagId.postValue(Event(tag.id.toHexString()))
}
}

View file

@ -1,64 +1,96 @@
package com.example.palto
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuInflater
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import com.example.palto.databinding.ActivityPaltoBinding
import com.example.palto.ui.CardViewModel
class PaltoActivity : AppCompatActivity() {
private var nfcAdapter: NfcAdapter? = null
private val paltoViewModel: PaltoViewModel by viewModels()
private val nfcViewModel: NfcViewModel by viewModels()
private lateinit var binding: ActivityPaltoBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// get the NFC Adapter
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
binding = ActivityPaltoBinding.inflate(layoutInflater)
setContentView(binding.root)
// check if NFC is supported
//
// Toolbar
//
// Set the toolbar as the app bar for the activity.
setSupportActionBar(binding.paltoToolbar)
// Configure the app bar
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.palto_nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.menuFragment, R.id.loginFragment
)
)
binding.paltoToolbar.setupWithNavController(navController, appBarConfiguration)
//
// NFC Adapter
//
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
// Check if NFC is supported (already checked in the app manifest).
if (nfcAdapter == null) {
Log.e("NFC", "NFC is not supported")
return
}
// check if NFC is disabled
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)
*/
}
/**
* Specify the options menu for the Activity.
*/
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val inflater: MenuInflater = menuInflater
inflater.inflate(R.menu.palto_menu, menu)
return true
}
/**
* Just before the application is displayed.
*/
override fun onResume() {
super.onResume()
// Begin to read NFC Cards.
nfcAdapter?.enableReaderMode(
this,
paltoViewModel.tagLiveData::postValue,
nfcViewModel::setTag,
NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
null
)
}
/**
* Just after the application has been quit.
*/
override fun onPause() {
super.onPause()
// disable the NFC discovery
// Disable the NFC discovery.
nfcAdapter?.disableReaderMode(this)
}
@OptIn(ExperimentalStdlibApi::class)
fun processTag(tag: Tag) {
Log.d("NFC", "Tag ID : " + tag.id.toHexString())
}
}

View file

@ -1,9 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,24 +0,0 @@
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,46 @@
package com.example.palto.data.network
import com.example.palto.data.network.model.UserCredentials
import com.example.palto.domain.Tokens
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.Body
import retrofit2.http.POST
/**
* A public Api object that exposes the lazy-initialized Retrofit service
*/
object PaltoApi {
// Build the Moshi object that Retrofit will be using, making sure to add the Kotlin adapter for
// full Kotlin compatibility.
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
fun createService(url: String) {
// Use the Retrofit builder to build a retrofit object using a Moshi converter
// with our Moshi object.
val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(url)
.build()
retrofitService = retrofit.create(PaltoApiService::class.java)
}
// Retrofit service that Palto will use to do requests.
// Make sure to call createService once before using it.
lateinit var retrofitService: PaltoApiService
}
/**
* Functions to query the API.
*/
interface PaltoApiService {
@POST("api/auth/jwt/token/")
suspend fun getTokens(@Body userCredentials: UserCredentials): Tokens
}

View file

@ -1,60 +0,0 @@
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,6 @@
package com.example.palto.data.network.model
data class UserCredentials(
val username: String,
val password: String
)

View file

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

View file

@ -1,47 +0,0 @@
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

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

View file

@ -0,0 +1,23 @@
package com.example.palto.data.repository
import com.example.palto.data.network.PaltoApi
import com.example.palto.data.network.model.UserCredentials
import com.example.palto.domain.Tokens
/**
* Class that requests authentication tokens from Palto server.
*/
class TokenRepository() {
private var tokens: Tokens? = null
suspend fun authenticate(
hostname: String,
username: String,
password: String
) {
PaltoApi.createService("http://$hostname:8000/")
val tokens = PaltoApi.retrofitService.getTokens((UserCredentials(username, password)))
this.tokens = tokens
}
}

View file

@ -1,43 +0,0 @@
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,3 @@
package com.example.palto.data.repository
class UserRepository

View file

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

View file

@ -0,0 +1,7 @@
package com.example.palto.domain
data class Card(
val id: Int,
val tagId: String,
val user: User,
)

View file

@ -0,0 +1,18 @@
package com.example.palto.domain
class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun peekContent(): T = content
}

View file

@ -0,0 +1,12 @@
package com.example.palto.domain
import java.io.Serializable
/**
* Data class that captures tokens for logged in users retrieved from LoginRepository
*/
data class Session(
val id: Int,
val name: String,
var attendances: List<Attendance>
// When the list is updated, it is replaced by a new one.
)

View file

@ -1,4 +1,4 @@
package com.example.palto.model
package com.example.palto.domain
import java.io.Serializable
/**
@ -7,4 +7,4 @@ import java.io.Serializable
data class Tokens(
val refresh: String,
val access: String
) : Serializable
)

View file

@ -0,0 +1,12 @@
package com.example.palto.domain
/**
* Data class that captures user information for logged in users retrieved from LoginRepository
*/
data class User(
val id: Int,
val username: String,
val firstName: String,
val lastName: String,
val email: String
)

View file

@ -1,23 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,9 +0,0 @@
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,34 @@
package com.example.palto.ui
import android.nfc.Tag
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.palto.domain.Card
import com.example.palto.domain.User
/**
* CardViewModel maintain a list of cards application wide.
* May be converted in a repository.
*/
class CardViewModel: ViewModel() {
private var _cards = MutableLiveData<List<Card>>()
private val cards: LiveData<List<Card>> = _cards
fun createCard(user: User, tagId: String): Card {
val list = _cards.value ?: emptyList()
val card = Card(list.size, tagId, user)
_cards.value = list + card
Log.d("Palto", "CardViewModel: a card has been added into the list.")
return card
}
fun getCard(tagId: String): Card? {
val card = _cards.value?.find() {
it.tagId == tagId
}
return card
}
}

View file

@ -0,0 +1,31 @@
package com.example.palto.ui
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.palto.domain.Card
import com.example.palto.domain.User
/**
* UserViewModel maintain a list of users application wide.
* May be converted into a repository.
*/
class UserViewModel: ViewModel() {
private var _users = MutableLiveData<List<User>>()
val users : LiveData<List<User>> = _users
fun createUser(username: String): User {
val list = _users.value ?: emptyList()
val user = User(
id = list.size,
username = username,
firstName = "",
lastName = "",
email = "")
_users.value = list + user
Log.d("Palto", "UserViewModel: a user has been added into the list.")
return user
}
}

View file

@ -1,58 +0,0 @@
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

@ -1,69 +0,0 @@
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

@ -1,49 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,42 +1,62 @@
package com.example.palto.ui.login
import androidx.lifecycle.Observer
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import android.annotation.SuppressLint
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.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
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() }
// userViewModel is where the user is logged in, at the activity level.
private val loginViewModel: LoginViewModel by activityViewModels() { LoginViewModel.Factory }
private var _binding: FragmentLoginBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
private lateinit var binding: FragmentLoginBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLoginBinding.inflate(inflater, container, false)
val navController = findNavController()
// Bind the login button.
binding.login.setOnClickListener {
binding.loading.visibility = View.VISIBLE
loginViewModel.login(
binding.hostname.text.toString(),
binding.username.text.toString(),
binding.password.text.toString()
)
}
// Bind anonymous login clickable text.
binding.loginAnonymous.setOnClickListener {
loginViewModel.loginAnonymous()
}
// On result of logging.
loginViewModel.result.observe(viewLifecycleOwner) {
binding.loading.visibility = View.GONE
if (it.success) {
navController.popBackStack()
} else if (it.error != null) {
binding.loginError.text = "Exception : ${it.exception.toString()}"
Toast.makeText(activity, it.error, Toast.LENGTH_LONG).show()
}
}
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
/*
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -46,23 +66,21 @@ class LoginFragment : Fragment() {
val loginButton = binding.login
val loadingProgressBar = binding.loading
//
loginViewModel.loginFormState.observe(viewLifecycleOwner,
Observer { loginFormState ->
if (loginFormState == null) {
loginViewModel.loginFormState.observe(viewLifecycleOwner) {
if (it == null) {
return@Observer
}
loginButton.isEnabled = loginFormState.isDataValid
loginFormState.hostnameError?.let {
loginButton.isEnabled = it.isDataValid
it.hostnameError?.let {
hostnameEditText.error = getString(it)
}
loginFormState.usernameError?.let {
it.usernameError?.let {
usernameEditText.error = getString(it)
}
loginFormState.passwordError?.let {
it.passwordError?.let {
passwordEditText.error = getString(it)
}
})
}
loginViewModel.loginResult.observe(viewLifecycleOwner,
Observer { loginResult ->
@ -103,17 +121,8 @@ class LoginFragment : Fragment() {
}
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) {
@ -122,15 +131,11 @@ class LoginFragment : Fragment() {
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

@ -1,9 +1,10 @@
package com.example.palto.ui.login
/**
* Authentication result : success (user details) or error message.
* Authentication result : success is true if connected or error message with exception.
*/
data class LoginResult(
val success: LoggedInUserView? = null,
val error: Int? = null
val success: Boolean,
val error: Int? = null, // Id of the string resource to display to the user.
val exception: Exception? = null
)

View file

@ -1,41 +1,70 @@
package com.example.palto.ui.login
import android.util.Log
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 androidx.lifecycle.viewModelScope
import com.example.palto.R
import com.example.palto.data.network.ServerDataSource
import com.example.palto.data.repository.TokenRepository
import com.example.palto.data.repository.UserRepository
import com.example.palto.domain.User
import kotlinx.coroutines.launch
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
/**
* LoginViewModel to access information about the logged in user and login form.
*/
class LoginViewModel(
private val tokenRepository: TokenRepository,
private val userRepository: UserRepository
): ViewModel() {
private val _loginForm = MutableLiveData<LoginFormState>()
val loginFormState: LiveData<LoginFormState> = _loginForm
private var _result = MutableLiveData<LoginResult>()
val result = _result as LiveData<LoginResult>
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult
// User is initially set to null to be disconnected.
private var _user = MutableLiveData<User?>(null)
val user = _user as LiveData<User?>
/*
private val _loginFormState = MutableLiveData<LoginFormState>()
val loginFormState: LiveData<LoginFormState> = _loginFormState
*/
fun login(
hostname: String,
username: String,
password: String) {
// can be launched in a separate asynchronous job
val result = loginRepository.login(hostname, username, password)
// Coroutine runs in background.
viewModelScope.launch {
try {
tokenRepository.authenticate(hostname, username, password)
_user.value = User(-1, username, "", "", "")
_result.value = LoginResult(success = true)
} catch (e: Exception) {
Log.e("Palto", "Connection error: " + e.message)
_result.value = LoginResult(
success = false,
error = R.string.login_failed,
exception = e
)
}
}
}
fun loginAnonymous() {
_user.value = User(-2, "anonymous", "", "", "")
_result.value = LoginResult(success = true)
}
fun logout() {
_user.value = null
_result.value = LoginResult(success = false)
}
/*
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,
@ -50,35 +79,28 @@ class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel()
_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()
}
return username.isNotBlank()
}
// A placeholder password validation check
private fun isPasswordValid(password: String): Boolean {
return password.length > 5
}
}
*/
class LoginViewModelFactory : ViewModelProvider.Factory {
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
return LoginViewModel(
loginRepository = LoginRepository(
dataSource = ServerDataSource()
)
tokenRepository = TokenRepository(),
userRepository = UserRepository()
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}

View file

@ -0,0 +1,58 @@
package com.example.palto.ui.menu
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.FragmentMenuItemBinding
import com.example.palto.domain.Session
/**
* A [ListAdapter] that can display [Session] items.
*/
class MenuAdapter(private val onClick: (Session) -> Unit) :
ListAdapter<Session, MenuAdapter.ViewHolder>(SessionDiffCallback) {
inner class ViewHolder(binding: FragmentMenuItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private val sessionNameText: TextView = binding.sessionName
private var currentSession: Session? = null
init {
binding.root.setOnClickListener {
currentSession?.let {
onClick(it)
}
}
}
fun bind(session: Session) {
currentSession = session
sessionNameText.text = session.name
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = FragmentMenuItemBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
override fun getItemCount() = currentList.size
}
object SessionDiffCallback : DiffUtil.ItemCallback<Session>() {
override fun areItemsTheSame(oldItem: Session, newItem: Session): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Session, newItem: Session): Boolean {
return oldItem.id == newItem.id
}
}

View file

@ -0,0 +1,71 @@
package com.example.palto.ui.menu
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.example.palto.R
import com.example.palto.databinding.FragmentMenuListBinding
import com.example.palto.domain.Session
import com.example.palto.ui.login.LoginViewModel
/**
* A fragment representing a list of Sessions.
*/
class MenuFragment : Fragment() {
private val menuViewModel: MenuViewModel by
navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory }
private val loginViewModel: LoginViewModel by
activityViewModels() { LoginViewModel.Factory }
// This property is only valid between onCreateView and onDestroyView
private lateinit var binding: FragmentMenuListBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMenuListBinding.inflate(inflater, container, false)
val navController = findNavController()
// Connect the user.
loginViewModel.user.observe(viewLifecycleOwner) {
if (it != null) {
// Get sessions of the user from remote.
} else {
navController.navigate(R.id.loginFragment)
}
}
// Display the list of sessions.
// Create a new MenuAdapter (list) with the given function when clicking an item.
val adapter = MenuAdapter { adapterOnClick(it) }
binding.menuList.adapter = adapter
// Link the adapter with the session list in the menuViewMode.
menuViewModel.sessions.observe(viewLifecycleOwner) {
adapter.submitList(it)
}
// Bind the add button.
binding.createSession.setOnClickListener {
navController.navigate(R.id.action_menuFragment_to_newSessionFragment)
}
return binding.root
}
private fun adapterOnClick(session: Session) {
val bundle = bundleOf("session" to session.id)
findNavController().navigate(R.id.action_menuFragment_to_sessionFragment, bundle)
}
}

View file

@ -0,0 +1,47 @@
package com.example.palto.ui.menu
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.palto.domain.Attendance
import com.example.palto.domain.Session
/**
* ViewModel for accessing all the sessions created.
*/
class MenuViewModel() : ViewModel() {
// A list of sessions.
private var _sessions = MutableLiveData<List<Session>>()
val sessions: LiveData<List<Session>> = _sessions
fun createSession(name: String) {
val list = _sessions.value ?: emptyList()
val session = Session(
id = list.size,
name = name,
attendances = emptyList()
)
_sessions.value = list + session
Log.d("Palto", "MenuViewModel: A session has been added into the list.")
}
fun getSession(id: Int): Session? {
return _sessions.value?.find { it.id == id }
}
fun setAttendanceListSession(id: Int, list: List<Attendance>) {
getSession(id)?.let { it.attendances = list }
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MenuViewModel() as T
}
}
}
}

View file

@ -0,0 +1,46 @@
package com.example.palto.ui.menu.new_session
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.menu.MenuView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.example.palto.R
import com.example.palto.databinding.FragmentNewSessionBinding
import com.example.palto.ui.menu.MenuViewModel
/**
*
*/
class NewSessionFragment : Fragment() {
private val newSessionViewModel: NewSessionViewModel by viewModels()
private val menuViewModel: MenuViewModel by
navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory }
// This property is only valid between onCreateView and onDestroyView
private lateinit var binding: FragmentNewSessionBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentNewSessionBinding.inflate(inflater, container, false)
// Bind the create button
binding.newSessionCreate.setOnClickListener {
menuViewModel.createSession(
name = binding.newSessionName.text.toString()
)
findNavController().popBackStack()
}
return binding.root
}
}

View file

@ -0,0 +1,8 @@
package com.example.palto.ui.menu.new_session
import androidx.lifecycle.ViewModel
/**
* ViewModel of the session creation form. Used for verification.
*/
class NewSessionViewModel() : ViewModel()

View file

@ -0,0 +1,52 @@
package com.example.palto.ui.session
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.FragmentSessionItemBinding
import com.example.palto.domain.Attendance
import com.example.palto.domain.Card
import java.time.format.DateTimeFormatter
/**
* A [ListAdapter] that can display [Attendance] items.
*/
class SessionAdapter : ListAdapter<Attendance, SessionAdapter.ViewHolder>(AttendanceDiffCallback) {
inner class ViewHolder(binding: FragmentSessionItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private val attendanceUsernameText: TextView = binding.attendanceUsername
private val attendanceDate: TextView = binding.attendanceDate
fun bind(attendance: Attendance) {
attendanceUsernameText.text = attendance.student.username
attendanceDate.text = attendance.date.format(DateTimeFormatter.ofPattern("HH:mm:ss"))
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = FragmentSessionItemBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
@OptIn(ExperimentalStdlibApi::class)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
}
object AttendanceDiffCallback : DiffUtil.ItemCallback<Attendance>() {
override fun areItemsTheSame(oldItem: Attendance, newItem: Attendance): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Attendance, newItem: Attendance): Boolean {
return oldItem.id == newItem.id
}
}

View file

@ -0,0 +1,106 @@
package com.example.palto.ui.session
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.example.palto.NfcViewModel
import com.example.palto.R
import com.example.palto.databinding.FragmentSessionListBinding
import com.example.palto.ui.CardViewModel
import com.example.palto.ui.menu.MenuViewModel
/**
* A fragment representing a list of attendances.
*/
class SessionFragment : Fragment() {
private val sessionViewModel: SessionViewModel by
navGraphViewModels(R.id.nav_graph) { SessionViewModel.Factory }
private val menuViewModel: MenuViewModel by
navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory }
private val cardViewModel: CardViewModel by
navGraphViewModels(R.id.nav_graph)
private val nfcViewModel: NfcViewModel by activityViewModels()
// This property is only valid between onCreateView and onDestroyView
private lateinit var binding: FragmentSessionListBinding
/**
* Have the fragment instantiate the user interface.
*/
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentSessionListBinding.inflate(inflater, container, false)
// If the bundle value id of key "session" is not empty,
// set the attendance list to that of the selected session.
arguments?.getInt("session")?.let { id ->
val session = menuViewModel.getSession(id)
if (session != null) {
Log.d("Palto", "SessionFragment: Session id ${session.id} has been found.")
sessionViewModel.session = session
sessionViewModel.setAttendanceList(session.attendances)
}
}
// Set the adapter of the view for managing automatically the list of items on the screen.
val adapter = SessionAdapter()
binding.sessionList.adapter = adapter
sessionViewModel.attendances.observe(viewLifecycleOwner) {
adapter.submitList(it)
}
// Set the listener for a new NFC tag.
nfcViewModel.tagId.observe(viewLifecycleOwner) {
// If the NFC tag has not been handled.
it.getContentIfNotHandled()?.let { cardId ->
val card = cardViewModel.getCard(cardId)
// If a card with this tag exists, add this card.
if (card != null) {
sessionViewModel.addAttendance(card.user)
// Else go to the NewStudentFragment to create a new card and student.
} else {
val bundle = bundleOf("tagId" to cardId)
findNavController()
.navigate(R.id.action_sessionFragment_to_newStudentFragment, bundle)
}
}
}
// Manual add student button
binding.addStudent.setOnClickListener {
findNavController().navigate(R.id.action_sessionFragment_to_newStudentFragment)
}
// Print the result of adding an attendance on the view.
sessionViewModel.result.observe(viewLifecycleOwner) {
// If the result has not been already shown
it.getContentIfNotHandled()?.let { result ->
Toast.makeText(
activity,
getString(result.message, result.username),
Toast.LENGTH_LONG).show()
}
}
return binding.root
}
}

View file

@ -0,0 +1,10 @@
package com.example.palto.ui.session
/**
* Authentication result : success is true if connected or error message with exception.
*/
data class SessionResult(
val success: Boolean,
val message: Int, // Id of the string resource to display to the user
val username: String
)

View file

@ -0,0 +1,84 @@
package com.example.palto.ui.session
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.palto.R
import com.example.palto.domain.Attendance
import com.example.palto.domain.Event
import com.example.palto.domain.Session
import com.example.palto.domain.User
import java.time.LocalTime
/**
* ViewModel of a session which has a list of attendances.
*/
class SessionViewModel() : ViewModel() {
private var _result = MutableLiveData<Event<SessionResult>>()
val result: LiveData<Event<SessionResult>> = _result
private var _attendances = MutableLiveData<List<Attendance>>()
val attendances: LiveData<List<Attendance>> = _attendances
// The opened session which have been selected in the menu.
var session: Session? = null
/**
* Add the [student] in the attendance list [attendances].
* Return true if it has been added, else return false.
*/
fun addAttendance(student: User) {
val list = _attendances.value ?: emptyList()
// If the list already contains the user, return false
if (list.any { it.student == student }) {
Log.d("Palto", "SessionViewModel: User already in the list.")
_result.value = Event(SessionResult(
false,
R.string.session_user_already_added,
student.username
))
// Else create a new attendance and add it into the list.
} else {
val attendance = Attendance(
id = list.size,
student = student,
date = LocalTime.now()
)
// Add the attendance in the attendance list, and trigger the observers.
_attendances.value = list + attendance
// Saved the list in the session.
session?.attendances = list + attendance
_result.value = Event(SessionResult(
true,
R.string.session_user_added,
student.username
))
Log.d("Palto", "SessionViewModel: An attendance has been added into the list.")
}
}
fun setAttendanceList(list: List<Attendance>) {
_attendances.value = 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 SessionViewModel() as T
}
}
}
}

View file

@ -0,0 +1,69 @@
package com.example.palto.ui.session.new_student
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.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.example.palto.NfcViewModel
import com.example.palto.R
import com.example.palto.databinding.FragmentNewSessionBinding
import com.example.palto.databinding.FragmentNewStudentBinding
import com.example.palto.ui.CardViewModel
import com.example.palto.ui.UserViewModel
import com.example.palto.ui.menu.MenuViewModel
import com.example.palto.ui.session.SessionViewModel
/**
*
*/
class NewStudentFragment : Fragment() {
private val newStudentViewModel: NewStudentViewModel by viewModels()
private val sessionViewModel: SessionViewModel by navGraphViewModels(R.id.nav_graph)
private val userViewModel: UserViewModel by navGraphViewModels(R.id.nav_graph)
private val cardViewModel: CardViewModel by navGraphViewModels(R.id.nav_graph)
// This property is only valid between onCreateView and onDestroyView
private lateinit var binding: FragmentNewStudentBinding
@OptIn(ExperimentalStdlibApi::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentNewStudentBinding.inflate(inflater, container, false)
// If the bundle value tagId of key "tagId" exists,
// Set it on the screen.
val tagId = arguments?.getString("tagId")
if (tagId != null) {
binding.newStudentCardId.text = tagId
}
// Bind the create button.
binding.newStudentCreate.setOnClickListener {
val user = userViewModel.createUser(binding.newStudentName.text.toString())
// If a tag has been provided, create the card.
// The user would not need to create his account afterward.
if (tagId != null) {
cardViewModel.createCard(user, tagId)
}
sessionViewModel.addAttendance(user)
findNavController().popBackStack()
}
return binding.root
}
}

View file

@ -0,0 +1,8 @@
package com.example.palto.ui.session.new_student
import androidx.lifecycle.ViewModel
/**
* ViewModel of the session creation form. Used for verifications.
*/
class NewStudentViewModel() : ViewModel()

View file

@ -1,44 +0,0 @@
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

@ -1,31 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,57 +0,0 @@
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

@ -1,12 +1,21 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/palto_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:id="@+id/palto_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
@ -14,6 +23,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@+id/palto_toolbar"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,13 +0,0 @@
<?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

@ -13,11 +13,8 @@
<EditText
android:id="@+id/hostname"
android:layout_width="0dp"
android:layout_width="331dp"
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"
@ -30,10 +27,8 @@
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_width="331dp"
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"
@ -46,29 +41,28 @@
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_width="331dp"
android:layout_height="50dp"
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_constraintBottom_toTopOf="@+id/login_error"
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:id="@+id/login_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/help_message"
android:text=""
app:layout_constraintBottom_toTopOf="@+id/login"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password" />
@ -77,33 +71,38 @@
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:enabled="true"
android:text="@string/action_sign_in"
app:layout_constraintBottom_toTopOf="@+id/login_anonymous"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/login_error" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="39dp"
android:layout_height="47dp"
android:layout_gravity="center"
android:visibility="gone"
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" />
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5" />
<ProgressBar
android:id="@+id/loading"
<TextView
android:id="@+id/login_anonymous"
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"
android:clickable="true"
android:text="@string/login_anonymous_text"
android:textColor="#F4511E"
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" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/login" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,10 +4,17 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/card_̤id"
<!--<TextView
android:id="@+id/session_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
android:textAppearance="?attr/textAppearanceListItem" />-->
<TextView
android:id="@+id/session_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textAppearance="?attr/textAppearanceListItem"/>
</LinearLayout>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/menu_list"
android:name="com.example.palto.ui.menu.menuFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".ui.menu.MenuFragment"
tools:listitem="@layout/fragment_menu_item" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/create_session"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:clickable="true"
android:contentDescription="@string/create_session"
app:srcCompat="@android:drawable/ic_input_add" />
</FrameLayout>

View file

@ -0,0 +1,33 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/new_session_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/new_session_name_hint"
android:inputType="text"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@+id/new_session_create"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/new_session_create"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_session_button_create"
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/new_session_name" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,44 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/new_student_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/new_student_name"
android:inputType="text"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@+id/new_student_card_id"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/new_student_card_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_student_card_id_text"
app:layout_constraintBottom_toTopOf="@+id/new_student_create"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/new_student_name" />
<Button
android:id="@+id/new_student_create"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_student_button_create"
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/new_student_card_id" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -5,14 +5,14 @@
android:orientation="horizontal">
<TextView
android:id="@+id/item_number"
android:id="@+id/attendance_username"
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:id="@+id/attendance_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"

View file

@ -1,13 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout 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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/session_list"
android:name="com.example.palto.ui.session.sessionFragment"
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:context=".ui.session.SessionFragment"
tools:listitem="@layout/fragment_session_item" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_student"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:clickable="true"
android:contentDescription="@string/create_card"
app:srcCompat="@android:drawable/ic_input_add" />
</FrameLayout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<!--<item
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:title="@string/menu_item_logout" />-->
</menu>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<background android:drawable="@color/white" />
<foreground android:drawable="@drawable/icon_foreground" />
</adaptive-icon>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<background android:drawable="@color/white" />
<foreground android:drawable="@drawable/icon_foreground" />
</adaptive-icon>

View file

@ -3,20 +3,47 @@
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">
app:startDestination="@id/menuFragment">
<fragment
android:id="@+id/menuFragment"
android:name="com.example.palto.ui.menu.MenuFragment"
android:label="Menu"
tools:layout="@layout/fragment_menu_list" >
<action
android:id="@+id/action_menuFragment_to_sessionFragment"
app:destination="@id/sessionFragment" />
<action
android:id="@+id/action_menuFragment_to_newSessionFragment"
app:destination="@id/newSessionFragment" />
</fragment>
<fragment
android:id="@+id/loginFragment"
android:name="com.example.palto.ui.login.LoginFragment"
android:label="fragment._login"
android:label="Connexion"
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" />
android:id="@+id/newSessionFragment"
android:name="com.example.palto.ui.menu.new_session.NewSessionFragment"
android:label="Nouvelle fiche"
tools:layout="@layout/fragment_new_session"/>
<fragment
android:id="@+id/attendanceListFragment"
android:name="com.example.palto.ui.attendanceList.AttendanceListFragment"
android:label="fragment_attendance_list"
tools:layout="@layout/fragment_attendance_list" />
android:id="@+id/sessionFragment"
android:name="com.example.palto.ui.session.SessionFragment"
android:label="Fiche de présence"
tools:layout="@layout/fragment_session_list" >
<action
android:id="@+id/action_sessionFragment_to_newStudentFragment"
app:destination="@id/newStudentFragment" />
</fragment>
<fragment
android:id="@+id/newStudentFragment"
android:name="com.example.palto.ui.session.new_student.NewStudentFragment"
android:label="Nouvel étudiant"
tools:layout="@layout/fragment_new_student" />
</navigation>

View file

@ -1,7 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Palto" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#00345f</color>
<color name="colorOnPrimary">#ffffff</color>
<color name="colorSecondary">#ef800a</color>
<color name="colorOnSecondary">#ffffff</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

@ -5,11 +5,23 @@
<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="login_anonymous_text">Connexion anonyme</string>
<string name="action_sign_in_short">Connexion</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>
<string name="create_session">Créer une session</string>
<string name="create_card">Créer une présence</string>
<string name="nouvelle_fiche">Nouvelle Fiche</string>
<string name="new_session_name_hint">Nom de la fiche</string>
<string name="new_session_button_create">Créer</string>
<string name="menu_item_logout">Déconnexion</string>
<string name="new_student_button_create">Ajouter</string>
<string name="new_student_name">Nom de létudiant</string>
<string name="new_student_card_id_text">ID Carte : Pas de carte</string>
<string name="session_user_already_added">Létudiant %1$s existe déjà dans la liste !\n</string>
<string name="session_user_added">Létudiant %1$s a été ajouté</string>
</resources>

View file

@ -1,8 +1,17 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Palto" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<resources>
<style name="Base.Theme.Palto" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorOnPrimary">@color/colorOnPrimary</item>
<item name="colorAccent">@color/colorSecondary</item>
<item name="android:textSize">24sp</item>
<item name="floatingActionButtonStyle">@style/actionButton</item>
</style>
<style name="actionButton">
<item name="maxImageSize">50dp</item>
<item name="tint">@color/white</item>
<item name="backgroundTint">@color/colorSecondary</item>
</style>
<style name="Theme.Palto" parent="Base.Theme.Palto" />

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.2.0" apply false
id("com.android.application") version "8.2.1" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
}