Compare commits
13 Commits
de2915064a
...
v0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 0dbe47ad3b | |||
| 1df2d95586 | |||
| ebb10ed61b | |||
| e274352f98 | |||
| 36dec7123f | |||
| f4be7a90eb | |||
| 2c225f6821 | |||
| 7b53f8407c | |||
| 7bea2fa9cf | |||
| 5e69a13487 | |||
| cca2183e3d | |||
| 049a8d08cb | |||
| 352a6e797a |
10
Readme.md
@@ -2,6 +2,8 @@
|
||||
|
||||
Application Android (4.2+) de gestion de stock et d'aide à la vente à prix libre. Conçue pour l'[**Atelier du Pignon**](https://www.atelierdupignon.fr/), atelier d'auto-réparation à Nantes.
|
||||
|
||||

|
||||
|
||||
## 🛠 Fonctionnalités
|
||||
- **Catalogue visuel** : Permet aux adhérent·es de noter rapidement les pièces qu'iels ont emportées.
|
||||
- **Prix libre & conscient** : Calculateur de panier affichant une fourchette de prix suggérée.
|
||||
@@ -28,4 +30,10 @@ Application Android (4.2+) de gestion de stock et d'aide à la vente à prix lib
|
||||
## ⚖️ Licence
|
||||
|
||||
Ce projet est sous licence **CC BY-NC-SA 4.0**.
|
||||
Cela signifie que vous êtes libre de partager et d'adapter le code, tant que vous citez l'auteur original, que vous n'en faites pas un usage commercial, et que vous diffusez vos modifications sous la même licence.
|
||||
Cela signifie que vous êtes libre de partager et d'adapter le code, tant que vous citez l'auteur original, que vous n'en faites pas un usage commercial, et que vous diffusez vos modifications sous la même licence.
|
||||
|
||||
## 👀 Screenshots
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ android {
|
||||
applicationId "com.stock.pignon"
|
||||
minSdkVersion 17
|
||||
targetSdkVersion 36
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
versionCode 8
|
||||
versionName "0.6.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -41,6 +41,9 @@ dependencies {
|
||||
// GIF
|
||||
implementation 'com.github.bumptech.glide:glide:3.8.0'
|
||||
|
||||
// NanoHTTPD
|
||||
implementation 'org.nanohttpd:nanohttpd:2.3.1'
|
||||
|
||||
// Tests
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
||||
316
app/src/main/assets/editor.html
Normal file
@@ -0,0 +1,316 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Éditeur de Catalogue</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
padding: 20px;
|
||||
background: #eee;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='number'] {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.img-p {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
background: #f0f0f0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: #0049AF;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: #2E7D32;
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-del {
|
||||
background: #d32f2f;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.link-mod {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #0049AF;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.actions-bar {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #F54927;
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-cat {
|
||||
background: #0049AF;
|
||||
color: white;
|
||||
padding: 15px 25px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
background: #000000;
|
||||
color: white;
|
||||
padding: 15px 25px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.input-h2 {
|
||||
font-size: 1.5rem !important;
|
||||
font-weight: bold !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
color: #555 !important;
|
||||
width: auto;
|
||||
max-width: 20%;
|
||||
border-bottom: 1px dashed #ccc !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-h2:focus {
|
||||
border-bottom: 2px solid #0049AF;
|
||||
color: #0049AF;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Éditeur de Catalogue</h1>
|
||||
<form id="editor-form" accept-charset="UTF-8">
|
||||
|
||||
{{GENERATED_CONTENT}}
|
||||
|
||||
<div class="actions-bar">
|
||||
<button type="button" class="btn-back" onclick="window.location.href= '/'">← Retour </button>
|
||||
<button type="button" class="btn-cat" onclick="addCategory()">+ Catégorie</button>
|
||||
<button type="button" onclick="sendData()" class="btn-save" style="position:static; transform:none;">💾
|
||||
Enregistrer tout</button>
|
||||
<button type="button" class="btn-cancel" onclick="handleCancel(event)">✖ Tout annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function addRow(cat) {
|
||||
var t = document.getElementById('table-' + cat);
|
||||
var r = t.insertRow(-1);
|
||||
var id = Date.now();
|
||||
|
||||
r.innerHTML = '<td>' +
|
||||
'<img src="" class="img-p" onerror="this.src=\'https://placehold.co/60?text=?\'" style="display:block; margin-bottom:5px;">' +
|
||||
'<input type="file" accept=".jpg,.png" style="font-size:10px; width:70px" onchange="uImg(this)">' +
|
||||
'<input type="hidden" name="new|' + cat + '|' + id + '|img" value="">' +
|
||||
'</td>' +
|
||||
'<td><input type="text" name="new|' + cat + '|' + id + '|name" placeholder="Nom..." required></td>' +
|
||||
'<td><input type="number" name="new|' + cat + '|' + id + '|min" value="0" min="0" required></td>' +
|
||||
'<td><input type="number" name="new|' + cat + '|' + id + '|max" value="0" min="0" required></td>' +
|
||||
'<td><button type="button" class="btn-del" onclick="var l=this.parentNode.parentNode; l.parentNode.removeChild(l);">×</button></td>';
|
||||
}
|
||||
|
||||
function uImg(element) {
|
||||
var f = element.files[0];
|
||||
if (!f) return;
|
||||
|
||||
var td = element.parentNode;
|
||||
var imgTag = td.querySelector('.img-p');
|
||||
var hiddenInput = td.querySelector('input[type="hidden"]');
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
imgTag.src = e.target.result;
|
||||
hiddenInput.value = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(f);
|
||||
}
|
||||
|
||||
function handleCancel(e) {
|
||||
// Avoid form to wait for data to send
|
||||
if (e) e.preventDefault();
|
||||
|
||||
if (confirm("Attention : toutes vos modifications seront perdues. Continuer ?")) {
|
||||
// Random URL parameters ?t= to force browser to forget previous data
|
||||
window.location.href = "/edit?t=" + Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
function addCategory() {
|
||||
var name = prompt("Nom de la nouvelle catégorie ?");
|
||||
if (!name || name.trim() === "") return;
|
||||
|
||||
// Temp ID
|
||||
var tempId = "newcat_" + Date.now();
|
||||
|
||||
var container = document.createElement('div');
|
||||
container.className = 'card';
|
||||
container.innerHTML =
|
||||
'<h2>' +
|
||||
'<input type="text" class="input-h2" name="cat|' + tempId + '|name" value="' + name + '">' +
|
||||
'<div style="display:inline-block; vertical-align:middle; margin: 0 15px;">' +
|
||||
'<small>Fond:</small><br><input type="color" name="cat|' + tempId + '|bgColor" value="#0049AF" style="width:30px; height:25px; border:none; cursor:pointer;">' +
|
||||
'</div>' +
|
||||
'<div style="display:inline-block; vertical-align:middle; margin-right:15px;">' +
|
||||
'<small>Texte:</small><br><input type="color" name="cat|' + tempId + '|textColor" value="#FFFFFF" style="width:30px; height:25px; border:none; cursor:pointer;">' +
|
||||
'</div>' +
|
||||
' <button type="button" class="btn-del" onclick="if(confirm(\'Supprimer cette catégorie ?\')) this.closest(\'.card\').remove()">×</button>' +
|
||||
'</h2>' +
|
||||
'<table id="table-' + tempId + '">' +
|
||||
'<tr><th>Image</th><th>Nom</th><th>Prix Min</th><th>Prix Max</th><th></th></tr>' +
|
||||
'</table>' +
|
||||
'<button type="button" class="btn-add" onclick="addRow(\'' + tempId + '\')">+ Ajouter article</button>';
|
||||
|
||||
var form = document.querySelector('form');
|
||||
var actionBar = document.querySelector('.actions-bar');
|
||||
form.insertBefore(container, actionBar);
|
||||
|
||||
container.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function sendData() {
|
||||
const form = document.getElementById('editor-form');
|
||||
const data = {
|
||||
cat_order_list: "",
|
||||
items: {}
|
||||
};
|
||||
|
||||
// Browse showed categories
|
||||
const catOrder = [];
|
||||
document.querySelectorAll('.card').forEach((card, catIdx) => {
|
||||
let techId = card.querySelector('input[name^="cat|"]')?.name.split('|')[1] || "c" + catIdx; // c0, c1 but avoid globals
|
||||
catOrder.push(techId);
|
||||
|
||||
const catInput = card.querySelector('input[name$="|name"]');
|
||||
const bgInput = card.querySelector('input[name$="|bgColor"]');
|
||||
const textInput = card.querySelector('input[name$="|textColor"]');
|
||||
if (!catInput) return;
|
||||
|
||||
data.items["cat|" + techId + "|name"] = catInput.value;
|
||||
data.items["cat|" + techId + "|bgColor"] = bgInput ? bgInput.value : "#0049AF";
|
||||
data.items["cat|" + techId + "|textColor"] = textInput ? textInput.value : "#FFFFFF";
|
||||
|
||||
// Browse each line
|
||||
card.querySelectorAll('table tr').forEach((row, itemIdx) => {
|
||||
if (itemIdx === 0) return; // Skip title line (th)
|
||||
const realIdx = itemIdx - 1; // Start at 0
|
||||
row.querySelectorAll('input').forEach(input => {
|
||||
const parts = input.name.split('|');
|
||||
if (parts.length >= 4) {
|
||||
// parts[0] = "item" ou "new", parts[3] = "name" ou "min" ou "max"
|
||||
const finalKey = "item|" + techId + "|" + realIdx + "|" + parts[3];
|
||||
data.items[finalKey] = input.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
data.cat_order_list = catOrder.join(',');
|
||||
|
||||
// Create new clean struct : cat|c0|name, item|c0|0|name...
|
||||
fetch('/save_editor', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
alert("✅ Enregistré !");
|
||||
window.location.reload(); // Optionnel : rafraîchit pour voir le résultat propre
|
||||
} else {
|
||||
alert("❌ Erreur serveur");
|
||||
}
|
||||
})
|
||||
.catch(() => alert("❌ Erreur réseau"));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
Before Width: | Height: | Size: 8.7 KiB |
BIN
app/src/main/assets/images/axe_pedalier.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
BIN
app/src/main/assets/images/boitier_hollowtech.jpg
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
app/src/main/assets/images/chambre_air.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/assets/images/derailleur_avant_tiragebas.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/assets/images/derailleur_avant_tiragehaut.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/assets/images/etrier_cantilever.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
app/src/main/assets/images/etrier_hydraulique.jpg
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
app/src/main/assets/images/etrier_mafac_caliper.jpg
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
BIN
app/src/main/assets/images/fond_jante.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/assets/images/fourche_suspendue.jpg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
app/src/main/assets/images/frein_pontcentral.jpg
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/assets/images/jante.jpg
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/assets/images/manette_cadre.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
BIN
app/src/main/assets/images/moyeu_arriere_rouelibre.jpg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/assets/images/moyeu_retropedalage.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/assets/images/panier.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
app/src/main/assets/images/plateau_4.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
BIN
app/src/main/assets/images/pneu_26.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/assets/images/pneu_large.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/assets/images/pneu_petit.jpg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
app/src/main/assets/images/pneu_route.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/assets/images/poignee.jpg
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 16 KiB |
BIN
app/src/main/assets/images/pompe.jpg
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
BIN
app/src/main/assets/images/potence_headset_reglable.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
BIN
app/src/main/assets/images/potence_plongeur_reglable.jpg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/assets/images/retroviseur.jpg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
BIN
app/src/main/assets/images/roulement_direction.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/assets/images/roulement_pedalier.jpg
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
BIN
app/src/main/assets/images/velo.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
51
app/src/main/assets/index.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||
<style>
|
||||
body { font-family: sans-serif; line-height: 1.6; padding: 20px; color: #333; max-width: 800px; margin: auto; }
|
||||
h1 { color: #0049AF; border-bottom: 2px solid #0049AF; }
|
||||
h2 { color: #555; margin-top: 30px; }
|
||||
.card { background: #f4f4f4; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #ddd; }
|
||||
.btn { display: inline-block; width: 280px; height: 45px; line-height: 45px; text-align: center; background: #0049AF; color: white; text-decoration: none; border-radius: 4px; border: none; cursor: pointer; font-weight: bold; box-sizing: border-box; padding: 0; -webkit-appearance: none; font-family: inherit; font-size: 14px; font-style: normal; letter-spacing: normal; margin: 10px 5px; vertical-align: middle;}
|
||||
.btn-download { background: #2E7D32; }
|
||||
input[type='file'] { margin: 10px 0; }
|
||||
.status { font-weight: bold; color: green; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Application : stock-pignon</h1>
|
||||
<h2>Gestion des stocks</h2>
|
||||
<div class='card'>
|
||||
<h3>Récupérer les sacoches des adhérent·es</h3>
|
||||
<p>Télécharger le décompte des pièces sorties de l'atelier.</p>
|
||||
<a href='/output_json' class='btn'>Voir stock.json</a>
|
||||
<a href='/output_csv' class='btn'>Voir stock.csv</a>
|
||||
<a href='/download_output_json' class='btn'>Télécharger stock.json</a>
|
||||
<a href='/download_output_csv' class='btn'>Télécharger stock.csv</a>
|
||||
</div>
|
||||
<h2>Modifier le catalogue</h2>
|
||||
<div class='card'>
|
||||
<h3>Gestion du fichier JSON</h3>
|
||||
<p>Catalogue actuel : <a href='/download_input' class='btn'>Télécharger pieces.json</a></p>
|
||||
<hr>
|
||||
<form action='/upload_json' method='post' enctype='multipart/form-data'>
|
||||
<p><strong>Envoyer un nouveau catalogue :</strong></p>
|
||||
<input type='file' name='json_file' accept='.json'><br>
|
||||
<input type='submit' value='Remplacer pieces.json' class='btn'>
|
||||
</form>
|
||||
</div>
|
||||
<div class='card'>
|
||||
<h3>Gestion des images</h3>
|
||||
<form action='/upload_images' method='post' enctype='multipart/form-data'>
|
||||
<p><strong>Ajouter/Modifier des images (PNG/JPG) :</strong></p>
|
||||
<input type='file' name='images' accept='image/*' multiple><br>
|
||||
<input type='submit' value='Envoyer les images' class='btn'>
|
||||
</form>
|
||||
</div><div class='card'>
|
||||
<h3>Modifier dans le navigateur</h3>
|
||||
<p><a href='/edit' class='btn'>Editeur en ligne</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,10 +8,11 @@
|
||||
"bgColor": "#FF0000",
|
||||
"textColor": "#000000",
|
||||
"items": [
|
||||
{"name": "Vélo", "minPrice": 20, "maxPrice": 40, "image": "velo"},
|
||||
{"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": "Tige de selle", "minPrice": 1, "maxPrice": 10, "image": "tige_selle"},
|
||||
{"name": "Collier de selle", "minPrice": 1, "maxPrice": 4, "image": "collier_selle"}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -20,10 +21,14 @@
|
||||
"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": "Fourche suspendue", "minPrice": 10, "maxPrice": 20, "image": "fourche_suspendue"},
|
||||
{"name": "Roulement direction", "minPrice": 1, "maxPrice": 8, "image": "roulement_direction"},
|
||||
{"name": "Jeu de direction", "minPrice": 1, "maxPrice": 8, "image": "jeu_direction"},
|
||||
{"name": "Potence headset", "minPrice": 2, "maxPrice": 8, "image": "potence_headset"},
|
||||
{"name": "Potence headset réglable", "minPrice": 2, "maxPrice": 8, "image": "potence_headset_reglable"},
|
||||
{"name": "Potence plongeur", "minPrice": 2, "maxPrice": 8, "image": "potence_plongeur"},
|
||||
{"name": "Potence plongeur réglable", "minPrice": 2, "maxPrice": 8, "image": "potence_plongeur_reglable"},
|
||||
{"name": "Tige plongeur", "minPrice": 1, "maxPrice": 4, "image": "tige_plongeur"}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -31,13 +36,14 @@
|
||||
"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": "Poignée combo", "minPrice": 2, "maxPrice": 8, "image": "poignee_combo"},
|
||||
{"name": "Poignée frein", "minPrice": 2, "maxPrice": 8, "image": "poignee_frein"},
|
||||
{"name": "Poignée gachette", "minPrice": 2, "maxPrice": 8, "image": "poignee_gachette"},
|
||||
{"name": "Poignée tournante", "minPrice": 2, "maxPrice": 6, "image": "poignee_tournante"},
|
||||
{"name": "Manette cadre", "minPrice": 2, "maxPrice": 6, "image": "manette_cadre"},
|
||||
{"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": "Poignée", "minPrice": 1, "maxPrice": 4, "image": "poignee"},
|
||||
{"name": "Cintre", "minPrice": 5, "maxPrice": 10,"image": "cintre"}
|
||||
]
|
||||
},
|
||||
@@ -46,11 +52,15 @@
|
||||
"bgColor": "#FF00FF",
|
||||
"textColor": "#000000",
|
||||
"items": [
|
||||
{"name": "Étrier patin", "minPrice": 2, "maxPrice": 8, "image": "etrierpatin"},
|
||||
{"name": "Étrier disque", "minPrice": 2, "maxPrice": 8, "image": "etrierdisque"},
|
||||
{"name": "Étrier V-brake", "minPrice": 2, "maxPrice": 8, "image": "etrier_vbrake"},
|
||||
{"name": "Étrier Cantilever", "minPrice": 2, "maxPrice": 8, "image": "etrier_cantilever"},
|
||||
{"name": "Étrier Caliper ou Mafac", "minPrice": 2, "maxPrice": 8, "image": "etrier_mafac_caliper"},
|
||||
{"name": "Étrier disque câble", "minPrice": 2, "maxPrice": 8, "image": "etrier_cable"},
|
||||
{"name": "Étrier disque hydraulique", "minPrice": 2, "maxPrice": 8, "image": "etrier_hydraulique"},
|
||||
{"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": "Pont central cantilever", "minPrice": 0, "maxPrice": 1, "image": "frein_pontcentral"},
|
||||
{"name": "Visserie frein", "minPrice": 0, "maxPrice": 1, "image": "visserie_frein"}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -58,15 +68,23 @@
|
||||
"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": "Chambre à air", "minPrice": 20, "maxPrice": 40, "image": "chambre_air"},
|
||||
{"name": "Roue avant", "minPrice": 20, "maxPrice": 40, "image": "roue_avant"},
|
||||
{"name": "Roue arrière", "minPrice": 20, "maxPrice": 40, "image": "roue_arriere"},
|
||||
{"name": "Axe rapide", "minPrice": 1, "maxPrice": 2, "image": "axe_rapide"},
|
||||
{"name": "Axe roue", "minPrice": 1, "maxPrice": 3, "image": "axe_roue"},
|
||||
{"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": "Jante", "minPrice": 0, "maxPrice": 2, "image": "jante"},
|
||||
{"name": "Fond de jante", "minPrice": 0, "maxPrice": 2, "image": "fond_jante"},
|
||||
{"name": "Moyeu avant", "minPrice": 2, "maxPrice": 8, "image": "moyeu_avant"},
|
||||
{"name": "Moyeu arrière cassette", "minPrice": 2, "maxPrice": 10, "image": "moyeu_arriere_cassette"},
|
||||
{"name": "Moyeu arrière roue libre", "minPrice": 2, "maxPrice": 10, "image": "moyeu_arriere_rouelibre"},
|
||||
{"name": "Moyeu arrière rétropédalage", "minPrice": 2, "maxPrice": 10, "image": "moyeu_retropedalage"},
|
||||
{"name": "Pneu petite taille", "minPrice": 10, "maxPrice": 20, "image": "pneu_petit"},
|
||||
{"name": "Pneu 26\"", "minPrice": 10, "maxPrice": 20, "image": "pneu_26"},
|
||||
{"name": "Pneu 28\" route", "minPrice": 10, "maxPrice": 20, "image": "pneu_route"},
|
||||
{"name": "Pneu 28\" large", "minPrice": 10, "maxPrice": 20, "image": "pneu_large"},
|
||||
{"name": "Visserie roue", "minPrice": 0, "maxPrice": 1, "image": "visserie_roue"}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -74,12 +92,13 @@
|
||||
"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": "Dérailleur arrière", "minPrice": 2, "maxPrice": 10, "image": "derailleur_arriere"},
|
||||
{"name": "Dérailleur avant tirage bas", "minPrice": 4, "maxPrice": 12, "image": "derailleur_avant_tiragebas"},
|
||||
{"name": "Dérailleur avant tirage haut", "minPrice": 4, "maxPrice": 12, "image": "derailleur_avant_tiragebas"},
|
||||
{"name": "Cassette", "minPrice": 2, "maxPrice": 6, "image": "cassette"},
|
||||
{"name": "Roue libre", "minPrice": 2, "maxPrice": 6, "image": "rouelibre"},
|
||||
{"name": "Roue libre", "minPrice": 2, "maxPrice": 6, "image": "roue_libre"},
|
||||
{"name": "Chaîne", "minPrice": 2, "maxPrice": 6, "image": "chaine"},
|
||||
{"name": "Visserie transmission", "minPrice": 0, "maxPrice": 1, "image": "visserietransmission"}
|
||||
{"name": "Visserie transmission", "minPrice": 0, "maxPrice": 1, "image": "visserie_transmission"}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -87,13 +106,17 @@
|
||||
"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": "Axe pédalier carré ou clavette", "minPrice": 4, "maxPrice": 12, "image": "axe_pedalier"},
|
||||
{"name": "Roulement pédalier", "minPrice": 4, "maxPrice": 12, "image": "roulement_pedalier"},
|
||||
{"name": "Boîtier pédalier cartouche", "minPrice": 4, "maxPrice": 12, "image": "boitier_cartouche"},
|
||||
{"name": "Boîtier pédalier Hollowtech 2", "minPrice": 4, "maxPrice": 12, "image": "boitier_hollowtech"},
|
||||
{"name": "Manivelle gauche", "minPrice": 4, "maxPrice": 12, "image": "manivelle_gauche"},
|
||||
{"name": "Manivelle droite", "minPrice": 4, "maxPrice": 12, "image": "manivelle_droite"},
|
||||
{"name": "Pédalier complet", "minPrice": 6, "maxPrice": 15, "image": "pedalier_complet"},
|
||||
{"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": "Plateau 4 branches", "minPrice": 2, "maxPrice": 8, "image": "plateau_4"},
|
||||
{"name": "Plateau 5 branches", "minPrice": 2, "maxPrice": 8, "image": "plateau_5"},
|
||||
{"name": "Visserie pédalier", "minPrice": 0, "maxPrice": 1, "image": "visserie_pedalier"}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -102,10 +125,13 @@
|
||||
"textColor": "#000000",
|
||||
"items": [
|
||||
{"name": "Béquille", "minPrice": 2, "maxPrice": 8, "image": "bequille"},
|
||||
{"name": "Garde boue", "minPrice": 2, "maxPrice": 8, "image": "gardeboue"},
|
||||
{"name": "Garde boue", "minPrice": 2, "maxPrice": 8, "image": "garde_boue"},
|
||||
{"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"}
|
||||
{"name": "Porte bagage", "minPrice": 2, "maxPrice": 10, "image": "porte_bagage"},
|
||||
{"name": "Porte bidon", "minPrice": 0, "maxPrice": 1, "image": "porte_bidon"},
|
||||
{"name": "Pompe", "minPrice": 0, "maxPrice": 1, "image": "pompe"},
|
||||
{"name": "Panier", "minPrice": 0, "maxPrice": 1, "image": "panier"},
|
||||
{"name": "Rétroviseur", "minPrice": 0, "maxPrice": 1, "image": "retroviseur"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,7 +3,6 @@ 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;
|
||||
@@ -12,7 +11,6 @@ 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;
|
||||
@@ -23,11 +21,8 @@ import java.io.FileOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
/**
|
||||
* Action on shopping cart: validation, clearing, and persistence
|
||||
@@ -36,6 +31,21 @@ public class CartActionHelper {
|
||||
|
||||
private static final String TAG = "CartActionHelper";
|
||||
|
||||
/**
|
||||
* Data structure for JSON and CSV
|
||||
*/
|
||||
private static class StockEntry {
|
||||
String date;
|
||||
String name;
|
||||
int qty;
|
||||
|
||||
StockEntry(String date, String name, int qty) {
|
||||
this.date = date;
|
||||
this.name = name;
|
||||
this.qty = qty;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the cart data and refreshes UI
|
||||
*/
|
||||
@@ -93,6 +103,74 @@ public class CartActionHelper {
|
||||
.show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Merges current cart items with the existing stock file (json and csv) on the SD Card.
|
||||
*/
|
||||
private static void saveCartToExternalFile(List<CartItem> cartItems) {
|
||||
File dir = new File(Environment.getExternalStorageDirectory(), Config.EXTERNAL_DIR_NAME);
|
||||
File stockFile = new File(dir, Config.OUTPUT_JSON_NAME);
|
||||
File csvFile = new File(dir, Config.OUTPUT_CSV_NAME);
|
||||
|
||||
String today = DateHelper.getTodayIso();
|
||||
Gson gson = new Gson();
|
||||
|
||||
// Load previous list
|
||||
List<StockEntry> history = loadHistory(stockFile, gson);
|
||||
|
||||
// Merge current cart items in previous list
|
||||
for (CartItem cartItem : cartItems) {
|
||||
boolean merged = false;
|
||||
for (StockEntry entry : history) {
|
||||
// Same date same name ? Add it
|
||||
if (entry.date.equals(today) && entry.name.equals(cartItem.getName())) {
|
||||
entry.qty += cartItem.getQuantity();
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Not found on the same date ? Create it
|
||||
if (!merged) {
|
||||
history.add(new StockEntry(today, cartItem.getName(), cartItem.getQuantity()));
|
||||
}
|
||||
}
|
||||
// Save to JSON
|
||||
try (FileOutputStream fos = new FileOutputStream(stockFile);
|
||||
OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8")) {
|
||||
gson.newBuilder().setPrettyPrinting().create().toJson(history, writer);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error writing JSON", e);
|
||||
}
|
||||
|
||||
// Save to CSV, french format with ";"
|
||||
try (FileOutputStream fos = new FileOutputStream(csvFile);
|
||||
OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8")) {
|
||||
writer.write('\ufeff');
|
||||
writer.write("Date;Article;Quantité\n");
|
||||
for (StockEntry entry : history) {
|
||||
writer.write(entry.date + ";" + entry.name.replace(";", ",") + ";" + entry.qty + "\n");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error writing CSV", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load JSON history
|
||||
*/
|
||||
private static List<StockEntry> loadHistory(File file, Gson gson) {
|
||||
if (!file.exists()) return new ArrayList<>();
|
||||
try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file), "UTF-8")) {
|
||||
Type type = new TypeToken<List<StockEntry>>(){}.getType();
|
||||
List<StockEntry> result = gson.fromJson(reader, type);
|
||||
return (result != null) ? result : new ArrayList<>();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error reading json history", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a thank you popup and returns to the main menu after 2 seconds.
|
||||
*/
|
||||
@@ -104,78 +182,21 @@ public class CartActionHelper {
|
||||
.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);
|
||||
|
||||
if (activity instanceof MainActivity) {
|
||||
MainActivity main = (MainActivity) activity;
|
||||
// Go to home if not already
|
||||
main.showHome();
|
||||
// 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);
|
||||
GridLayout grid = main.findViewById(R.id.gridPieces);
|
||||
refreshGridQuantities(grid);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges current cart items with the existing stock file on the SD Card.
|
||||
*/
|
||||
private static void saveCartToExternalFile(List<CartItem> cartItems) {
|
||||
File stockFile = new File(Environment.getExternalStorageDirectory(), Config.STOCK_FILE_NAME);
|
||||
Gson gson = new Gson();
|
||||
Map<String, Integer> 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<String, Integer> 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 : <String, Integer>
|
||||
Type type = new TypeToken<Map<String, Integer>>(){}.getType();
|
||||
// Read and fill map
|
||||
Map<String, Integer> 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.
|
||||
*/
|
||||
|
||||
@@ -20,8 +20,10 @@ public class CartItem {
|
||||
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; }
|
||||
|
||||
// Setters
|
||||
public void setQuantity(int quantity) { this.quantity = quantity; }
|
||||
}
|
||||
|
||||
@@ -8,10 +8,14 @@ public class CartManager {
|
||||
// Unified and global list for whole application
|
||||
private static final List<CartItem> items = new ArrayList<>();
|
||||
|
||||
// Private constructor: utility class, it should not be instantiated
|
||||
/**
|
||||
* Private constructor: utility class, it should not be instantiated
|
||||
*/
|
||||
private CartManager() {}
|
||||
|
||||
// Returns the direct reference to the list to save memory on older devices
|
||||
/**
|
||||
* Returns the direct reference to the list to save memory on older devices
|
||||
*/
|
||||
public static List<CartItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
@@ -25,7 +29,9 @@ public class CartManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Logic for adding, updating or removing items based on quantity, prevents duplicate entries
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -40,20 +46,27 @@ public class CartManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Range-based pricing
|
||||
|
||||
/**
|
||||
* Range-based pricing
|
||||
*/
|
||||
public static int getGlobalTotalMin() {
|
||||
int total = 0;
|
||||
for (CartItem item : items) total += item.getTotalMin();
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Range-based pricing
|
||||
*/
|
||||
public static int getGlobalTotalMax() {
|
||||
int total = 0;
|
||||
for (CartItem item : items) total += item.getTotalMax();
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from cart
|
||||
*/
|
||||
public static void clear() {
|
||||
items.clear();
|
||||
}
|
||||
|
||||
@@ -22,17 +22,16 @@ import java.util.List;
|
||||
public class CartViewHelper {
|
||||
|
||||
/**
|
||||
* Refreshes the entire cart side-panel or list
|
||||
* Clears everything and rebuild on current CartManager data
|
||||
* Refreshes cart UI by synchronizing the visual list with the current CartManager state
|
||||
*/
|
||||
public static void updateCartView(LinearLayout cartList, Context context) {
|
||||
if (cartList == null) return;
|
||||
|
||||
// Clear and get current items
|
||||
// Clean item lists
|
||||
cartList.removeAllViews();
|
||||
List<CartItem> cartItems = CartManager.getItems();
|
||||
|
||||
// If empty cart, show it to user
|
||||
// Manage empty cart
|
||||
if (cartItems.isEmpty()) {
|
||||
TextView empty = new TextView(context);
|
||||
empty.setText(context.getString(R.string.cart_empty));
|
||||
@@ -40,35 +39,19 @@ public class CartViewHelper {
|
||||
empty.setGravity(Gravity.CENTER);
|
||||
empty.setPadding(0, 50, 0, 0);
|
||||
cartList.addView(empty);
|
||||
|
||||
updateTotalDisplay(context, 0, 0);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Create list if not empty
|
||||
int totalMin = 0;
|
||||
int totalMax = 0;
|
||||
for (CartItem item : cartItems) {
|
||||
totalMin += item.getTotalMin();
|
||||
totalMax += item.getTotalMax();
|
||||
|
||||
// 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, "€"));
|
||||
// Create a line for each item
|
||||
cartList.addView(createCartItemView(item, cartList, context));
|
||||
}
|
||||
updateTotalDisplay(context, totalMin, totalMax);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +64,6 @@ public class CartViewHelper {
|
||||
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);
|
||||
@@ -90,13 +72,13 @@ public class CartViewHelper {
|
||||
|
||||
// Image
|
||||
ImageView image = new ImageView(context);
|
||||
ImageLoader.loadImage(image, item.getImageFile(),200,200);
|
||||
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]
|
||||
// Sublayout infos
|
||||
LinearLayout infoLayout = new LinearLayout(context);
|
||||
infoLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
infoLayout.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
|
||||
@@ -108,7 +90,7 @@ public class CartViewHelper {
|
||||
nameView.setTypeface(null, Typeface.BOLD);
|
||||
infoLayout.addView(nameView);
|
||||
|
||||
// Quantity and price range
|
||||
// Quantity / price
|
||||
TextView detailsView = new TextView(context);
|
||||
String details = context.getString(R.string.cart_item, item.getQuantity(), item.getTotalMin(), item.getTotalMax());
|
||||
detailsView.setText(details);
|
||||
@@ -124,42 +106,35 @@ public class CartViewHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the delete button
|
||||
*/
|
||||
* Creates the delete button for each item
|
||||
*/
|
||||
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);
|
||||
btn.setLayoutParams(new LinearLayout.LayoutParams(sizePx, sizePx));
|
||||
|
||||
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.setColor(Color.parseColor("#E53935"));
|
||||
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
|
||||
CartManager.addOrUpdateItem(item.getName(), item.getMinPrice(), item.getMaxPrice(), 0, item.getImageFile());
|
||||
updateCartView(cartList, context);
|
||||
});
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage price range for whole cart
|
||||
*/
|
||||
private static void updateTotalDisplay(Context context, int min, int max) {
|
||||
TextView totalView = ((Activity) context).findViewById(R.id.totalView);
|
||||
if (totalView != null) {
|
||||
totalView.setText(context.getString(R.string.price_range, min, max, "€"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,25 @@ public class Category {
|
||||
@SuppressWarnings("unused")
|
||||
private List<Item> items;
|
||||
|
||||
// Empty constructor for GSON
|
||||
public Category() {}
|
||||
|
||||
// Full constructor for online editor
|
||||
public Category(String name, List<Item> items) {
|
||||
this.name = name;
|
||||
this.items = items;
|
||||
// Default colors
|
||||
this.bgColor = "#0049AF";
|
||||
this.textColor = "#FFFFFF";
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getName() { return name; }
|
||||
public String getBgColor() { return bgColor; }
|
||||
public String getTextColor() { return textColor; }
|
||||
public List<Item> getItems() { return items != null ? items : new ArrayList<>(); }
|
||||
|
||||
// Avoid crash if json isn't readable
|
||||
public List<Item> getItems() {
|
||||
return items != null ? items : new ArrayList<>();
|
||||
}
|
||||
// Setters
|
||||
public void setBgColor(String bgColor) { this.bgColor = bgColor; }
|
||||
public void setTextColor(String textColor) { this.textColor = textColor; }
|
||||
}
|
||||
@@ -9,8 +9,11 @@ public class Config {
|
||||
public static final String IMAGES_SUBDIR_NAME = "images";
|
||||
|
||||
// Input json
|
||||
public static final String PIECES_FILE_NAME = "pieces.json";
|
||||
public static final String INPUT_JSON_NAME = "pieces.json";
|
||||
|
||||
// Output json
|
||||
public static final String STOCK_FILE_NAME = "stock.json";
|
||||
public static final String OUTPUT_JSON_NAME = "stock.json";
|
||||
|
||||
// Output json
|
||||
public static final String OUTPUT_CSV_NAME = "stock.csv";
|
||||
}
|
||||
450
app/src/main/java/com/stock/pignon/ControlServer.java
Normal file
@@ -0,0 +1,450 @@
|
||||
// ControlServer.java
|
||||
package com.stock.pignon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Create a web server to remote management of app assets.
|
||||
* Inherit what is needed to build a web server from NanoHTTPD
|
||||
*/
|
||||
public class ControlServer extends NanoHTTPD {
|
||||
|
||||
private final Context context;
|
||||
|
||||
// Assets folders
|
||||
private final File storageRoot = Environment.getExternalStorageDirectory();
|
||||
private final File baseDir = new File(storageRoot, Config.EXTERNAL_DIR_NAME);
|
||||
private final File imagesDir = new File(baseDir, Config.IMAGES_SUBDIR_NAME);
|
||||
|
||||
public ControlServer(Context context, int port) {
|
||||
super(port);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method which manage requests
|
||||
*/
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
// Get requested address
|
||||
String uri = session.getUri();
|
||||
|
||||
if (uri.startsWith("/img_view/")) {
|
||||
return serveImage(uri.replace("/img_view/", ""));
|
||||
}
|
||||
|
||||
if (session.getMethod() == Method.GET) {
|
||||
switch (uri) {
|
||||
// Soon deprecated with online editor
|
||||
case "/download_input":
|
||||
return downloadFile(Config.INPUT_JSON_NAME);
|
||||
// Soon deprecated with online editor
|
||||
case "/output_json":
|
||||
return viewFile(Config.OUTPUT_JSON_NAME);
|
||||
case "/download_output_json":
|
||||
return downloadFile(Config.OUTPUT_JSON_NAME);
|
||||
case "/output_csv":
|
||||
return viewFile(Config.OUTPUT_CSV_NAME);
|
||||
case "/download_output_csv":
|
||||
return downloadFile(Config.OUTPUT_CSV_NAME);
|
||||
case "/edit":
|
||||
return newFixedLengthResponse(fillEditor());
|
||||
case "/":
|
||||
return newFixedLengthResponse(getHtml("index.html"));
|
||||
}
|
||||
}
|
||||
|
||||
if (session.getMethod() == Method.POST) {
|
||||
switch (uri) {
|
||||
// Soon deprecated with online editor
|
||||
case "/upload_json":
|
||||
return handleJsonUpload(session);
|
||||
// Soon deprecated with online editor
|
||||
case "/upload_images":
|
||||
return handleImagesUpload(session);
|
||||
case "/save_editor":
|
||||
return handleSaveEditor(session);
|
||||
}
|
||||
}
|
||||
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Page non trouvée");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from asset and return html file
|
||||
*/
|
||||
private String getHtml(String file) {
|
||||
try {
|
||||
InputStream is = context.getAssets().open(file);
|
||||
// Scanner html file from the beginning with "\\A"
|
||||
Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A");
|
||||
String html = s.hasNext() ? s.next() : "";
|
||||
is.close();
|
||||
return html;
|
||||
} catch (IOException e) {
|
||||
return "<html><body>❌ Erreur de chargement de la page</body></html>";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from asset and return html file
|
||||
*/
|
||||
private String fillEditor() {
|
||||
// Get data from assets
|
||||
DataLoader.loadData();
|
||||
// Create an html string to fill the template
|
||||
StringBuilder html = new StringBuilder();
|
||||
// Save categories order
|
||||
List<String> orderList = new ArrayList<>();
|
||||
|
||||
// Globals
|
||||
String globalId = "global";
|
||||
Category globalCat = new Category("global", DataLoader.getGlobalItems());
|
||||
html.append(renderCategorySection(globalId, globalCat));
|
||||
orderList.add(globalId);
|
||||
|
||||
// Browser categories
|
||||
int index = 0;
|
||||
for (Category cat : DataLoader.getCategories()) {
|
||||
// cat_0, cat_1...
|
||||
String techId = "cat_" + index;
|
||||
|
||||
html.append(renderCategorySection(techId, cat));
|
||||
orderList.add(techId);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
String orderField = "<input type='hidden' name='cat_order_list' value='" +
|
||||
android.text.TextUtils.join(",", orderList) + "'>";
|
||||
|
||||
return getHtml("editor.html").replace("{{GENERATED_CONTENT}}", html + orderField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML section for each category
|
||||
*/
|
||||
private String renderCategorySection(String techId, Category cat) {
|
||||
String displayName = cat.getName();
|
||||
List<Item> items = cat.getItems();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("<div class='card'>");
|
||||
sb.append("<h2>");
|
||||
|
||||
if ("global".equals(displayName)) {
|
||||
sb.append("Articles globaux");
|
||||
sb.append("<input type='hidden' name='cat|global|name' value='global'>");
|
||||
techId = "global";
|
||||
} else {
|
||||
// Nom
|
||||
sb.append("<input type='text' name='cat|").append(techId).append("|name' value=\"").append(displayName).append("\" class='input-h2'>");
|
||||
|
||||
// Couleur de Fond (on utilise cat.getBgColor())
|
||||
sb.append("<div style='display:inline-block; vertical-align:middle; margin-right:15px;'>");
|
||||
sb.append("<div style='font-weight: bold; font-size: 0.7em; padding-bottom: 5px;'>Fond</div>");
|
||||
sb.append("<input type='color' name='cat|").append(techId).append("|bgColor' value='").append(cat.getBgColor()).append("' style='width:30px; height:25px; border:none; cursor:pointer;'>");
|
||||
sb.append("</div>");
|
||||
|
||||
// Couleur de Texte (on utilise cat.getTextColor())
|
||||
sb.append("<div style='display:inline-block; vertical-align:middle; margin-right:15px;'>");
|
||||
sb.append("<div style='font-weight: bold; font-size: 0.7em; padding-bottom: 5px;'>Texte</div>");
|
||||
sb.append("<input type='color' name='cat|").append(techId).append("|textColor' value='").append(cat.getTextColor()).append("' style='width:30px; height:25px; border:none; cursor:pointer;'>");
|
||||
sb.append("</div>");
|
||||
|
||||
// Bouton supprimer
|
||||
sb.append(" <button type='button' class='btn-del' style='vertical-align: middle; margin-left: 10px;' ")
|
||||
.append("onclick=\"if(confirm('Supprimer cette catégorie?')) this.closest('.card').remove()\">×</button>");
|
||||
}
|
||||
sb.append("</h2>");
|
||||
|
||||
// Build table
|
||||
sb.append("<table id='table-").append(techId).append("'>");
|
||||
sb.append("<tr><th>Image</th><th>Nom</th><th>Prix Min</th><th>Prix Max</th></tr>");
|
||||
|
||||
int rowIdx = 0;
|
||||
for (Item item : items) {
|
||||
sb.append(renderItemRow(techId, item, rowIdx++));
|
||||
}
|
||||
|
||||
sb.append("</table>");
|
||||
|
||||
// Button to add item in this category
|
||||
sb.append("<button type='button' class='btn-add' onclick=\"addRow('").append(techId).append("')\">+ Ajouter article</button>");
|
||||
sb.append("</div>");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML row for each item
|
||||
*/
|
||||
private String renderItemRow(String techId, Item item, int idx) {
|
||||
String prefix = "item|" + techId + "|" + idx;
|
||||
return "<tr>" +
|
||||
"<td><img src='/img_view/" + item.getImage() + "' class='img-p' onerror=\"this.src='https://placehold.co/60?text=?'\">" +
|
||||
"<br><input type='file' accept='.jpg,.png' style='font-size:10px; width:70px' onchange='uImg(this)'>" +
|
||||
"<input type='hidden' name='" + prefix + "|img' value='" + item.getImage() + "'></td>" +
|
||||
"<td><input type='text' name='" + prefix + "|name' value=\"" + item.getName() + "\"></td>" +
|
||||
"<td><input type='number' name='" + prefix + "|min' value='" + item.getMinPrice() + "'></td>" +
|
||||
"<td><input type='number' name='" + prefix + "|max' value='" + item.getMaxPrice() + "'></td>" +
|
||||
// Retro JS compatibility
|
||||
"<td><button type='button' class='btn-del' onclick='var r=this.parentNode.parentNode; r.parentNode.removeChild(r);'>×</button></td>" +
|
||||
"</tr>";
|
||||
}
|
||||
|
||||
private Response serveImage(String filename) {
|
||||
File imgJpg = new File(imagesDir, filename + ".jpg");
|
||||
File imgPng = new File(imagesDir, filename + ".png");
|
||||
File file = imgJpg.exists() ? imgJpg : (imgPng.exists() ? imgPng : null);
|
||||
|
||||
if (file == null || !file.exists()) return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "No image");
|
||||
|
||||
try {
|
||||
Response res = newChunkedResponse(Response.Status.OK, "image/jpeg", new FileInputStream(file));
|
||||
// Avoid cache if user change image
|
||||
res.addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.addHeader("Pragma", "no-cache");
|
||||
res.addHeader("Expires", "0");
|
||||
return res;
|
||||
} catch (IOException e) {
|
||||
return newFixedLengthResponse("Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive JSON and update cache and storage catalog
|
||||
*/
|
||||
private Response handleSaveEditor(IHTTPSession session) {
|
||||
try {
|
||||
// NanoHTTPD read request
|
||||
Map<String, String> files = new HashMap<>();
|
||||
session.parseBody(files);
|
||||
|
||||
String jsonStr = files.get("postData");
|
||||
if (jsonStr == null || jsonStr.isEmpty()) {
|
||||
return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT, "Données vides");
|
||||
}
|
||||
|
||||
JSONObject fullJson = new JSONObject(jsonStr);
|
||||
// Get items
|
||||
JSONObject allFields = fullJson.getJSONObject("items");
|
||||
// Get categories order
|
||||
String[] orderedIds = fullJson.getString("cat_order_list").split(",");
|
||||
|
||||
List<Category> finalData = new ArrayList<>();
|
||||
|
||||
// For each category
|
||||
for (String techId : orderedIds) {
|
||||
String catNameKey = "cat|" + techId + "|name";
|
||||
// Does it exist
|
||||
if (!allFields.has(catNameKey)) continue;
|
||||
|
||||
String name = allFields.getString(catNameKey);
|
||||
List<Item> itemsInCategory = new ArrayList<>();
|
||||
|
||||
// Browse and create items
|
||||
int i = 0;
|
||||
while (true) {
|
||||
String itemBase = "item|" + techId + "|" + i + "|";
|
||||
// On vérifie si l'item suivant existe (via son champ name)
|
||||
if (!allFields.has(itemBase + "name")) break;
|
||||
|
||||
// Manage image
|
||||
String imgVal = allFields.optString(itemBase + "img", "");
|
||||
if (imgVal.startsWith("data:image")) {
|
||||
try {
|
||||
String newImgName = "img_" + System.currentTimeMillis() + "_" + i;
|
||||
String base64Data = imgVal.split(",")[1];
|
||||
byte[] decoded = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT);
|
||||
|
||||
File imageFile = new File(imagesDir, newImgName + ".jpg");
|
||||
try (FileOutputStream fos = new FileOutputStream(imageFile)) {
|
||||
fos.write(decoded);
|
||||
}
|
||||
imgVal = newImgName; // Replace base64 with name of created jpg file
|
||||
} catch (Exception e) { Log.e("ControlServer", "Img Error", e); }
|
||||
}
|
||||
|
||||
itemsInCategory.add(new Item(
|
||||
// Mandatory
|
||||
allFields.getString(itemBase + "name"),
|
||||
// Not mandatory
|
||||
imgVal,
|
||||
parseSafely(allFields.optString(itemBase + "min", "0")),
|
||||
parseSafely(allFields.optString(itemBase + "max", "0"))
|
||||
));
|
||||
i++;
|
||||
}
|
||||
|
||||
// Create category with items and colors
|
||||
Category cat = new Category(name, itemsInCategory);
|
||||
cat.setBgColor(allFields.optString("cat|" + techId + "|bgColor", "#0049AF"));
|
||||
cat.setTextColor(allFields.optString("cat|" + techId + "|textColor", "#FFFFFF"));
|
||||
finalData.add(cat);
|
||||
}
|
||||
|
||||
DataLoader.saveData(finalData);
|
||||
return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "OK");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e("ControlServer", "Erreur save", e);
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Erreur");
|
||||
}
|
||||
}
|
||||
|
||||
private int parseSafely(String val) {
|
||||
if (val == null) return 0;
|
||||
try {
|
||||
return Integer.parseInt(val.trim());
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Soon deprecated with online editor
|
||||
/**
|
||||
* Manage JSON sent by remote.
|
||||
*/
|
||||
private Response handleJsonUpload(IHTTPSession session) {
|
||||
Map<String, String> tmpFiles = new HashMap<>();
|
||||
try {
|
||||
// NanoHTTPD stores loaded file in temp file
|
||||
session.parseBody(tmpFiles);
|
||||
// tmpFiles map stores file name and path to tmp folder
|
||||
String tmpPath = tmpFiles.get("json_file");
|
||||
if (tmpPath == null) return newFixedLengthResponse("❌ Aucun fichier reçu.");
|
||||
|
||||
File src = new File(tmpPath);
|
||||
File dest = new File(baseDir, Config.INPUT_JSON_NAME);
|
||||
|
||||
copyFile(src, dest);
|
||||
return newFixedLengthResponse("✅ Catalogue mis à jour ! <a href='/'>Retour</a>");
|
||||
} catch (Exception e) {
|
||||
return newFixedLengthResponse("❌ Erreur JSON : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Soon deprecated with online editor
|
||||
/**
|
||||
* Manage multiple images sent by remote.
|
||||
*/
|
||||
private Response handleImagesUpload(IHTTPSession session) {
|
||||
Map<String, String> tmpFiles = new HashMap<>();
|
||||
try {
|
||||
session.parseBody(tmpFiles);
|
||||
|
||||
if (!imagesDir.exists() && !imagesDir.mkdirs()) {
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT,
|
||||
"❌ Erreur : Impossible de créer le dossier des images sur la tablette.");
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
|
||||
// Browse temp files created by NanoHTTPD
|
||||
for (Map.Entry<String, String> entry : tmpFiles.entrySet()) {
|
||||
// Manage multiple files uppload
|
||||
if (entry.getKey().startsWith("images")) {
|
||||
String tmpPath = entry.getValue();
|
||||
|
||||
// Get file name
|
||||
List<String> params = session.getParameters().get(entry.getKey());
|
||||
|
||||
if (params != null && !params.isEmpty()) {
|
||||
// Clean file name to remove path, only keep image.jpg or image.png
|
||||
String originalName = new File(params.get(0)).getName();
|
||||
|
||||
File src = new File(tmpPath);
|
||||
File dest = new File(imagesDir, originalName);
|
||||
|
||||
copyFile(src, dest);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newFixedLengthResponse("✅ " + count + " images enregistrées ! <a href='/'>Retour</a>");
|
||||
|
||||
} catch (Exception e) {
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT,
|
||||
"❌ Erreur serveur : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Soon deprecated with online editor
|
||||
/**
|
||||
* Utility method to copy file byte per byte.
|
||||
* Not always possible to simply move a file on Android.
|
||||
*/
|
||||
private void copyFile(File src, File dst) throws IOException {
|
||||
try (InputStream in = new FileInputStream(src);
|
||||
OutputStream out = new FileOutputStream(dst)) {
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
while ((len = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare file and offer as download
|
||||
*/
|
||||
private Response downloadFile(String filename) {
|
||||
File file = new File(baseDir, filename);
|
||||
if (!file.exists()) {
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Fichier non trouvé.");
|
||||
}
|
||||
try {
|
||||
InputStream is = new FileInputStream(file);
|
||||
|
||||
// Force download
|
||||
Response res = newChunkedResponse(Response.Status.OK, "application/octet-stream", is);
|
||||
|
||||
res.addHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
|
||||
return res;
|
||||
} catch (IOException e) {
|
||||
return newFixedLengthResponse("❌ Erreur de lecture.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare file and print it in browser
|
||||
*/
|
||||
private Response viewFile(String filename) {
|
||||
File file = new File(baseDir, filename);
|
||||
if (!file.exists()) {
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Fichier non trouvé.");
|
||||
}
|
||||
try {
|
||||
InputStream is = new FileInputStream(file);
|
||||
|
||||
// Force Mime type to print file inside browser
|
||||
String mimeType = "text/plain";
|
||||
|
||||
Response res = newChunkedResponse(Response.Status.OK, mimeType + "; charset=utf-8", is);
|
||||
|
||||
res.addHeader("Content-Disposition", "inline");
|
||||
return res;
|
||||
} catch (IOException e) {
|
||||
return newFixedLengthResponse("❌ Erreur de lecture.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,30 @@ package com.stock.pignon;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
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;
|
||||
private static final String PIECES_FILE = Config.INPUT_JSON_NAME;
|
||||
|
||||
// Raw data
|
||||
private static List<Category> cachedCategories = new ArrayList<>();
|
||||
private static List<Item> cachedGlobals = new ArrayList<>();
|
||||
|
||||
|
||||
/**
|
||||
* Get data from input JSON PIECES_FILE
|
||||
*/
|
||||
public static void loadData() {
|
||||
File dir = new File(Environment.getExternalStorageDirectory(), EXTERNAL_DIR);
|
||||
File jsonFile = new File(dir, PIECES_FILE);
|
||||
@@ -56,7 +64,10 @@ public class DataLoader {
|
||||
}
|
||||
}
|
||||
|
||||
// Compromise between CPU usage and memory usage : keep cache raw data and combinate global and specific items at call
|
||||
/**
|
||||
* Create a merge list with global items and items from a specified category for main activity
|
||||
* Compromise between CPU usage and memory usage : keep cache raw data and combinate global and specific items at call
|
||||
*/
|
||||
public static List<Item> getItemsForCategory(String categoryName) {
|
||||
|
||||
// Add global items
|
||||
@@ -72,13 +83,68 @@ public class DataLoader {
|
||||
return combinedList;
|
||||
}
|
||||
|
||||
/**
|
||||
* To fill online editor, create a map with all items sorted by category
|
||||
*/
|
||||
public static Map<String, List<Item>> getAllSections() {
|
||||
// LinkedHashMap remember item order instead of HashMap
|
||||
Map<String, List<Item>> sections = new LinkedHashMap<>();
|
||||
sections.put("global", cachedGlobals);
|
||||
for (Category cat : cachedCategories) {
|
||||
sections.put(cat.getName(), cat.getItems());
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a web server to remote management of app assets.
|
||||
*/
|
||||
public static List<Category> getCategories() {
|
||||
return cachedCategories;
|
||||
}
|
||||
public static List<Item> getGlobalItems() {
|
||||
return cachedGlobals;
|
||||
}
|
||||
|
||||
// Internal class for Gson
|
||||
/**
|
||||
* Internal class for GSON
|
||||
*/
|
||||
private static class CategoriesWrapper {
|
||||
List<Item> globalItems;
|
||||
List<Category> categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write JSON from online editor data
|
||||
*/
|
||||
public static void saveData(List<Category> categoriesList) throws Exception {
|
||||
CategoriesWrapper wrapper = new CategoriesWrapper();
|
||||
wrapper.categories = new ArrayList<>();
|
||||
wrapper.globalItems = new ArrayList<>();
|
||||
|
||||
// Browse category
|
||||
for (Category cat : categoriesList) {
|
||||
if ("global".equals(cat.getName())) {
|
||||
wrapper.globalItems = cat.getItems();
|
||||
} else {
|
||||
wrapper.categories.add(cat);
|
||||
}
|
||||
}
|
||||
|
||||
// Create JSON
|
||||
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
String jsonString = gson.toJson(wrapper);
|
||||
|
||||
File dir = new File(Environment.getExternalStorageDirectory(), Config.EXTERNAL_DIR_NAME);
|
||||
File jsonFile = new File(dir, Config.INPUT_JSON_NAME);
|
||||
|
||||
// Write JSON
|
||||
try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(jsonFile), "UTF-8")) {
|
||||
writer.write(jsonString);
|
||||
}
|
||||
|
||||
// Update app cache
|
||||
cachedGlobals = wrapper.globalItems;
|
||||
cachedCategories = wrapper.categories;
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/com/stock/pignon/DateHelper.java
Normal file
@@ -0,0 +1,15 @@
|
||||
// DateHelper.java
|
||||
package com.stock.pignon;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class DateHelper {
|
||||
/**
|
||||
* Return ISO format date (AAAA-MM-JJ)
|
||||
*/
|
||||
public static String getTodayIso() {
|
||||
return new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,24 @@ public class Item {
|
||||
// Empty constructor for GSON
|
||||
public Item() {}
|
||||
|
||||
// Full constructor for online editor
|
||||
public Item(String name, String image, int minPrice, int maxPrice) {
|
||||
this.name = name;
|
||||
this.image = image;
|
||||
this.minPrice = minPrice;
|
||||
this.maxPrice = maxPrice;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getName() { return name; }
|
||||
public int getMinPrice() { return minPrice; }
|
||||
public int getMaxPrice() { return maxPrice; }
|
||||
public String getImage() { return image; }
|
||||
|
||||
// Setters
|
||||
public void setName(String name) { this.name = name; }
|
||||
public void setMinPrice(int minPrice) { this.minPrice = minPrice; }
|
||||
public void setMaxPrice(int maxPrice) { this.maxPrice = maxPrice; }
|
||||
public void setImage(String image) { this.image = image; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// MainActivity.java
|
||||
package com.stock.pignon;
|
||||
|
||||
import android.graphics.Color;
|
||||
@@ -11,6 +12,7 @@ import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
import android.os.Environment;
|
||||
import java.io.File;
|
||||
@@ -31,14 +33,17 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
// UI Components
|
||||
// UI components
|
||||
private LinearLayout cartList;
|
||||
private LinearLayout homeLayout;
|
||||
private LinearLayout categoryItemsLayout;
|
||||
private FrameLayout categoryItemsLayout;
|
||||
private LinearLayout categoriesLayout;
|
||||
private GridLayout gridPieces;
|
||||
private ImageView mainImage;
|
||||
|
||||
// Server component
|
||||
private ControlServer server;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -57,12 +62,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
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
|
||||
@@ -74,6 +73,71 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
// Initial cart visual sync
|
||||
CartViewHelper.updateCartView(cartList, this);
|
||||
|
||||
// Action bar
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
getSupportActionBar().setTitle(" 🚲 Atelier du Pignon - Gestion du stock à prix libre");
|
||||
}
|
||||
|
||||
// Get app version
|
||||
String versionName;
|
||||
try {
|
||||
versionName = "App v" + getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
|
||||
} catch (Exception e) {
|
||||
versionName = ""; // Fallback
|
||||
}
|
||||
|
||||
// Launch server
|
||||
server = new ControlServer(this,8080);
|
||||
try {
|
||||
server.start();
|
||||
String url = "http://" + getDeviceIP() + ":8080";
|
||||
|
||||
// Print URL in action bar for user
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setSubtitle("Serveur en ligne : " + url + " - " + versionName);
|
||||
}
|
||||
Log.i(TAG, "Server started on : " + url);
|
||||
} catch (IOException e) {
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setSubtitle("Erreur serveur" + " - " + versionName);
|
||||
}
|
||||
}
|
||||
|
||||
// Back button (blue)
|
||||
Button btnBack = findViewById(R.id.btnBackFromCategory);
|
||||
if (btnBack != null) {
|
||||
btnBack.setOnClickListener(v -> showHome());
|
||||
}
|
||||
|
||||
// Empty button (red)
|
||||
Button btnClear = findViewById(R.id.clearCartBtn);
|
||||
if (btnClear != null) {
|
||||
btnClear.setOnClickListener(v -> CartActionHelper.emptyCart(cartList, this));
|
||||
}
|
||||
|
||||
// Validate button (green)
|
||||
Button btnValidate = findViewById(R.id.validateCartBtn);
|
||||
if (btnValidate != null) {
|
||||
btnValidate.setOnClickListener(v -> CartActionHelper.validateCart(cartList, this));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// Ensure cart is up to date if returning to the activity
|
||||
CartViewHelper.updateCartView(cartList, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (server != null) {
|
||||
server.stop();
|
||||
Log.i(TAG, "Server stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,7 +279,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
CartViewHelper.updateCartView(cartList, this);
|
||||
}
|
||||
|
||||
private void showHome() {
|
||||
public void showHome() {
|
||||
categoryItemsLayout.setVisibility(View.GONE);
|
||||
homeLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
@@ -225,46 +289,70 @@ public class MainActivity extends AppCompatActivity {
|
||||
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 rootDir = new File(Environment.getExternalStorageDirectory(), Config.EXTERNAL_DIR_NAME);
|
||||
|
||||
// Create root folder if not found
|
||||
if (!rootDir.exists()) {
|
||||
if (!rootDir.mkdirs()) {
|
||||
Log.e("MainActivity", "Can't create root dir." + rootDir.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check pieces.json
|
||||
File inputJson = new File(rootDir, Config.INPUT_JSON_NAME);
|
||||
if (!inputJson.exists()) {
|
||||
Log.i("MainActivity", "pieces.json not found, copying it...");
|
||||
copyFileFromAssets(Config.INPUT_JSON_NAME, inputJson);
|
||||
} else {
|
||||
Log.d("MainActivity", "Keep existing pieces.json");
|
||||
}
|
||||
|
||||
// Check stock.json and stock.csv output files to avoid control server error
|
||||
checkOrCreateEmptyFile(new File(rootDir, Config.OUTPUT_JSON_NAME), "[]");
|
||||
checkOrCreateEmptyFile(new File(rootDir, Config.OUTPUT_CSV_NAME), "");
|
||||
|
||||
// Check images folder
|
||||
File imgDir = new File(rootDir, Config.IMAGES_SUBDIR_NAME);
|
||||
if (!imgDir.exists()) {
|
||||
if (!imgDir.mkdirs()) {
|
||||
Log.e("MainActivity", "Can't create images dir.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy images only if not found
|
||||
String[] filesInImgDir = imgDir.list();
|
||||
if (filesInImgDir == null || filesInImgDir.length == 0) {
|
||||
Log.i("MainActivity", "Images folder empty. Installing images...");
|
||||
copyFolderFromAssets(Config.IMAGES_SUBDIR_NAME, imgDir);
|
||||
} else {
|
||||
Log.d("MainActivity", "Keep existing images.");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* Create a file with default content if not found
|
||||
*/
|
||||
private void checkOrCreateEmptyFile(File file, String defaultContent) {
|
||||
if (!file.exists()) {
|
||||
try (FileOutputStream fos = new FileOutputStream(file)) {
|
||||
fos.write(defaultContent.getBytes());
|
||||
Log.d(TAG, "Initialisation de : " + file.getName());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Erreur lors de l'initialisation de " + file.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy folder from assets
|
||||
*/
|
||||
private void copyFolderFromAssets(String assetDirName, File destDir) {
|
||||
try {
|
||||
String[] files = getAssets().list(assetDirName);
|
||||
@@ -280,25 +368,50 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("MainActivity", "Erreur listing assets: " + assetDirName, e);
|
||||
Log.e("MainActivity", "Listing assets error: " + assetDirName, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy file from assets
|
||||
*/
|
||||
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];
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
Log.d("MainActivity", "Succès : " + assetName + " copié.");
|
||||
Log.d("MainActivity", "Success : " + assetName + " copied.");
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e("MainActivity", "Erreur copie asset: " + assetName, e);
|
||||
Log.e("MainActivity", "Can't copy asset: " + assetName, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device IP to print it to users for easy remote control
|
||||
*/
|
||||
private String getDeviceIP() {
|
||||
try {
|
||||
java.util.Enumeration<java.net.NetworkInterface> interfaces = java.net.NetworkInterface.getNetworkInterfaces();
|
||||
while (interfaces.hasMoreElements()) {
|
||||
java.net.NetworkInterface iface = interfaces.nextElement();
|
||||
java.util.Enumeration<java.net.InetAddress> addresses = iface.getInetAddresses();
|
||||
while (addresses.hasMoreElements()) {
|
||||
java.net.InetAddress addr = addresses.nextElement();
|
||||
if (!addr.isLoopbackAddress() && addr instanceof java.net.Inet4Address) {
|
||||
return addr.getHostAddress();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "IP error", e);
|
||||
}
|
||||
return "127.0.0.1";
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- activity_main.xml -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:background="#ffffff">
|
||||
android:background="#ffffff"
|
||||
android:baselineAligned="false"> <FrameLayout
|
||||
android:id="@+id/contentFrame"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/homeLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
tools:ignore="RtlSymmetry"> <LinearLayout
|
||||
android:id="@+id/categoriesLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mainImage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="@string/app_name" /> </LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/contentFrame"
|
||||
android:layout_width="0dp"
|
||||
android:id="@+id/categoryItemsLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/homeLayout"
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="20dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/categoriesLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mainImage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="fitCenter" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/categoryItemsLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
android:padding="10dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/backToHomeBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="← Retour" />
|
||||
|
||||
<ScrollView
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<GridLayout
|
||||
android:id="@+id/gridPieces"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="4" />
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/btnBackFromCategory"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:layout_margin="20dp"
|
||||
android:text="@string/cart_back_btn"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#FFFFFF"
|
||||
app:backgroundTint="#1E88E5"
|
||||
android:elevation="8dp"
|
||||
tools:targetApi="l" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<include layout="@layout/partial_cart" />
|
||||
|
||||
|
||||
@@ -19,13 +19,15 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Item name"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:lines="1"
|
||||
android:ellipsize="end" />
|
||||
android:minLines="2"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemPrice"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- partial_panier.xml -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/cartContainer"
|
||||
android:layout_width="350dp"
|
||||
android:layout_height="match_parent"
|
||||
@@ -56,29 +57,29 @@
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<Button
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/clearCartBtn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="60dp"
|
||||
android:layout_height="70dp"
|
||||
android:text="@string/cart_empty_btn"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#FFFFFF"
|
||||
android:background="#E53935"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:onClick="emptyCart"/>
|
||||
app:backgroundTint="#E53935"
|
||||
android:layout_marginEnd="5dp"/>
|
||||
|
||||
<Button
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/validateCartBtn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="60dp"
|
||||
android:layout_height="70dp"
|
||||
android:text="@string/cart_validate_btn"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#FFFFFF"
|
||||
android:background="#43A047"
|
||||
android:layout_marginStart="5dp"
|
||||
android:onClick="validateCart"/>
|
||||
app:backgroundTint="#43A047"
|
||||
android:layout_marginStart="5dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -2,9 +2,9 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Theme.Pignon" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryDark">@color/purple_700</item>
|
||||
<item name="colorAccent">@color/teal_200</item>
|
||||
<item name="colorPrimary">@color/pignon_blue</item>
|
||||
<item name="colorPrimaryDark">@color/pignon_blue_dark</item>
|
||||
<item name="colorAccent">@color/pignon_blue</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="pignon_blue">#0049AF</color>
|
||||
<color name="pignon_blue_dark">#003580</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
<string name="cart_name">Mes sacoches</string>
|
||||
<string name="cart_validate_btn">Valider</string>
|
||||
<string name="cart_back_btn">Retour</string>
|
||||
<string name="cart_empty_btn">Vider</string>
|
||||
<string name="cart_empty">Sacoches vides</string>
|
||||
<string name="cart_empty">C\'est vide !</string>
|
||||
<string name="cart_item">Quantité : %1$d (%2$d - %3$d €)</string>
|
||||
|
||||
<string name="currency">€</string>
|
||||
@@ -13,6 +14,6 @@
|
||||
<string name="popup_name">Mes sacoches</string>
|
||||
<string name="popup_item"><![CDATA[<b>%1$d x %2$s</b> : %3$d - %4$d €<br>]]></string>
|
||||
<string name="popup_total"><![CDATA[<br><b>Contribution consciente dans la petite boîte : %1$d - %2$d €</b>]]></string>
|
||||
<string name="popup_end">Sacoches sauvegardées, merci !</string>
|
||||
<string name="popup_end">Sacoches sauvegardées, merci ! ❤️</string>
|
||||
|
||||
</resources>
|
||||
@@ -2,11 +2,9 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Theme.Pignon" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryDark">@color/purple_700</item>
|
||||
<item name="colorAccent">@color/teal_200</item>
|
||||
|
||||
<item name="actionBarSize">40dp</item>
|
||||
<item name="colorPrimary">@color/pignon_blue</item>
|
||||
<item name="colorPrimaryDark">@color/pignon_blue_dark</item>
|
||||
<item name="colorAccent">@color/pignon_blue</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||