commit 4dcb0b1b5ae45b195b229d73e387c36661f4d037 Author: lucasroyerdev Date: Wed Jan 14 18:42:53 2026 +0100 feat: initial functional release of Stock Pignon diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df01bf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Build Android +.gradle/ +build/ +app/build/ +app/.gradle/ +captures/ + +# Output Android +**/release/*.apk +**/debug/*.apk +app/release/ + +# Android Studio / VS Code +.idea/ +*.iml +.vscode/ +local.properties + +# System files and logs +*.log +.DS_Store +Thumbs.db + +# Others +*.bak +*.swp +*~ \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..2ee9c8e --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.stock.pignon' + compileSdkVersion 36 + + defaultConfig { + applicationId "com.stock.pignon" + minSdkVersion 17 + targetSdkVersion 36 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.4.2' + + // Matériel design + implementation 'com.google.android.material:material:1.6.1' + + // JSON + implementation 'com.google.code.gson:gson:2.8.5' + + // GIF + implementation 'com.github.bumptech.glide:glide:3.8.0' + + // Tests + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/stock/pignon/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/stock/pignon/ExampleInstrumentedTest.java new file mode 100644 index 0000000..9a84a59 --- /dev/null +++ b/app/src/androidTest/java/com/stock/pignon/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.stock.pignon; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.stock.pignon", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3e38fd8 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/images/FD.jpg b/app/src/main/assets/images/FD.jpg new file mode 100644 index 0000000..468b5ee Binary files /dev/null and b/app/src/main/assets/images/FD.jpg differ diff --git a/app/src/main/assets/images/RD.jpg b/app/src/main/assets/images/RD.jpg new file mode 100644 index 0000000..f61fa53 Binary files /dev/null and b/app/src/main/assets/images/RD.jpg differ diff --git a/app/src/main/assets/images/_velo.jpg b/app/src/main/assets/images/_velo.jpg new file mode 100644 index 0000000..90771bf Binary files /dev/null and b/app/src/main/assets/images/_velo.jpg differ diff --git a/app/src/main/assets/images/axeRapide.jpg b/app/src/main/assets/images/axeRapide.jpg new file mode 100644 index 0000000..625c479 Binary files /dev/null and b/app/src/main/assets/images/axeRapide.jpg differ diff --git a/app/src/main/assets/images/axeRoue.jpg b/app/src/main/assets/images/axeRoue.jpg new file mode 100644 index 0000000..7916823 Binary files /dev/null and b/app/src/main/assets/images/axeRoue.jpg differ diff --git a/app/src/main/assets/images/bequille.jpg b/app/src/main/assets/images/bequille.jpg new file mode 100644 index 0000000..5d03b0a Binary files /dev/null and b/app/src/main/assets/images/bequille.jpg differ diff --git a/app/src/main/assets/images/boitierPedalier.jpg b/app/src/main/assets/images/boitierPedalier.jpg new file mode 100644 index 0000000..72172f4 Binary files /dev/null and b/app/src/main/assets/images/boitierPedalier.jpg differ diff --git a/app/src/main/assets/images/cable.jpg b/app/src/main/assets/images/cable.jpg new file mode 100644 index 0000000..77354ca Binary files /dev/null and b/app/src/main/assets/images/cable.jpg differ diff --git a/app/src/main/assets/images/cadre.jpg b/app/src/main/assets/images/cadre.jpg new file mode 100644 index 0000000..09daa98 Binary files /dev/null and b/app/src/main/assets/images/cadre.jpg differ diff --git a/app/src/main/assets/images/cassette.jpg b/app/src/main/assets/images/cassette.jpg new file mode 100644 index 0000000..3b6b1b2 Binary files /dev/null and b/app/src/main/assets/images/cassette.jpg differ diff --git a/app/src/main/assets/images/chaine.jpg b/app/src/main/assets/images/chaine.jpg new file mode 100644 index 0000000..7428b6e Binary files /dev/null and b/app/src/main/assets/images/chaine.jpg differ diff --git a/app/src/main/assets/images/cintre.jpg b/app/src/main/assets/images/cintre.jpg new file mode 100644 index 0000000..60145fc Binary files /dev/null and b/app/src/main/assets/images/cintre.jpg differ diff --git a/app/src/main/assets/images/collierSelle.jpg b/app/src/main/assets/images/collierSelle.jpg new file mode 100644 index 0000000..8f14960 Binary files /dev/null and b/app/src/main/assets/images/collierSelle.jpg differ diff --git a/app/src/main/assets/images/disque.jpg b/app/src/main/assets/images/disque.jpg new file mode 100644 index 0000000..cbe4b3a Binary files /dev/null and b/app/src/main/assets/images/disque.jpg differ diff --git a/app/src/main/assets/images/etrierDisque.jpg b/app/src/main/assets/images/etrierDisque.jpg new file mode 100644 index 0000000..762c089 Binary files /dev/null and b/app/src/main/assets/images/etrierDisque.jpg differ diff --git a/app/src/main/assets/images/etrierPatin.jpg b/app/src/main/assets/images/etrierPatin.jpg new file mode 100644 index 0000000..17cfcb5 Binary files /dev/null and b/app/src/main/assets/images/etrierPatin.jpg differ diff --git a/app/src/main/assets/images/fourche.jpg b/app/src/main/assets/images/fourche.jpg new file mode 100644 index 0000000..d227374 Binary files /dev/null and b/app/src/main/assets/images/fourche.jpg differ diff --git a/app/src/main/assets/images/gaine.jpg b/app/src/main/assets/images/gaine.jpg new file mode 100644 index 0000000..d22c91e Binary files /dev/null and b/app/src/main/assets/images/gaine.jpg differ diff --git a/app/src/main/assets/images/gardeBoue.jpg b/app/src/main/assets/images/gardeBoue.jpg new file mode 100644 index 0000000..14e9d62 Binary files /dev/null and b/app/src/main/assets/images/gardeBoue.jpg differ diff --git a/app/src/main/assets/images/jeuDirection.jpg b/app/src/main/assets/images/jeuDirection.jpg new file mode 100644 index 0000000..cd41065 Binary files /dev/null and b/app/src/main/assets/images/jeuDirection.jpg differ diff --git a/app/src/main/assets/images/lampe.jpg b/app/src/main/assets/images/lampe.jpg new file mode 100644 index 0000000..85e4c01 Binary files /dev/null and b/app/src/main/assets/images/lampe.jpg differ diff --git a/app/src/main/assets/images/manivelleDroite.jpg b/app/src/main/assets/images/manivelleDroite.jpg new file mode 100644 index 0000000..0bfeb8a Binary files /dev/null and b/app/src/main/assets/images/manivelleDroite.jpg differ diff --git a/app/src/main/assets/images/manivelleGauche.jpg b/app/src/main/assets/images/manivelleGauche.jpg new file mode 100644 index 0000000..8730a08 Binary files /dev/null and b/app/src/main/assets/images/manivelleGauche.jpg differ diff --git a/app/src/main/assets/images/merci.gif b/app/src/main/assets/images/merci.gif new file mode 100644 index 0000000..1053d7d Binary files /dev/null and b/app/src/main/assets/images/merci.gif differ diff --git a/app/src/main/assets/images/moyeuArriere.jpg b/app/src/main/assets/images/moyeuArriere.jpg new file mode 100644 index 0000000..a539f3e Binary files /dev/null and b/app/src/main/assets/images/moyeuArriere.jpg differ diff --git a/app/src/main/assets/images/moyeuAvant.jpg b/app/src/main/assets/images/moyeuAvant.jpg new file mode 100644 index 0000000..70e3fbb Binary files /dev/null and b/app/src/main/assets/images/moyeuAvant.jpg differ diff --git a/app/src/main/assets/images/patins.jpg b/app/src/main/assets/images/patins.jpg new file mode 100644 index 0000000..40c60ea Binary files /dev/null and b/app/src/main/assets/images/patins.jpg differ diff --git a/app/src/main/assets/images/pedales.jpg b/app/src/main/assets/images/pedales.jpg new file mode 100644 index 0000000..2b069ce Binary files /dev/null and b/app/src/main/assets/images/pedales.jpg differ diff --git a/app/src/main/assets/images/pedalierComplet.jpg b/app/src/main/assets/images/pedalierComplet.jpg new file mode 100644 index 0000000..335ecc8 Binary files /dev/null and b/app/src/main/assets/images/pedalierComplet.jpg differ diff --git a/app/src/main/assets/images/plateau.jpg b/app/src/main/assets/images/plateau.jpg new file mode 100644 index 0000000..2a712f7 Binary files /dev/null and b/app/src/main/assets/images/plateau.jpg differ diff --git a/app/src/main/assets/images/pneu.jpg b/app/src/main/assets/images/pneu.jpg new file mode 100644 index 0000000..5b71fa1 Binary files /dev/null and b/app/src/main/assets/images/pneu.jpg differ diff --git a/app/src/main/assets/images/poigneeCombo.jpg b/app/src/main/assets/images/poigneeCombo.jpg new file mode 100644 index 0000000..f1f6455 Binary files /dev/null and b/app/src/main/assets/images/poigneeCombo.jpg differ diff --git a/app/src/main/assets/images/poigneeGachette.jpg b/app/src/main/assets/images/poigneeGachette.jpg new file mode 100644 index 0000000..d84e248 Binary files /dev/null and b/app/src/main/assets/images/poigneeGachette.jpg differ diff --git a/app/src/main/assets/images/poigneeTournante.jpg b/app/src/main/assets/images/poigneeTournante.jpg new file mode 100644 index 0000000..8342d6c Binary files /dev/null and b/app/src/main/assets/images/poigneeTournante.jpg differ diff --git a/app/src/main/assets/images/poigneefrein.jpg b/app/src/main/assets/images/poigneefrein.jpg new file mode 100644 index 0000000..dd0bab3 Binary files /dev/null and b/app/src/main/assets/images/poigneefrein.jpg differ diff --git a/app/src/main/assets/images/poignees.jpg b/app/src/main/assets/images/poignees.jpg new file mode 100644 index 0000000..dedc8bb Binary files /dev/null and b/app/src/main/assets/images/poignees.jpg differ diff --git a/app/src/main/assets/images/porteBagage.jpg b/app/src/main/assets/images/porteBagage.jpg new file mode 100644 index 0000000..fa3e8f2 Binary files /dev/null and b/app/src/main/assets/images/porteBagage.jpg differ diff --git a/app/src/main/assets/images/portebidon.jpg b/app/src/main/assets/images/portebidon.jpg new file mode 100644 index 0000000..c0fefe9 Binary files /dev/null and b/app/src/main/assets/images/portebidon.jpg differ diff --git a/app/src/main/assets/images/potenceHeadset.jpg b/app/src/main/assets/images/potenceHeadset.jpg new file mode 100644 index 0000000..7d05dc4 Binary files /dev/null and b/app/src/main/assets/images/potenceHeadset.jpg differ diff --git a/app/src/main/assets/images/potencePlongeur.jpg b/app/src/main/assets/images/potencePlongeur.jpg new file mode 100644 index 0000000..509da63 Binary files /dev/null and b/app/src/main/assets/images/potencePlongeur.jpg differ diff --git a/app/src/main/assets/images/rayon.jpg b/app/src/main/assets/images/rayon.jpg new file mode 100644 index 0000000..e0418c2 Binary files /dev/null and b/app/src/main/assets/images/rayon.jpg differ diff --git a/app/src/main/assets/images/roueLibre.jpg b/app/src/main/assets/images/roueLibre.jpg new file mode 100644 index 0000000..0eda6b0 Binary files /dev/null and b/app/src/main/assets/images/roueLibre.jpg differ diff --git a/app/src/main/assets/images/rouearriere.jpg b/app/src/main/assets/images/rouearriere.jpg new file mode 100644 index 0000000..b329f89 Binary files /dev/null and b/app/src/main/assets/images/rouearriere.jpg differ diff --git a/app/src/main/assets/images/roueavant.jpg b/app/src/main/assets/images/roueavant.jpg new file mode 100644 index 0000000..654e58f Binary files /dev/null and b/app/src/main/assets/images/roueavant.jpg differ diff --git a/app/src/main/assets/images/selle.jpg b/app/src/main/assets/images/selle.jpg new file mode 100644 index 0000000..2ab47dc Binary files /dev/null and b/app/src/main/assets/images/selle.jpg differ diff --git a/app/src/main/assets/images/tigePlongeur.jpg b/app/src/main/assets/images/tigePlongeur.jpg new file mode 100644 index 0000000..d59127a Binary files /dev/null and b/app/src/main/assets/images/tigePlongeur.jpg differ diff --git a/app/src/main/assets/images/tigeSelle.jpg b/app/src/main/assets/images/tigeSelle.jpg new file mode 100644 index 0000000..e2885cf Binary files /dev/null and b/app/src/main/assets/images/tigeSelle.jpg differ diff --git a/app/src/main/assets/images/visserie.jpg b/app/src/main/assets/images/visserie.jpg new file mode 100644 index 0000000..9c09d27 Binary files /dev/null and b/app/src/main/assets/images/visserie.jpg differ diff --git a/app/src/main/assets/images/visseriefrein.jpg b/app/src/main/assets/images/visseriefrein.jpg new file mode 100644 index 0000000..e49fe4a Binary files /dev/null and b/app/src/main/assets/images/visseriefrein.jpg differ diff --git a/app/src/main/assets/images/visseriepedalier.jpg b/app/src/main/assets/images/visseriepedalier.jpg new file mode 100644 index 0000000..7293718 Binary files /dev/null and b/app/src/main/assets/images/visseriepedalier.jpg differ diff --git a/app/src/main/assets/images/visserieroue.jpg b/app/src/main/assets/images/visserieroue.jpg new file mode 100644 index 0000000..1574c5f Binary files /dev/null and b/app/src/main/assets/images/visserieroue.jpg differ diff --git a/app/src/main/assets/images/visserietransmission.jpg b/app/src/main/assets/images/visserietransmission.jpg new file mode 100644 index 0000000..fc8f7bb Binary files /dev/null and b/app/src/main/assets/images/visserietransmission.jpg differ diff --git a/app/src/main/assets/pieces.json b/app/src/main/assets/pieces.json new file mode 100644 index 0000000..d8b4921 --- /dev/null +++ b/app/src/main/assets/pieces.json @@ -0,0 +1,112 @@ +{ + "globalItems": [ + {"name": "Visserie classique", "minPrice": 0, "maxPrice": 1, "image": "visserie"} + ], + "categories": [ + { + "name": "Structure", + "bgColor": "#FF0000", + "textColor": "#000000", + "items": [ + {"name": "Cadre", "minPrice": 20, "maxPrice": 40, "image": "cadre"}, + {"name": "Selle", "minPrice": 1, "maxPrice": 7, "image": "selle"}, + {"name": "Tige de selle", "minPrice": 1, "maxPrice": 10, "image": "tigeselle"}, + {"name": "Collier de selle", "minPrice": 1, "maxPrice": 4, "image": "collierselle"} + ] + }, + { + "name": "Direction", + "bgColor": "#CC00FF", + "textColor": "#000000", + "items": [ + {"name": "Fourche", "minPrice": 10, "maxPrice": 20, "image": "fourche"}, + {"name": "Jeu de direction", "minPrice": 1, "maxPrice": 8, "image": "jeudirection"}, + {"name": "Potence headset", "minPrice": 2, "maxPrice": 8, "image": "potenceheadset"}, + {"name": "Potence plongeur", "minPrice": 2, "maxPrice": 8, "image": "potenceplongeur"}, + {"name": "Tige plongeur", "minPrice": 1, "maxPrice": 4, "image": "tigeplongeur"} + ] + }, + { + "name": "Guidon", + "bgColor": "#F16000", + "textColor": "#000000", + "items": [ + {"name": "Poignée combo", "minPrice": 2, "maxPrice": 8, "image": "poigneecombo"}, + {"name": "Poignée frein", "minPrice": 2, "maxPrice": 8, "image": "poigneefrein"}, + {"name": "Poignée gachette", "minPrice": 2, "maxPrice": 8, "image": "poigneegachette"}, + {"name": "Poignée tournante", "minPrice": 2, "maxPrice": 6, "image": "poigneetournante"}, + {"name": "Gaine", "minPrice": 0, "maxPrice": 2, "image": "gaine"}, + {"name": "Câble", "minPrice": 0, "maxPrice": 1, "image": "cable"}, + {"name": "Poignée", "minPrice": 1, "maxPrice": 4, "image": "poignees"}, + {"name": "Cintre", "minPrice": 5, "maxPrice": 10,"image": "cintre"} + ] + }, + { + "name": "Freins", + "bgColor": "#FF00FF", + "textColor": "#000000", + "items": [ + {"name": "Étrier patin", "minPrice": 2, "maxPrice": 8, "image": "etrierpatin"}, + {"name": "Étrier disque", "minPrice": 2, "maxPrice": 8, "image": "etrierdisque"}, + {"name": "Patins", "minPrice": 1, "maxPrice": 2, "image": "patins"}, + {"name": "Disque", "minPrice": 1, "maxPrice": 5, "image": "disque"}, + {"name": "Visserie frein", "minPrice": 0, "maxPrice": 1, "image": "visseriefrein"} + ] + }, + { + "name": "Roue", + "bgColor": "#0000FF", + "textColor": "#FFFFFF", + "items": [ + {"name": "Roue avant", "minPrice": 20, "maxPrice": 40, "image": "roueavant"}, + {"name": "Roue arrière", "minPrice": 20, "maxPrice": 40, "image": "rouearriere"}, + {"name": "Axe rapide", "minPrice": 1, "maxPrice": 2, "image": "axerapide"}, + {"name": "Axe roue", "minPrice": 1, "maxPrice": 3, "image": "axeroue"}, + {"name": "Rayon", "minPrice": 0, "maxPrice": 2, "image": "rayon"}, + {"name": "Moyeu avant", "minPrice": 2, "maxPrice": 8, "image": "moyeuavant"}, + {"name": "Moyeu arrière", "minPrice": 2, "maxPrice": 10, "image": "moyeuarriere"}, + {"name": "Pneu", "minPrice": 10, "maxPrice": 20, "image": "pneu"}, + {"name": "Visserie roue", "minPrice": 0, "maxPrice": 1, "image": "visserieroue"} + ] + }, + { + "name": "Transmission", + "bgColor": "#FFCC00", + "textColor": "#000000", + "items": [ + {"name": "Dérailleur avant", "minPrice": 2, "maxPrice": 10, "image": "fd"}, + {"name": "Dérailleur arrière", "minPrice": 4, "maxPrice": 12, "image": "rd"}, + {"name": "Cassette", "minPrice": 2, "maxPrice": 6, "image": "cassette"}, + {"name": "Roue libre", "minPrice": 2, "maxPrice": 6, "image": "rouelibre"}, + {"name": "Chaîne", "minPrice": 2, "maxPrice": 6, "image": "chaine"}, + {"name": "Visserie transmission", "minPrice": 0, "maxPrice": 1, "image": "visserietransmission"} + ] + }, + { + "name": "Pédalier", + "bgColor": "#00FFFF", + "textColor": "#000000", + "items": [ + {"name": "Boîtier pédalier", "minPrice": 4, "maxPrice": 12, "image": "boitierpedalier"}, + {"name": "Manivelle gauche", "minPrice": 4, "maxPrice": 12, "image": "manivellegauche"}, + {"name": "Manivelle droite", "minPrice": 4, "maxPrice": 12, "image": "manivelledroite"}, + {"name": "Pédalier complet", "minPrice": 6, "maxPrice": 15, "image": "pedaliercomplet"}, + {"name": "Pédales", "minPrice": 2, "maxPrice": 8, "image": "pedales"}, + {"name": "Plateau", "minPrice": 2, "maxPrice": 8, "image": "plateau"}, + {"name": "Visserie pédalier", "minPrice": 0, "maxPrice": 1, "image": "visseriepedalier"} + ] + }, + { + "name": "Accessoires", + "bgColor": "#00FF00", + "textColor": "#000000", + "items": [ + {"name": "Béquille", "minPrice": 2, "maxPrice": 8, "image": "bequille"}, + {"name": "Garde boue", "minPrice": 2, "maxPrice": 8, "image": "gardeboue"}, + {"name": "Lampe", "minPrice": 1, "maxPrice": 5, "image": "lampe"}, + {"name": "Porte bagage", "minPrice": 2, "maxPrice": 10, "image": "portebagage"}, + {"name": "Porte bidon", "minPrice": 0, "maxPrice": 1, "image": "portebidon"} + ] + } + ] +} diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..aed5b40 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/stock/pignon/CartActionHelper.java b/app/src/main/java/com/stock/pignon/CartActionHelper.java new file mode 100644 index 0000000..700ef99 --- /dev/null +++ b/app/src/main/java/com/stock/pignon/CartActionHelper.java @@ -0,0 +1,207 @@ +// CartActionHelper.java +package com.stock.pignon; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Environment; +import android.text.Html; +import android.util.Log; +import android.view.View; +import android.widget.GridLayout; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.ImageView; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.bumptech.glide.Glide; + +/** + * Action on shopping cart: validation, clearing, and persistence + */ +public class CartActionHelper { + + private static final String TAG = "CartActionHelper"; + + /** + * Resets the cart data and refreshes UI + */ + public static void emptyCart(LinearLayout cartList, Context activity) { + CartManager.clear(); + CartViewHelper.updateCartView(cartList, activity); + + if (activity instanceof MainActivity) { + GridLayout grid = ((MainActivity) activity).findViewById(R.id.gridPieces); + refreshGridQuantities(grid); + } + } + + /** + * Handles checkout process: shows summary, saves to disk on confirmation + * and redirects the user after a success message + */ + public static void validateCart(LinearLayout cartList, Context activity) { + List items = CartManager.getItems(); + if (items.isEmpty()) return; + + // Build a readable HTML summary for the confirmation dialog + StringBuilder recap = new StringBuilder(); + + // Show a line for each item + for (CartItem item : items) { + recap.append(activity.getString(R.string.popup_item, + item.getQuantity(), + item.getName(), + item.getTotalMin(), + item.getTotalMax())); + } + + // Show total price range + recap.append(activity.getString(R.string.popup_total, + CartManager.getGlobalTotalMin(), + CartManager.getGlobalTotalMax())); + + // Build dialog with HTML summary and buttons + new AlertDialog.Builder(activity) + .setTitle(activity.getString(R.string.popup_name)) + .setMessage(Html.fromHtml(recap.toString())) + .setPositiveButton("Oui", (dialog, which) -> { + // Save to storage + saveCartToExternalFile(items); + + // Clear current session + CartManager.clear(); + CartViewHelper.updateCartView(cartList, activity); + + // Show success message and close + showSuccessAndRedirect(activity); + }) + .setNegativeButton("Non", null) + .show(); + } + + /** + * Displays a thank you popup and returns to the main menu after 2 seconds. + */ + private static void showSuccessAndRedirect(Context activity) { + AlertDialog merciDialog = new AlertDialog.Builder(activity) + .setMessage(activity.getString(R.string.popup_end)) + // Info popup, not cancelable + .setCancelable(false) + .create(); + merciDialog.show(); + + + new Handler().postDelayed(() -> { + // Close dialog + merciDialog.dismiss(); + // Go to home if not already + if (!(activity instanceof MainActivity)) { + Intent intent = new Intent(activity, MainActivity.class); + // Clear the backstack so the user can't "go back" to a validated cart + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + activity.startActivity(intent); + } + }, 2000); + } + + /** + * Merges current cart items with the existing stock file on the SD Card. + */ + private static void saveCartToExternalFile(List cartItems) { + File stockFile = new File(Environment.getExternalStorageDirectory(), Config.STOCK_FILE_NAME); + Gson gson = new Gson(); + Map stockMap; + + // Load data into a Map (Key: Item Name, Value: Total Quantity) + stockMap = loadExistingStock(stockFile, gson); + + // Merge with new items from current cart + for (CartItem item : cartItems) { + // Get previous quantity + Integer qtyObj = stockMap.get(item.getName()); + int currentQty = (qtyObj != null) ? qtyObj : 0; + // Add + stockMap.put(item.getName(), currentQty + item.getQuantity()); + } + + // Overwrite the file with updated data + // Needs WRITE_EXTERNAL_STORAGE permission + try (FileOutputStream fos = new FileOutputStream(stockFile); + @SuppressWarnings("CharsetObjectCanBeUsed") + OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8")) { + + // Writing directly to the stream + gson.toJson(stockMap, writer); + Log.i(TAG, "Stock updated successfully at: " + stockFile.getAbsolutePath()); + + } catch (Exception e) { + Log.e(TAG, "Failed to write stock file", e); + } + } + + /** + * Reads the current stock file. If the file is missing or corrupted, returns an empty map. + */ + private static Map loadExistingStock(File stockFile, Gson gson) { + if (!stockFile.exists()) return new HashMap<>(); + + try (FileInputStream fis = new FileInputStream(stockFile); + @SuppressWarnings("CharsetObjectCanBeUsed") + InputStreamReader reader = new InputStreamReader(fis, "UTF-8")) { + + // Type definition for Map required by GSON : + Type type = new TypeToken>(){}.getType(); + // Read and fill map + Map result = gson.fromJson(reader, type); + + return (result != null) ? result : new HashMap<>(); + + } catch (Exception e) { + Log.e(TAG, "Error reading existing stock, starting fresh", e); + return new HashMap<>(); + } + } + + /** + * Updates the UI grid to reflect quantities. + */ + public static void refreshGridQuantities(GridLayout grid) { + if (grid == null) return; + + // Iterate through all child views in the grid + for (int i = 0; i < grid.getChildCount(); i++) { + View itemView = grid.getChildAt(i); + + // Locate the name and quantity views inside the current item layout + TextView nameView = itemView.findViewById(R.id.itemName); + TextView qtyView = itemView.findViewById(R.id.qtyView); + + + if (nameView != null && qtyView != null) { + // Match names + String itemName = nameView.getText().toString(); + + // Fetch current quantity from centralized CartManager + CartItem cartItem = CartManager.getItemByName(itemName); + int quantity = (cartItem != null) ? cartItem.getQuantity() : 0; + + // Update the visual counter + qtyView.setText(String.valueOf(quantity)); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stock/pignon/CartItem.java b/app/src/main/java/com/stock/pignon/CartItem.java new file mode 100644 index 0000000..a799af3 --- /dev/null +++ b/app/src/main/java/com/stock/pignon/CartItem.java @@ -0,0 +1,27 @@ +// CartItem.java +package com.stock.pignon; + +public class CartItem { + private final String name; + private final int minPrice; + private final int maxPrice; + private int quantity; + private final String imageFile; // Image name without extension + + public CartItem(String name, int minPrice, int maxPrice, int quantity, String imageFile) { + this.name = name; + this.minPrice = minPrice; + this.maxPrice = maxPrice; + this.quantity = quantity; + this.imageFile = imageFile; + } + + public String getName() { return name; } + public int getMinPrice() { return minPrice; } + public int getMaxPrice() { return maxPrice; } + public int getQuantity() { return quantity; } + public void setQuantity(int quantity) { this.quantity = quantity; } + public int getTotalMin() { return minPrice * quantity; } + public int getTotalMax() { return maxPrice * quantity; } + public String getImageFile() { return imageFile; } +} diff --git a/app/src/main/java/com/stock/pignon/CartManager.java b/app/src/main/java/com/stock/pignon/CartManager.java new file mode 100644 index 0000000..c5184db --- /dev/null +++ b/app/src/main/java/com/stock/pignon/CartManager.java @@ -0,0 +1,60 @@ +// CartManager.java +package com.stock.pignon; + +import java.util.ArrayList; +import java.util.List; + +public class CartManager { + // Unified and global list for whole application + private static final List items = new ArrayList<>(); + + // Private constructor: utility class, it should not be instantiated + private CartManager() {} + + // Returns the direct reference to the list to save memory on older devices + public static List getItems() { + return items; + } + + public static CartItem getItemByName(String name) { + if (name == null) return null; + for (CartItem item : items) { + // if item.getName() is null, avoid crash + if (item != null && name.equals(item.getName())) return item; + } + return null; + } + + // Logic for adding, updating or removing items based on quantity, prevents duplicate entries + public static void addOrUpdateItem(String name, int minPrice, int maxPrice, int quantity, String imageFile) { + CartItem current = getItemByName(name); + + if (current != null) { + if (quantity <= 0) { + items.remove(current); + } else { + current.setQuantity(quantity); + } + } else if (quantity > 0) { + items.add(new CartItem(name, minPrice, maxPrice, quantity, imageFile)); + } + } + + // Range-based pricing + + public static int getGlobalTotalMin() { + int total = 0; + for (CartItem item : items) total += item.getTotalMin(); + return total; + } + + public static int getGlobalTotalMax() { + int total = 0; + for (CartItem item : items) total += item.getTotalMax(); + return total; + } + + public static void clear() { + items.clear(); + } +} diff --git a/app/src/main/java/com/stock/pignon/CartViewHelper.java b/app/src/main/java/com/stock/pignon/CartViewHelper.java new file mode 100644 index 0000000..8799007 --- /dev/null +++ b/app/src/main/java/com/stock/pignon/CartViewHelper.java @@ -0,0 +1,165 @@ +// CartViewHelper.java +package com.stock.pignon; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.List; + +/** + * UI Helper to dynamically build and update cart's list + */ +public class CartViewHelper { + + /** + * Refreshes the entire cart side-panel or list + * Clears everything and rebuild on current CartManager data + */ + public static void updateCartView(LinearLayout cartList, Context context) { + if (cartList == null) return; + + // Clear and get current items + cartList.removeAllViews(); + List cartItems = CartManager.getItems(); + + // If empty cart, show it to user + if (cartItems.isEmpty()) { + TextView empty = new TextView(context); + empty.setText(context.getString(R.string.cart_empty)); + empty.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); + empty.setGravity(Gravity.CENTER); + empty.setPadding(0, 50, 0, 0); + cartList.addView(empty); + + updateTotalDisplay(context, 0, 0); + return; + } + + // Build list and calculate total at the same time + int totalMin = 0; + int totalMax = 0; + + for (CartItem item : cartItems) { + totalMin += item.getTotalMin(); + totalMax += item.getTotalMax(); + + // Create and add the row view + cartList.addView(createCartItemView(item, cartList, context)); + } + + // Display global price range at the end + updateTotalDisplay(context, totalMin, totalMax); + } + + /** + * Updates the global price range at the bottom of the screen + */ + private static void updateTotalDisplay(Context context, int min, int max) { + TextView totalView = ((Activity) context).findViewById(R.id.totalView); + if (totalView != null) { + // Using string resources + totalView.setText(context.getString(R.string.price_range, min, max, "€")); + } + } + + /** + * Creates a horizontal row for a single cart item + * Layout: [image] [name / [quantity / price]] [remove button] + */ + private static View createCartItemView(CartItem item, LinearLayout cartList, Context context) { + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + + // Layout parameters + LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + rowParams.setMargins(0, 10, 0, 10); + row.setLayoutParams(rowParams); + + // Image + ImageView image = new ImageView(context); + ImageLoader.loadImage(image, item.getImageFile(),200,200); + LinearLayout.LayoutParams imgParams = new LinearLayout.LayoutParams(100, 100); + imgParams.setMargins(0, 0, 20, 0); + image.setLayoutParams(imgParams); + row.addView(image); + + // Sublayout for name + [quantity + price] + LinearLayout infoLayout = new LinearLayout(context); + infoLayout.setOrientation(LinearLayout.VERTICAL); + infoLayout.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + + // Name + TextView nameView = new TextView(context); + nameView.setText(item.getName()); + nameView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18); + nameView.setTypeface(null, Typeface.BOLD); + infoLayout.addView(nameView); + + // Quantity and price range + TextView detailsView = new TextView(context); + String details = context.getString(R.string.cart_item, item.getQuantity(), item.getTotalMin(), item.getTotalMax()); + detailsView.setText(details); + detailsView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + infoLayout.addView(detailsView); + + row.addView(infoLayout); + + // Remove button + row.addView(createRemoveButton(item, cartList, context)); + + return row; + } + + /** + * Creates the delete button + */ + private static Button createRemoveButton(CartItem item, LinearLayout cartList, Context context) { + Button btn = new Button(context); + btn.setText("✖"); + btn.setTextSize(18); + btn.setTextColor(Color.WHITE); + btn.setGravity(Gravity.CENTER); + + // Conversion DP to PX for consistent size on all screens + float scale = context.getResources().getDisplayMetrics().density; + int sizePx = (int) (48 * scale + 0.5f); + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(sizePx, sizePx); + btn.setLayoutParams(params); + + // Background: grey rounded rectangle (API 17 fallback) + GradientDrawable shape = new GradientDrawable(); + shape.setColor(Color.parseColor("#E53935")); // Reddish to indicate delete + shape.setCornerRadius(4 * scale); + btn.setBackground(shape); + + btn.setOnClickListener(v -> { + // setting quantity to 0 removes the item + CartManager.addOrUpdateItem( + item.getName(), + item.getMinPrice(), + item.getMaxPrice(), + 0, // Setting quantity to 0 triggers removal + item.getImageFile() + ); + + // visual refresh of the cart list + updateCartView(cartList, context); + }); + + return btn; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stock/pignon/Category.java b/app/src/main/java/com/stock/pignon/Category.java new file mode 100644 index 0000000..502feeb --- /dev/null +++ b/app/src/main/java/com/stock/pignon/Category.java @@ -0,0 +1,25 @@ +// Category.java +package com.stock.pignon; + +import java.util.ArrayList; +import java.util.List; + +public class Category { + @SuppressWarnings("unused") + private String name; + @SuppressWarnings("unused") + private String bgColor; + @SuppressWarnings("unused") + private String textColor; + @SuppressWarnings("unused") + private List items; + + public String getName() { return name; } + public String getBgColor() { return bgColor; } + public String getTextColor() { return textColor; } + + // Avoid crash if json isn't readable + public List getItems() { + return items != null ? items : new ArrayList<>(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stock/pignon/Config.java b/app/src/main/java/com/stock/pignon/Config.java new file mode 100644 index 0000000..c4ed597 --- /dev/null +++ b/app/src/main/java/com/stock/pignon/Config.java @@ -0,0 +1,16 @@ +// Config.java +package com.stock.pignon; + +public class Config { + // Folder on sd card + public static final String EXTERNAL_DIR_NAME = "stock_pignon"; + + // Folder on sd card + public static final String IMAGES_SUBDIR_NAME = "images"; + + // Input json + public static final String PIECES_FILE_NAME = "pieces.json"; + + // Output json + public static final String STOCK_FILE_NAME = "stock.json"; +} \ No newline at end of file diff --git a/app/src/main/java/com/stock/pignon/DataLoader.java b/app/src/main/java/com/stock/pignon/DataLoader.java new file mode 100644 index 0000000..289ad94 --- /dev/null +++ b/app/src/main/java/com/stock/pignon/DataLoader.java @@ -0,0 +1,84 @@ +// DataLoader.java +package com.stock.pignon; + +import android.os.Environment; +import android.util.Log; +import com.google.gson.Gson; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +public class DataLoader { + private static final String TAG = "DataLoader"; // For readable logs + private static final String EXTERNAL_DIR = Config.EXTERNAL_DIR_NAME; + private static final String PIECES_FILE = Config.PIECES_FILE_NAME; + + // Raw data + private static List cachedCategories = new ArrayList<>(); + private static List cachedGlobals = new ArrayList<>(); + + + public static void loadData() { + File dir = new File(Environment.getExternalStorageDirectory(), EXTERNAL_DIR); + File jsonFile = new File(dir, PIECES_FILE); + + // Check if file exist + if (!jsonFile.exists()) { + Log.e(TAG, "File doesn't exist : " + jsonFile.getAbsolutePath()); + return; + } + + // Optimized read + // Try-with-resources ensures streams are automatically closed, avoid memory leaks + try (FileInputStream fis = new FileInputStream(jsonFile); + @SuppressWarnings("CharsetObjectCanBeUsed") + InputStreamReader reader = new InputStreamReader(fis, "UTF-8")) { + + // From json to java object + Gson gson = new Gson(); + // Internal class needed for Gson + CategoriesWrapper wrapper = gson.fromJson(reader, CategoriesWrapper.class); + + // Extract lists from wrapper with null-safety + if (wrapper != null) { + cachedGlobals = wrapper.globalItems != null ? wrapper.globalItems : new ArrayList<>(); + cachedCategories = wrapper.categories != null ? wrapper.categories : new ArrayList<>(); + + } + } catch (java.io.FileNotFoundException e) { + Log.e(TAG, "Can't find file", e); + } catch (java.io.IOException e) { + Log.e(TAG, "Read error (I/O)", e); + } catch (Exception e) { + Log.e(TAG, "Parsing JSON error", e); + } + } + + // Compromise between CPU usage and memory usage : keep cache raw data and combinate global and specific items at call + public static List getItemsForCategory(String categoryName) { + + // Add global items + List combinedList = new ArrayList<>(cachedGlobals); + + // Add specific items + for (Category cat : cachedCategories) { + if (cat.getName().equals(categoryName)) { + combinedList.addAll(cat.getItems()); + break; + } + } + return combinedList; + } + + public static List getCategories() { + return cachedCategories; + } + + // Internal class for Gson + private static class CategoriesWrapper { + List globalItems; + List categories; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stock/pignon/ImageLoader.java b/app/src/main/java/com/stock/pignon/ImageLoader.java new file mode 100644 index 0000000..ce69d8b --- /dev/null +++ b/app/src/main/java/com/stock/pignon/ImageLoader.java @@ -0,0 +1,61 @@ +// ImageLoader.java +package com.stock.pignon; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Environment; +import android.widget.ImageView; +import java.io.File; + +public class ImageLoader { + + /** + * Load image from /sdcard/Pignon/, priority JPG then PNG + * If image not found, default fallback + */ + public static void loadImage(ImageView imageView, String fileName, int reqWidth, int reqHeight) { + File storageRoot = Environment.getExternalStorageDirectory(); + File baseDir = new File(storageRoot, Config.EXTERNAL_DIR_NAME); + File dir = new File(baseDir, Config.IMAGES_SUBDIR_NAME); + + File imgFileJpg = new File(dir, fileName + ".jpg"); + File imgFilePng = new File(dir, fileName + ".png"); + File imgFile = imgFileJpg.exists() ? imgFileJpg : (imgFilePng.exists() ? imgFilePng : null); + + if (imgFile != null && imgFile.exists()) { + BitmapFactory.Options options = new BitmapFactory.Options(); + + // Check image size without loading + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(imgFile.getAbsolutePath(), options); + + // Compute size reduction + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // Load reduce size + options.inJustDecodeBounds = false; + Bitmap bmp = BitmapFactory.decodeFile(imgFile.getAbsolutePath(), options); + + if (bmp != null) { + imageView.setImageBitmap(bmp); + } else { + imageView.setImageResource(android.R.drawable.ic_menu_report_image); + } + } else { + // Fallback Android + imageView.setImageResource(android.R.drawable.ic_menu_report_image); + } + } + + private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { + int inSampleSize = 1; + if (options.outHeight > reqHeight || options.outWidth > reqWidth) { + final int halfHeight = options.outHeight / 2; + final int halfWidth = options.outWidth / 2; + while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2; + } + } + return inSampleSize; + } +} diff --git a/app/src/main/java/com/stock/pignon/Item.java b/app/src/main/java/com/stock/pignon/Item.java new file mode 100644 index 0000000..1023d07 --- /dev/null +++ b/app/src/main/java/com/stock/pignon/Item.java @@ -0,0 +1,22 @@ +// Item.java +package com.stock.pignon; + +public class Item { + @SuppressWarnings("unused") + private String name; + @SuppressWarnings("unused") + private int minPrice; + @SuppressWarnings("unused") + private int maxPrice; + @SuppressWarnings("unused") + private String image; + + // Empty constructor for GSON + public Item() {} + + public String getName() { return name; } + public int getMinPrice() { return minPrice; } + public int getMaxPrice() { return maxPrice; } + public String getImage() { return image; } +} + diff --git a/app/src/main/java/com/stock/pignon/MainActivity.java b/app/src/main/java/com/stock/pignon/MainActivity.java new file mode 100644 index 0000000..139fa67 --- /dev/null +++ b/app/src/main/java/com/stock/pignon/MainActivity.java @@ -0,0 +1,304 @@ +package com.stock.pignon; + +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.os.Bundle; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.GridLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.os.Environment; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import androidx.appcompat.app.AppCompatActivity; + +import java.util.List; + +/** + * Main Activity: Central Dashboard for the application. + * Manages category navigation and item selection within a single screen. + */ +public class MainActivity extends AppCompatActivity { + + private static final String TAG = "MainActivity"; + + // UI Components + private LinearLayout cartList; + private LinearLayout homeLayout; + private LinearLayout categoryItemsLayout; + private LinearLayout categoriesLayout; + private GridLayout gridPieces; + private ImageView mainImage; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Hide Android bar + View decorView = getWindow().getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN; + decorView.setSystemUiVisibility(uiOptions); + + // View Binding + cartList = findViewById(R.id.cartList); + homeLayout = findViewById(R.id.homeLayout); + categoryItemsLayout = findViewById(R.id.categoryItemsLayout); + categoriesLayout = findViewById(R.id.categoriesLayout); + gridPieces = findViewById(R.id.gridPieces); + mainImage = findViewById(R.id.mainImage); + + // Setup Back Button + Button backBtn = findViewById(R.id.backToHomeBtn); + if (backBtn != null) { + backBtn.setOnClickListener(v -> showHome()); + } + + // Copy assets to sd card if not founded + copyAssetsIfEmpty(); + // Get data from sd card + DataLoader.loadData(); + + // Initial UI Setup + setupCategoriesMenu(); + loadMainImage(); + + // Initial cart visual sync + CartViewHelper.updateCartView(cartList, this); + } + + /** + * Builds the left sidebar with category buttons using DataLoader's cache. + */ + private void setupCategoriesMenu() { + List categories = DataLoader.getCategories(); + if (categories == null || categories.isEmpty()) { + Log.w(TAG, "No categories found to display."); + return; + } + + categoriesLayout.removeAllViews(); + for (Category category : categories) { + Button btn = createCategoryButton(category); + // On click, we show the items for this specific category + btn.setOnClickListener(v -> showCategory(category.getName())); + categoriesLayout.addView(btn); + } + } + + /** + * Styles category buttons based on the new Category getters. + */ + private Button createCategoryButton(Category category) { + Button btn = new Button(this); + btn.setText(category.getName()); + btn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24); + + float density = getResources().getDisplayMetrics().density; + float radiusPx = 3 * density; + + // Apply background color with transparency (70% alpha) + if (category.getBgColor() != null) { + try { + int color = Color.parseColor(category.getBgColor()); + int alpha = (int) (0.7 * 255); + color = (color & 0x00FFFFFF) | (alpha << 24); + + GradientDrawable shape = new GradientDrawable(); + shape.setColor(color); + shape.setCornerRadius(radiusPx); + btn.setBackground(shape); + } catch (Exception e) { + Log.e(TAG, "Error parsing bgColor for: " + category.getName()); + } + } + + // Apply text color + if (category.getTextColor() != null) { + try { + btn.setTextColor(Color.parseColor(category.getTextColor())); + } catch (Exception e) { + Log.e(TAG, "Error parsing textColor for: " + category.getName()); + } + } + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + params.setMargins(0, 5, 0, 5); + btn.setLayoutParams(params); + + return btn; + } + + /** + * Switches center view to display the grid of items. + * Uses DataLoader.getItemsForCategory() to get the merged list. + */ + private void showCategory(String categoryName) { + homeLayout.setVisibility(View.GONE); + categoryItemsLayout.setVisibility(View.VISIBLE); + + gridPieces.removeAllViews(); + + // Calculate item width for a 4-column grid + int gridWidth = getResources().getDisplayMetrics().widthPixels - dpToPx(370); + int itemWidth = gridWidth / 4; + + // Fetch the PRE-MERGED list (globals + specifics) from DataLoader + List itemsToDisplay = DataLoader.getItemsForCategory(categoryName); + + for (Item item : itemsToDisplay) { + View itemView = LayoutInflater.from(this).inflate(R.layout.item_piece, gridPieces, false); + + // Apply width to the inflated view + GridLayout.LayoutParams params = (GridLayout.LayoutParams) itemView.getLayoutParams(); + if (params == null) { + params = new GridLayout.LayoutParams(); + } + params.width = itemWidth; + itemView.setLayoutParams(params); + + bindItemToView(itemView, item); + gridPieces.addView(itemView); + } + } + + /** + * Connects Item data to the inflated layout piece. + */ + private void bindItemToView(View itemView, Item item) { + TextView nom = itemView.findViewById(R.id.itemName); + TextView prix = itemView.findViewById(R.id.itemPrice); + ImageView img = itemView.findViewById(R.id.itemImage); + TextView qtyView = itemView.findViewById(R.id.qtyView); + + nom.setText(item.getName()); + prix.setText(getString(R.string.price_range, item.getMinPrice(), item.getMaxPrice(), "€")); + + // Use your existing ImageLoader helper + ImageLoader.loadImage(img, item.getImage(),200,200); + + // Sync visual quantity with CartManager + CartItem existing = CartManager.getItemByName(item.getName()); + int currentQty = (existing != null) ? existing.getQuantity() : 0; + qtyView.setText(String.valueOf(currentQty)); + + // Listeners for + and - buttons + itemView.findViewById(R.id.plusBtn).setOnClickListener(v -> updateItemQty(item, qtyView, 1)); + itemView.findViewById(R.id.minusBtn).setOnClickListener(v -> updateItemQty(item, qtyView, -1)); + } + + /** + * Updates the local counter and the global cart state simultaneously. + */ + private void updateItemQty(Item item, TextView qtyView, int delta) { + int current = Integer.parseInt(qtyView.getText().toString()); + int next = Math.max(0, current + delta); + + qtyView.setText(String.valueOf(next)); + + // Central data update + CartManager.addOrUpdateItem(item.getName(), item.getMinPrice(), item.getMaxPrice(), next, item.getImage()); + + // Refresh sidebar cart list + CartViewHelper.updateCartView(cartList, this); + } + + private void showHome() { + categoryItemsLayout.setVisibility(View.GONE); + homeLayout.setVisibility(View.VISIBLE); + } + + private void loadMainImage() { + // Fallback for home screen image + ImageLoader.loadImage(mainImage, "_velo", 800,800); + } + + // --- Button Actions (linked via android:onClick in XML) --- + + public void emptyCart(View view) { + CartActionHelper.emptyCart(cartList, this); + } + + public void validateCart(View view) { + CartActionHelper.validateCart(cartList, this); + } + + private int dpToPx(int dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); + } + + @Override + protected void onResume() { + super.onResume(); + // Ensure cart is up to date if returning to the activity + CartViewHelper.updateCartView(cartList, this); + } + + private void copyAssetsIfEmpty() { + File folder = new File(Environment.getExternalStorageDirectory(), Config.EXTERNAL_DIR_NAME); + + // First safety : is folder already in sdcard ? + if (!folder.exists()) { + // Second safety : are we able to create folder ? + if (folder.mkdirs()) { + // Copy JSON file + copyFileFromAssets(Config.PIECES_FILE_NAME, new File(folder, Config.PIECES_FILE_NAME)); + + // Copy images subfolder + File imgFolder = new File(folder, Config.IMAGES_SUBDIR_NAME); + if (imgFolder.mkdirs()) { + copyFolderFromAssets(Config.IMAGES_SUBDIR_NAME, imgFolder); + } + } + } + } + + private void copyFolderFromAssets(String assetDirName, File destDir) { + try { + String[] files = getAssets().list(assetDirName); + if (files != null) { + // For every image + for (String fileName : files) { + // Create source path + String assetPath = assetDirName + "/" + fileName; + // Create dest path + File destFile = new File(destDir, fileName); + // Copy + copyFileFromAssets(assetPath, destFile); + } + } + } catch (IOException e) { + Log.e("MainActivity", "Erreur listing assets: " + assetDirName, e); + } + } + + private void copyFileFromAssets(String assetName, File destFile) { + // Optimized read + // Try-with-resources ensures streams are automatically closed, avoid memory leaks + try (InputStream in = getAssets().open(assetName); + OutputStream out = new FileOutputStream(destFile)) { + + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + Log.d("MainActivity", "Succès : " + assetName + " copié."); + + } catch (IOException e) { + Log.e("MainActivity", "Erreur copie asset: " + assetName, e); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ac8c80c --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + +