diff --git a/app/src/main/assets/editor.html b/app/src/main/assets/editor.html new file mode 100644 index 0000000..c2f292d --- /dev/null +++ b/app/src/main/assets/editor.html @@ -0,0 +1,247 @@ + + + + + + + + + +

É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/Category.java b/app/src/main/java/com/stock/pignon/Category.java index 502feeb..7f21ed4 100644 --- a/app/src/main/java/com/stock/pignon/Category.java +++ b/app/src/main/java/com/stock/pignon/Category.java @@ -14,11 +14,22 @@ 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<>(); } diff --git a/app/src/main/java/com/stock/pignon/ControlServer.java b/app/src/main/java/com/stock/pignon/ControlServer.java index 93a497e..61da2c5 100644 --- a/app/src/main/java/com/stock/pignon/ControlServer.java +++ b/app/src/main/java/com/stock/pignon/ControlServer.java @@ -11,9 +11,11 @@ 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; /** @@ -42,10 +44,16 @@ public class ControlServer extends NanoHTTPD { // 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,8 +62,10 @@ 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")); } } @@ -65,6 +75,8 @@ public class ControlServer extends NanoHTTPD { return handleJsonUpload(session); case "/upload_images": return handleImagesUpload(session); + case "/save_editor": + return handleSaveEditor(session); } } @@ -72,20 +84,217 @@ 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 \\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
"); + + // Bouton pour ajouter un article dans CETTE table précise + 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); + + // Si on ne l'a pas déjà ajouté (sécurité) + 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]; + String value = params.get(key).get(0); + + String fullId = catId + "|" + itemId; + if (!itemDataCollector.containsKey(fullId)) { + itemDataCollector.put(fullId, new HashMap()); + } + itemDataCollector.get(fullId).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); + if (categoriesMap.containsKey(catId)) { + categoriesMap.get(catId).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) { + e.printStackTrace(); + return newFixedLengthResponse("❌ Erreur : " + e.getMessage()); + } + } + + // Soon deprecated with online editor /** * Manage JSON sent by remote. */ @@ -107,6 +316,7 @@ public class ControlServer extends NanoHTTPD { } } + // Soon deprecated with online editor /** * Manage multiple images sent by remote. */ @@ -152,6 +362,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 +421,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..0cc20b2 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 @@ -56,6 +61,9 @@ public class DataLoader { } } + /** + * 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) { @@ -72,13 +80,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";