diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d63fe80..4076d86 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 91fa271..9861072 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - + - + + + \ No newline at end of file diff --git a/app/src/main/java/com/dev/collectiondvd/MainActivity.kt b/app/src/main/java/com/dev/collectiondvd/MainActivity.kt index 6ea7c0f..2c65a83 100644 --- a/app/src/main/java/com/dev/collectiondvd/MainActivity.kt +++ b/app/src/main/java/com/dev/collectiondvd/MainActivity.kt @@ -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 diff --git a/app/src/main/java/com/dev/collectiondvd/data/local/AppDatabase.kt b/app/src/main/java/com/dev/collectiondvd/data/local/AppDatabase.kt index cff73ac..df73645 100644 --- a/app/src/main/java/com/dev/collectiondvd/data/local/AppDatabase.kt +++ b/app/src/main/java/com/dev/collectiondvd/data/local/AppDatabase.kt @@ -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 } } } diff --git a/app/src/main/java/com/dev/collectiondvd/data/local/dao/DvdDao.kt b/app/src/main/java/com/dev/collectiondvd/data/local/dao/DvdDao.kt index 363b246..b275c64 100644 --- a/app/src/main/java/com/dev/collectiondvd/data/local/dao/DvdDao.kt +++ b/app/src/main/java/com/dev/collectiondvd/data/local/dao/DvdDao.kt @@ -22,5 +22,8 @@ interface DvdDao{ @Query("SELECT * FROM dvds WHERE id = :numero") fun getDvdById(numero: Long): Flow + @Query("SELECT * FROM dvds WHERE barcode = :barcode LIMIT 1") + suspend fun getByBarcode(barcode: String): Dvd? + } \ No newline at end of file diff --git a/app/src/main/java/com/dev/collectiondvd/data/local/entities/Dvd.kt b/app/src/main/java/com/dev/collectiondvd/data/local/entities/Dvd.kt index 5c4f30a..7e0d5e4 100644 --- a/app/src/main/java/com/dev/collectiondvd/data/local/entities/Dvd.kt +++ b/app/src/main/java/com/dev/collectiondvd/data/local/entities/Dvd.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/com/dev/collectiondvd/ui/screen/AjoutDvdActivity.kt b/app/src/main/java/com/dev/collectiondvd/ui/screen/AjoutDvdActivity.kt index baf4024..ac9a6b2 100644 --- a/app/src/main/java/com/dev/collectiondvd/ui/screen/AjoutDvdActivity.kt +++ b/app/src/main/java/com/dev/collectiondvd/ui/screen/AjoutDvdActivity.kt @@ -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") + } } } } diff --git a/app/src/main/java/com/dev/collectiondvd/ui/screen/ScanCodeActivity.kt b/app/src/main/java/com/dev/collectiondvd/ui/screen/ScanCodeActivity.kt new file mode 100644 index 0000000..75a4a77 --- /dev/null +++ b/app/src/main/java/com/dev/collectiondvd/ui/screen/ScanCodeActivity.kt @@ -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()) +} diff --git a/app/src/main/java/com/dev/collectiondvd/ui/screen/StatDvdActivity.kt b/app/src/main/java/com/dev/collectiondvd/ui/screen/StatDvdActivity.kt new file mode 100644 index 0000000..0d6330f --- /dev/null +++ b/app/src/main/java/com/dev/collectiondvd/ui/screen/StatDvdActivity.kt @@ -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} ") + } + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dev/collectiondvd/viewmodel/LocalDvdViewModel.kt b/app/src/main/java/com/dev/collectiondvd/viewmodel/LocalDvdViewModel.kt index 39af12e..feda7fc 100644 --- a/app/src/main/java/com/dev/collectiondvd/viewmodel/LocalDvdViewModel.kt +++ b/app/src/main/java/com/dev/collectiondvd/viewmodel/LocalDvdViewModel.kt @@ -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) +// } +// } + } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 01870cc..b4ad44d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,9 @@ activityCompose = "1.12.1" composeBom = "2024.09.00" uiGraphics = "1.10.0" material3 = "1.4.0" +runtime = "1.10.0" +cameraLifecycle = "1.5.2" +cameraView = "1.5.2" [libraries] 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-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" } 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] android-application = { id = "com.android.application", version.ref = "agp" }