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.ui.graphics)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation("androidx.compose.material:material-icons-extended:1.4.3") 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) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
@@ -64,13 +72,15 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
//implementation pour RETROFIT (API) // Retrofit
implementation("com.squareup.retrofit2:retrofit:3.0.0") implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.retrofit2:converter-gson: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-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$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"?> <?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"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -23,8 +23,10 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ui.screen.AjoutDvdActivity"/> <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.SupprDvdActivity"/>
<activity android:name=".ui.screen.ScanCodeActivity"/>
</application> </application>
</manifest> </manifest>

View File

@@ -1,6 +1,8 @@
package com.dev.collectiondvd package com.dev.collectiondvd
import android.Manifest
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -56,6 +58,11 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem 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() { class MainActivity : ComponentActivity() {
@@ -132,7 +139,6 @@ fun MenuScreen(
filteredDvds.sortedByDescending { it.annee.orEmpty() } filteredDvds.sortedByDescending { it.annee.orEmpty() }
} }
// 🔍 Filtrage
Scaffold( Scaffold(
@@ -185,6 +191,7 @@ fun MenuScreen(
@Composable @Composable
fun TopBar() { fun TopBar() {
val context = LocalContext.current
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -195,12 +202,16 @@ fun TopBar() {
Text( Text(
text = "Votre Vidéothèque", text = "Votre Vidéothèque",
fontSize = 22.sp, fontSize = 22.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
) )
// Row { Row {
// Text("ⓘ", fontSize = 20.sp) MyButton (""){
// } context.startActivity(
Intent(context, StatDvdActivity::class.java) )
}
}
} }
} }
@@ -231,7 +242,7 @@ fun SortChip(
onSortSelected: (SortType) -> Unit onSortSelected: (SortType) -> Unit
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val context = LocalContext.current
Card( Card(
modifier = Modifier.clickable { expanded = true }, modifier = Modifier.clickable { expanded = true },
shape = RoundedCornerShape(50), shape = RoundedCornerShape(50),
@@ -260,6 +271,13 @@ fun SortChip(
) )
} }
} }
MyButton("Scanner") {
val intent = Intent(context, ScanCodeActivity::class.java)
context.startActivity(intent)
}
} }
@Composable @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.dao.DvdDao
import com.dev.collectiondvd.data.local.entities.Dvd 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 class AppDatabase : RoomDatabase(){
abstract fun dvdDao(): DvdDao abstract fun dvdDao(): DvdDao
@@ -21,7 +21,7 @@ abstract class AppDatabase : RoomDatabase(){
context.applicationContext, context.applicationContext,
AppDatabase::class.java, AppDatabase::class.java,
"dvd_manager" "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") @Query("SELECT * FROM dvds WHERE id = :numero")
fun getDvdById(numero: Long): Flow<Dvd?> 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 annee: String,
val realisateur: String, val realisateur: String,
val genre: 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.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -52,6 +53,7 @@ class AjoutDvdActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val barcode = intent.getStringExtra("barcode")
setContent { setContent {
CollectionDvdTheme(darkTheme = true) { CollectionDvdTheme(darkTheme = true) {
AjoutDvdScreen( AjoutDvdScreen(
@@ -59,7 +61,8 @@ class AjoutDvdActivity : ComponentActivity() {
onAdd = { dvd -> onAdd = { dvd ->
viewModel.addDvd(dvd) viewModel.addDvd(dvd)
finish() finish()
} },
preFilledBarcode = barcode
) )
} }
} }
@@ -71,12 +74,14 @@ class AjoutDvdActivity : ComponentActivity() {
@Composable @Composable
fun AjoutDvdScreen( fun AjoutDvdScreen(
onQuit: () -> Unit, onQuit: () -> Unit,
onAdd: (Dvd) -> Unit onAdd: (Dvd) -> Unit,
preFilledBarcode: String? = null
) { ) {
var titre by remember { mutableStateOf("") } var titre by remember { mutableStateOf("") }
var annee by remember { mutableStateOf("") } var annee by remember { mutableStateOf("") }
var realisateur by remember { mutableStateOf("") } var realisateur by remember { mutableStateOf("") }
var genre by remember { mutableStateOf("") } var genre by remember { mutableStateOf("") }
var barcode by remember { mutableStateOf(preFilledBarcode?: "") }
Scaffold( Scaffold(
containerColor = MaterialTheme.colorScheme.background, // fond sombre containerColor = MaterialTheme.colorScheme.background, // fond sombre
@@ -100,10 +105,15 @@ fun AjoutDvdScreen(
// Champs stylisés // Champs stylisés
StyledTextField(value = titre, onValueChange = { titre = it }, label = "Titre") 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 = realisateur, onValueChange = { realisateur = it }, label = "Réalisateur")
StyledTextField(value = annee, onValueChange = { annee = it }, label = "Année") StyledTextField(value = annee, onValueChange = { annee = it }, label = "Année")
StyledTextField(value = genre, onValueChange = { genre = it }, label = "Genre") StyledTextField(value = genre, onValueChange = { genre = it }, label = "Genre")
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Row{ Row{
@@ -120,6 +130,12 @@ fun AjoutDvdScreen(
} }
} }
MyButton("Quitter") { onQuit() } 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)
// }
// }
} }

View File

@@ -10,6 +10,9 @@ activityCompose = "1.12.1"
composeBom = "2024.09.00" composeBom = "2024.09.00"
uiGraphics = "1.10.0" uiGraphics = "1.10.0"
material3 = "1.4.0" material3 = "1.4.0"
runtime = "1.10.0"
cameraLifecycle = "1.5.2"
cameraView = "1.5.2"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -28,6 +31,9 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraLifecycle" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraView" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }