From ebb10ed61b436aeeec23b7cfef2a1612911af827 Mon Sep 17 00:00:00 2001 From: lucasroyerdev Date: Fri, 30 Jan 2026 19:48:30 +0100 Subject: [PATCH] feat: add online editor, work in progress --- app/src/main/assets/editor.html | 244 +++++++++++++++ app/src/main/assets/index.html | 3 + .../main/java/com/stock/pignon/CartItem.java | 4 +- .../java/com/stock/pignon/CartManager.java | 23 +- .../main/java/com/stock/pignon/Category.java | 19 +- .../java/com/stock/pignon/ControlServer.java | 280 ++++++++++++++++-- .../java/com/stock/pignon/DataLoader.java | 75 ++++- app/src/main/java/com/stock/pignon/Item.java | 15 + .../java/com/stock/pignon/MainActivity.java | 2 +- 9 files changed, 621 insertions(+), 44 deletions(-) create mode 100644 app/src/main/assets/editor.html diff --git a/app/src/main/assets/editor.html b/app/src/main/assets/editor.html new file mode 100644 index 0000000..e1965ce --- /dev/null +++ b/app/src/main/assets/editor.html @@ -0,0 +1,244 @@ + + + + + + + + + +

Éditeur de Catalogue

+
+ + {{GENERATED_CONTENT}} + +
+ + + + +
+
+ + + \ No newline at end of file diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html index 71d739b..536e01f 100644 --- a/app/src/main/assets/index.html +++ b/app/src/main/assets/index.html @@ -43,6 +43,9 @@
+
+

Modifier dans le navigateur

+

Editeur en ligne

\ 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 index a799af3..f4cffa7 100644 --- a/app/src/main/java/com/stock/pignon/CartItem.java +++ b/app/src/main/java/com/stock/pignon/CartItem.java @@ -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; } } diff --git a/app/src/main/java/com/stock/pignon/CartManager.java b/app/src/main/java/com/stock/pignon/CartManager.java index c5184db..7969635 100644 --- a/app/src/main/java/com/stock/pignon/CartManager.java +++ b/app/src/main/java/com/stock/pignon/CartManager.java @@ -8,10 +8,14 @@ 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 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 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(); } diff --git a/app/src/main/java/com/stock/pignon/Category.java b/app/src/main/java/com/stock/pignon/Category.java index 502feeb..07f9360 100644 --- a/app/src/main/java/com/stock/pignon/Category.java +++ b/app/src/main/java/com/stock/pignon/Category.java @@ -14,12 +14,21 @@ public class Category { @SuppressWarnings("unused") private List items; + // Empty constructor for GSON + public Category() {} + + // Full constructor for online editor + public Category(String name, List 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; } - - // Avoid crash if json isn't readable - public List getItems() { - return items != null ? items : new ArrayList<>(); - } + 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/ControlServer.java b/app/src/main/java/com/stock/pignon/ControlServer.java index 93a497e..1c46773 100644 --- a/app/src/main/java/com/stock/pignon/ControlServer.java +++ b/app/src/main/java/com/stock/pignon/ControlServer.java @@ -3,6 +3,7 @@ package com.stock.pignon; import android.content.Context; import android.os.Environment; +import android.util.Log; import fi.iki.elonen.NanoHTTPD; import java.io.File; @@ -11,15 +12,17 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.LinkedHashMap; 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 */ -// Inherit what is needed to build a web server public class ControlServer extends NanoHTTPD { private final Context context; @@ -35,17 +38,23 @@ public class ControlServer extends NanoHTTPD { } /** - * Main method which receive remote request + * 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": @@ -54,17 +63,23 @@ public class ControlServer extends NanoHTTPD { return viewFile(Config.OUTPUT_CSV_NAME); case "/download_output_csv": return downloadFile(Config.OUTPUT_CSV_NAME); + case "/edit": + return newFixedLengthResponse(fillEditor()); case "/": - return newFixedLengthResponse(getHome()); + 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); } } @@ -72,28 +87,233 @@ public class ControlServer extends NanoHTTPD { } /** - * HTML UI for users + * Read from asset and return html file */ - private String getHome() { + private String getHtml(String file) { try { - InputStream is = context.getAssets().open("index.html"); + 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 "❌ Erreur de chargement du template"; + return "❌ Erreur de chargement de la page"; } } + /** + * Read from asset and return html file + */ + private String fillEditor() { + // Get data for assets + DataLoader.loadData(); + // Create an html string to fill the template + StringBuilder html = new StringBuilder(); + + // Browse map : 'id' for categories name, 'items' for items list + for (Map.Entry> section : DataLoader.getAllSections().entrySet()) { + String id = section.getKey(); + List items = section.getValue(); + + // For each category, render a HTML card + html.append(renderCategorySection(id, items)); + } + + return getHtml("editor.html").replace("{{GENERATED_CONTENT}}", html.toString()); + } + + /** + * Generate HTML section for each category + */ + private String renderCategorySection(String id, List items) { + StringBuilder sb = new StringBuilder(); + + sb.append("
"); + sb.append("

"); + + if ("global".equals(id)) { + sb.append("Articles globaux"); + // Hidden input to mark cat for server + sb.append(""); + } else { + // Copy H2 theme + sb.append(""); + + // Delete category button + sb.append(" "); + } + + sb.append("

"); + + // Build table + sb.append(""); + sb.append(""); + + for (Item item : items) { + sb.append(renderItemRow(id, item)); + } + + sb.append("
ImageNomPrix MinPrix Max
"); + + // Button to add item in this category + sb.append(""); + sb.append("
"); + + return sb.toString(); + } + + /** + * Generate HTML row for each item + */ + private String renderItemRow(String catName, Item item) { + String prefix = "item|" + catName + "|" + item.getName(); + return "" + + "" + + "
" + + "" + + "" + + "" + + "" + + // Retro JS compatibility + "" + + ""; + } + + 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"); + } + } + + private Response handleSaveEditor(IHTTPSession session) { + try { + // NanoHTTPD reads post request : copy all data then sort parameters + Map files = new HashMap<>(); + session.parseBody(files); + Map> params = session.getParameters(); + + // Temp struct for modified categories + // Key is category is, value is item list + Map> categoriesMap = new LinkedHashMap<>(); + // Map id and categories names + Map categoryNames = new HashMap<>(); + + // Identify categories with "cat|id123|name" + for (Map.Entry> paramEntry : params.entrySet()) { + String key = paramEntry.getKey(); + if (key.startsWith("cat|") && key.endsWith("|name")) { + String[] parts = key.split("\\|"); + String catId = parts[1]; + String catName = paramEntry.getValue().get(0); + + // Create category only if it doesn't exist already + if (!categoriesMap.containsKey(catId)) { + categoriesMap.put(catId, new ArrayList<>()); + categoryNames.put(catId, catName); + } + } + } + + // Group field for each items + // Key = "catId|itemId", Value = "name, min, max, img"" + Map> itemDataCollector = new LinkedHashMap<>(); + + for (String key : params.keySet()) { + if (key.startsWith("item|") || key.startsWith("new|")) { + String[] parts = key.split("\\|"); // [type, catId, itemId, field] + String catId = parts[1]; + String itemId = parts[2]; + String field = parts[3]; + List values = params.get(key); + String value = (values != null && !values.isEmpty()) ? values.get(0) : ""; + + String fullId = catId + "|" + itemId; + Map itemFields = itemDataCollector.get(fullId); + + if (itemFields == null) { + itemFields = new HashMap<>(); + itemDataCollector.put(fullId, itemFields); + } + + itemFields.put(field, value); + } + } + + for (Map.Entry> entry : itemDataCollector.entrySet()) { + String catId = entry.getKey().split("\\|")[0]; + Map fields = entry.getValue(); + + String name = fields.get("name"); + if (name == null) name = "Sans nom"; + + String img = fields.get("img"); + if (img == null) img = ""; + + int min = 0; + int max = 0; + try { + String minStr = fields.get("min"); + if (minStr != null) min = Integer.parseInt(minStr); + + String maxStr = fields.get("max"); + if (maxStr != null) max = Integer.parseInt(maxStr); + } catch (Exception e) { + // If error, keep 0 + } + + Item item = new Item(name, img, min, max); + List categoryItems = categoriesMap.get(catId); + + if (categoryItems != null) { + categoryItems.add(item); + } + } + + // Stop to use cat ID and replace with cat name + Map> finalData = new LinkedHashMap<>(); + for (String catId : categoriesMap.keySet()) { + String realName = categoryNames.get(catId); + finalData.put(realName, categoriesMap.get(catId)); + } + + // Save data to disk in JSON format + DataLoader.saveData(finalData); + + return newFixedLengthResponse("✅ Catalogue enregistré ! Retour"); + + } catch (Exception e) { + Log.e("ControlServer", "Erreur lors de la sauvegarde", e); + return newFixedLengthResponse("❌ Erreur : " + e.getMessage()); + } + } + + // Soon deprecated with online editor /** * Manage JSON sent by remote. */ private Response handleJsonUpload(IHTTPSession session) { Map tmpFiles = new HashMap<>(); try { + // NanoHTTPD stores loaded file in temp file session.parseBody(tmpFiles); - // NanoHTTPD stocke le fichier avec le nom du champ HTML (json_file) + // 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."); @@ -107,6 +327,7 @@ public class ControlServer extends NanoHTTPD { } } + // Soon deprecated with online editor /** * Manage multiple images sent by remote. */ @@ -122,17 +343,17 @@ public class ControlServer extends NanoHTTPD { int count = 0; - // On parcourt les fichiers temporaires créés par NanoHTTPD + // Browse temp files created by NanoHTTPD for (Map.Entry entry : tmpFiles.entrySet()) { - // NanoHTTPD indexe les envois multiples (images, images1, images2...) + // Manage multiple files uppload if (entry.getKey().startsWith("images")) { String tmpPath = entry.getValue(); - // Récupération sécurisée des paramètres pour obtenir le nom original + // Get file name List params = session.getParameters().get(entry.getKey()); if (params != null && !params.isEmpty()) { - // Nettoyage du nom de fichier (pour ne garder que "image.jpg" sans le chemin PC) + // 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); @@ -152,6 +373,22 @@ public class ControlServer extends NanoHTTPD { } } + // 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 */ @@ -195,19 +432,4 @@ public class ControlServer extends NanoHTTPD { return newFixedLengthResponse("❌ Erreur de lecture."); } } - - /** - * 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); - } - } - } } \ 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 index 4206841..717f796 100644 --- a/app/src/main/java/com/stock/pignon/DataLoader.java +++ b/app/src/main/java/com/stock/pignon/DataLoader.java @@ -4,11 +4,16 @@ 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 @@ -20,6 +25,9 @@ public class DataLoader { private static List 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 getItemsForCategory(String categoryName) { // Add global items @@ -72,13 +83,71 @@ public class DataLoader { return combinedList; } + /** + * To fill online editor, create a map with all items sorted by category + */ + public static Map> getAllSections() { + // LinkedHashMap remember item order instead of HashMap + Map> 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 getCategories() { return cachedCategories; } - // Internal class for Gson + /** + * Internal class for GSON + */ private static class CategoriesWrapper { List globalItems; List categories; } + + /** + * Write JSON from online editor data + */ + public static void saveData(Map> sections) throws Exception { + File dir = new File(Environment.getExternalStorageDirectory(), EXTERNAL_DIR); + File jsonFile = new File(dir, PIECES_FILE); + + // To respect original format, we use the same format as GSON + List globalList = sections.get("global"); + if (globalList == null) { + globalList = new ArrayList<>(); + } + + CategoriesWrapper wrapper = new CategoriesWrapper(); + wrapper.globalItems = globalList; + wrapper.categories = new ArrayList<>(); + + // Fill each category + for (Map.Entry> entry : sections.entrySet()) { + if (!"global".equals(entry.getKey())) { + wrapper.categories.add(new Category(entry.getKey(), entry.getValue())); + } + } + + // Convert to pretty JSON, human readable + Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + String jsonString = gson.toJson(wrapper); + + // Write to disk + try (FileOutputStream fos = new FileOutputStream(jsonFile); + OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8")) { + writer.write(jsonString); + writer.flush(); + } + + // Update app cache + cachedGlobals = wrapper.globalItems; + cachedCategories = wrapper.categories; + } } \ No newline at end of file diff --git a/app/src/main/java/com/stock/pignon/Item.java b/app/src/main/java/com/stock/pignon/Item.java index 1023d07..c39d4c4 100644 --- a/app/src/main/java/com/stock/pignon/Item.java +++ b/app/src/main/java/com/stock/pignon/Item.java @@ -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; } } diff --git a/app/src/main/java/com/stock/pignon/MainActivity.java b/app/src/main/java/com/stock/pignon/MainActivity.java index ca2f6b9..9e31cc3 100644 --- a/app/src/main/java/com/stock/pignon/MainActivity.java +++ b/app/src/main/java/com/stock/pignon/MainActivity.java @@ -89,7 +89,7 @@ public class MainActivity extends AppCompatActivity { } // Launch server - server = new ControlServer(8080); + server = new ControlServer(this,8080); try { server.start(); String url = "http://" + getDeviceIP() + ":8080";