This commit is contained in:
MalcuitEmile
2025-12-18 11:37:41 +01:00
parent d4eed99351
commit 01437782cb
11 changed files with 463 additions and 16 deletions

View File

@@ -56,6 +56,14 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.material3)
implementation("androidx.compose.material:material-icons-extended:1.4.3")
implementation(libs.androidx.compose.runtime)
// CameraX
implementation("androidx.camera:camera-core:1.2.3")
implementation("androidx.camera:camera-camera2:1.2.3") // ⚠ Obligatoire
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -64,13 +72,15 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
//implementation pour RETROFIT (API)
// Retrofit
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.retrofit2:converter-gson:3.0.0")
//pour Room
// Room
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")
// ML Kit
implementation("com.google.mlkit:barcode-scanning:17.2.0")
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -23,8 +23,10 @@
</intent-filter>
</activity>
<activity android:name=".ui.screen.AjoutDvdActivity"/>
<activity android:name=".ui.screen.ListDvdActivity"/>
<activity android:name=".ui.screen.StatDvdActivity"/>
<activity android:name=".ui.screen.SupprDvdActivity"/>
<activity android:name=".ui.screen.ScanCodeActivity"/>
</application>
</manifest>

View File

@@ -1,6 +1,8 @@
package com.dev.collectiondvd
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -56,6 +58,11 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.dev.collectiondvd.ui.screen.ScanCodeActivity
import com.dev.collectiondvd.ui.screen.StatDvdActivity
import com.dev.collectiondvd.ui.theme.MyButton
class MainActivity : ComponentActivity() {
@@ -132,7 +139,6 @@ fun MenuScreen(
filteredDvds.sortedByDescending { it.annee.orEmpty() }
}
// 🔍 Filtrage
Scaffold(
@@ -185,6 +191,7 @@ fun MenuScreen(
@Composable
fun TopBar() {
val context = LocalContext.current
Row(
modifier = Modifier
.fillMaxWidth()
@@ -195,12 +202,16 @@ fun TopBar() {
Text(
text = "Votre Vidéothèque",
fontSize = 22.sp,
fontWeight = FontWeight.Bold
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
)
// Row {
// Text("ⓘ", fontSize = 20.sp)
// }
Row {
MyButton (""){
context.startActivity(
Intent(context, StatDvdActivity::class.java) )
}
}
}
}
@@ -231,7 +242,7 @@ fun SortChip(
onSortSelected: (SortType) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
val context = LocalContext.current
Card(
modifier = Modifier.clickable { expanded = true },
shape = RoundedCornerShape(50),
@@ -260,6 +271,13 @@ fun SortChip(
)
}
}
MyButton("Scanner") {
val intent = Intent(context, ScanCodeActivity::class.java)
context.startActivity(intent)
}
}
@Composable

View File

@@ -7,7 +7,7 @@ import androidx.room.RoomDatabase
import com.dev.collectiondvd.data.local.dao.DvdDao
import com.dev.collectiondvd.data.local.entities.Dvd
@Database( entities = [Dvd::class], version = 1)
@Database( entities = [Dvd::class], version = 2)
abstract class AppDatabase : RoomDatabase(){
abstract fun dvdDao(): DvdDao
@@ -21,7 +21,7 @@ abstract class AppDatabase : RoomDatabase(){
context.applicationContext,
AppDatabase::class.java,
"dvd_manager"
).build().also { INSTANCE = it }
).fallbackToDestructiveMigration().build().also { INSTANCE = it }
}
}

View File

@@ -22,5 +22,8 @@ interface DvdDao{
@Query("SELECT * FROM dvds WHERE id = :numero")
fun getDvdById(numero: Long): Flow<Dvd?>
@Query("SELECT * FROM dvds WHERE barcode = :barcode LIMIT 1")
suspend fun getByBarcode(barcode: String): Dvd?
}

View File

@@ -11,5 +11,7 @@ data class Dvd (
val annee: String,
val realisateur: String,
val genre: String,
val synced: Boolean = false
val synced: Boolean = false,
val barcode: String? = null,
val poster: String? = null
)

View File

@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition
import androidx.compose.material3.MaterialTheme
@@ -52,6 +53,7 @@ class AjoutDvdActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val barcode = intent.getStringExtra("barcode")
setContent {
CollectionDvdTheme(darkTheme = true) {
AjoutDvdScreen(
@@ -59,7 +61,8 @@ class AjoutDvdActivity : ComponentActivity() {
onAdd = { dvd ->
viewModel.addDvd(dvd)
finish()
}
},
preFilledBarcode = barcode
)
}
}
@@ -71,12 +74,14 @@ class AjoutDvdActivity : ComponentActivity() {
@Composable
fun AjoutDvdScreen(
onQuit: () -> Unit,
onAdd: (Dvd) -> Unit
onAdd: (Dvd) -> Unit,
preFilledBarcode: String? = null
) {
var titre by remember { mutableStateOf("") }
var annee by remember { mutableStateOf("") }
var realisateur by remember { mutableStateOf("") }
var genre by remember { mutableStateOf("") }
var barcode by remember { mutableStateOf(preFilledBarcode?: "") }
Scaffold(
containerColor = MaterialTheme.colorScheme.background, // fond sombre
@@ -100,10 +105,15 @@ fun AjoutDvdScreen(
// Champs stylisés
StyledTextField(value = titre, onValueChange = { titre = it }, label = "Titre")
StyledTextField(value = barcode, onValueChange = { barcode = it }, label = "Code-barres")
StyledTextField(value = realisateur, onValueChange = { realisateur = it }, label = "Réalisateur")
StyledTextField(value = annee, onValueChange = { annee = it }, label = "Année")
StyledTextField(value = genre, onValueChange = { genre = it }, label = "Genre")
Spacer(modifier = Modifier.height(16.dp))
Row{
@@ -120,6 +130,12 @@ fun AjoutDvdScreen(
}
}
MyButton("Quitter") { onQuit() }
Button(
onClick = { }
) {
Text("Scanner un DVD")
}
}
}
}

View File

@@ -0,0 +1,134 @@
package com.dev.collectiondvd.ui.screen
import android.Manifest
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
class ScanCodeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Permission launcher moderne
val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) startScan()
else finish() // permission refusée
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= android.content.pm.PackageManager.PERMISSION_GRANTED
) {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
} else {
startScan()
}
}
private fun startScan() {
setContent {
ScanBarcodeScreen(
onBarcodeDetected = { code ->
Log.d("SCAN", "Barcode détecté: $code")
val intent = Intent(this, AjoutDvdActivity::class.java)
intent.putExtra("barcode", code)
startActivity(intent)
finish()
},
onBack = { finish() }
)
}
}
}
@Composable
fun ScanBarcodeScreen(
onBarcodeDetected: (String) -> Unit,
onBack: () -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
// State pour éviter double scan
val scanned = remember { mutableStateOf(false) }
AndroidView(factory = { ctx ->
val previewView = PreviewView(ctx)
cameraProviderFuture.addListener({
try {
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeScanner = BarcodeScanning.getClient()
val imageAnalyzer = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { analysis ->
analysis.setAnalyzer(ContextCompat.getMainExecutor(ctx)) { imageProxy ->
val mediaImage = imageProxy.image
if (mediaImage != null) {
val inputImage = InputImage.fromMediaImage(
mediaImage,
imageProxy.imageInfo.rotationDegrees
)
barcodeScanner.process(inputImage)
.addOnSuccessListener { barcodes ->
if (!scanned.value) {
barcodes.forEach { barcode ->
barcode.rawValue?.let { code ->
scanned.value = true
onBarcodeDetected(code)
}
}
}
}
.addOnFailureListener { e ->
Log.e("SCAN", "Erreur ML Kit: ${e.message}")
}
.addOnCompleteListener { imageProxy.close() }
} else {
imageProxy.close()
}
}
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalyzer)
Log.d("SCAN", "CameraX bound")
} catch (e: Exception) {
Log.e("SCAN", "Erreur CameraX: ${e.message}")
e.printStackTrace()
}
}, ContextCompat.getMainExecutor(ctx))
previewView
}, modifier = Modifier.fillMaxSize())
}

View File

@@ -0,0 +1,245 @@
package com.dev.collectiondvd.ui.screen
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModelProvider
import com.dev.collectiondvd.MenuScreen
import com.dev.collectiondvd.data.local.AppDatabase
import com.dev.collectiondvd.data.repository.LocalDvdRepository
import com.dev.collectiondvd.ui.theme.CollectionDvdTheme
import com.dev.collectiondvd.ui.theme.MyButton
import com.dev.collectiondvd.viewmodel.LocalDvdViewModel
import com.dev.collectiondvd.viewmodel.factory.LocalDvdViewModelFactory
class StatDvdActivity: ComponentActivity() {
private lateinit var viewModel: LocalDvdViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val database = AppDatabase.getDatabase(applicationContext)
val dvdDao = database.dvdDao()
val repository = LocalDvdRepository(dvdDao)
val factory = LocalDvdViewModelFactory(repository)
viewModel = ViewModelProvider(this, factory).get(LocalDvdViewModel::class.java)
setContent {
CollectionDvdTheme(darkTheme = true) {
StatDvdScreen(
onQuit = { finish() },
viewModel = viewModel
)
}
}
}
}
@Composable
fun StatDvdScreen(
viewModel: LocalDvdViewModel,
onQuit: () -> Unit
) {
val dvds by viewModel.dvds.collectAsState(initial = emptyList())
val totalDvds = dvds.size
val genresCount = dvds.groupingBy { it.genre }.eachCount()
val directorsCount = dvds.groupingBy { it.realisateur }.eachCount()
val numberOfGenres = genresCount.size
val favoriteGenre = genresCount.maxByOrNull { it.value }?.key ?: "Aucun"
val favoriteDirector = directorsCount.maxByOrNull { it.value }?.key ?: "Aucun"
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(
top = WindowInsets.statusBars
.asPaddingValues()
.calculateTopPadding()
)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Mes Statistiques",
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold
)
)
}
StatCardDvd(totalDvds)
StatCard("Réalisateur", directorsCount.size, favoriteDirector)
StatCard("Genre", numberOfGenres, favoriteGenre)
Text("La suite est en développement...")
//Spacer(modifier = Modifier.weight(0.5f))
MyButton("Quitter") {
onQuit()
}
}
}
}
@Composable
fun StatCardDvd(
totalDvd :Int,
){
var expended by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
shape = RoundedCornerShape(12.dp)
){
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Text(
"Films",
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
color = Color(0xFFFFA405)
)
}
Divider()
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
){
// Box(
// modifier = Modifier
// .height(90.dp)
// .width(60.dp)
// .background(
// MaterialTheme.colorScheme.surfaceVariant,
// RoundedCornerShape(8.dp)
// )
// )
Spacer(modifier = Modifier.width(12.dp))
Column {
Text("Vous possedez: ${totalDvd} Dvds")
}
}
}
}
}
@Composable
fun StatCard(
name: String,
total :Int,
preferer: String
){
var expended by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
shape = RoundedCornerShape(12.dp)
){
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Text(
"${name}",
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
color = Color(0xFFFFA405)
)
}
Divider()
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
){
// Box(
// modifier = Modifier
// .height(90.dp)
// .width(60.dp)
// .background(
// MaterialTheme.colorScheme.surfaceVariant,
// RoundedCornerShape(8.dp)
// )
// )
Spacer(modifier = Modifier.width(12.dp))
Column {
Text("Vous avez: ${total} ${name}")
Text("Votre ${name} preferé est: ${preferer} ")
}
}
}
}
}

View File

@@ -44,4 +44,15 @@ class LocalDvdViewModel (
}
}
fun addDvdFromBarcode(barcode: String, titre: String, realisateur: String, annee: String, genre: String) {
addDvd(Dvd(titre = titre, realisateur = realisateur, annee = annee, genre = genre, barcode = barcode))
}
// fun getDvdByBarcode(barcode: String, onResult: (Dvd?) -> Unit) {
// viewModelScope.launch {
// val dvd = repository.getDvdByBarcode(barcode)
// onResult(dvd)
// }
// }
}