// MainActivity.java package com.stock.pignon; import android.graphics.Color; import android.graphics.drawable.GradientDrawable; import android.os.Bundle; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.GridLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.FrameLayout; import android.widget.TextView; import android.os.Environment; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import androidx.appcompat.app.AppCompatActivity; import java.util.List; /** * Main Activity: Central Dashboard for the application. * Manages category navigation and item selection within a single screen. */ public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; // UI components private LinearLayout cartList; private LinearLayout homeLayout; private 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); setContentView(R.layout.activity_main); // Hide Android bar View decorView = getWindow().getDecorView(); int uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN; decorView.setSystemUiVisibility(uiOptions); // View Binding cartList = findViewById(R.id.cartList); homeLayout = findViewById(R.id.homeLayout); categoryItemsLayout = findViewById(R.id.categoryItemsLayout); categoriesLayout = findViewById(R.id.categoriesLayout); gridPieces = findViewById(R.id.gridPieces); mainImage = findViewById(R.id.mainImage); // Copy assets to sd card if not founded copyAssetsIfEmpty(); // Get data from sd card DataLoader.loadData(); // Initial UI Setup setupCategoriesMenu(); loadMainImage(); // Initial cart visual sync CartViewHelper.updateCartView(cartList, this); // 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."); } } /** * Builds the left sidebar with category buttons using DataLoader's cache. */ private void setupCategoriesMenu() { List categories = DataLoader.getCategories(); if (categories == null || categories.isEmpty()) { Log.w(TAG, "No categories found to display."); return; } categoriesLayout.removeAllViews(); for (Category category : categories) { Button btn = createCategoryButton(category); // On click, we show the items for this specific category btn.setOnClickListener(v -> showCategory(category.getName())); categoriesLayout.addView(btn); } } /** * Styles category buttons based on the new Category getters. */ private Button createCategoryButton(Category category) { Button btn = new Button(this); btn.setText(category.getName()); btn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24); float density = getResources().getDisplayMetrics().density; float radiusPx = 3 * density; // Apply background color with transparency (70% alpha) if (category.getBgColor() != null) { try { int color = Color.parseColor(category.getBgColor()); int alpha = (int) (0.7 * 255); color = (color & 0x00FFFFFF) | (alpha << 24); GradientDrawable shape = new GradientDrawable(); shape.setColor(color); shape.setCornerRadius(radiusPx); btn.setBackground(shape); } catch (Exception e) { Log.e(TAG, "Error parsing bgColor for: " + category.getName()); } } // Apply text color if (category.getTextColor() != null) { try { btn.setTextColor(Color.parseColor(category.getTextColor())); } catch (Exception e) { Log.e(TAG, "Error parsing textColor for: " + category.getName()); } } LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ); params.setMargins(0, 5, 0, 5); btn.setLayoutParams(params); return btn; } /** * Switches center view to display the grid of items. * Uses DataLoader.getItemsForCategory() to get the merged list. */ private void showCategory(String categoryName) { homeLayout.setVisibility(View.GONE); categoryItemsLayout.setVisibility(View.VISIBLE); gridPieces.removeAllViews(); // Calculate item width for a 4-column grid int gridWidth = getResources().getDisplayMetrics().widthPixels - dpToPx(370); int itemWidth = gridWidth / 4; // Fetch the PRE-MERGED list (globals + specifics) from DataLoader List itemsToDisplay = DataLoader.getItemsForCategory(categoryName); for (Item item : itemsToDisplay) { View itemView = LayoutInflater.from(this).inflate(R.layout.item_piece, gridPieces, false); // Apply width to the inflated view GridLayout.LayoutParams params = (GridLayout.LayoutParams) itemView.getLayoutParams(); if (params == null) { params = new GridLayout.LayoutParams(); } params.width = itemWidth; itemView.setLayoutParams(params); bindItemToView(itemView, item); gridPieces.addView(itemView); } } /** * Connects Item data to the inflated layout piece. */ private void bindItemToView(View itemView, Item item) { TextView nom = itemView.findViewById(R.id.itemName); TextView prix = itemView.findViewById(R.id.itemPrice); ImageView img = itemView.findViewById(R.id.itemImage); TextView qtyView = itemView.findViewById(R.id.qtyView); nom.setText(item.getName()); prix.setText(getString(R.string.price_range, item.getMinPrice(), item.getMaxPrice(), "€")); // Use your existing ImageLoader helper ImageLoader.loadImage(img, item.getImage(),200,200); // Sync visual quantity with CartManager CartItem existing = CartManager.getItemByName(item.getName()); int currentQty = (existing != null) ? existing.getQuantity() : 0; qtyView.setText(String.valueOf(currentQty)); // Listeners for + and - buttons itemView.findViewById(R.id.plusBtn).setOnClickListener(v -> updateItemQty(item, qtyView, 1)); itemView.findViewById(R.id.minusBtn).setOnClickListener(v -> updateItemQty(item, qtyView, -1)); } /** * Updates the local counter and the global cart state simultaneously. */ private void updateItemQty(Item item, TextView qtyView, int delta) { int current = Integer.parseInt(qtyView.getText().toString()); int next = Math.max(0, current + delta); qtyView.setText(String.valueOf(next)); // Central data update CartManager.addOrUpdateItem(item.getName(), item.getMinPrice(), item.getMaxPrice(), next, item.getImage()); // Refresh sidebar cart list CartViewHelper.updateCartView(cartList, this); } public void showHome() { categoryItemsLayout.setVisibility(View.GONE); homeLayout.setVisibility(View.VISIBLE); } private void loadMainImage() { // Fallback for home screen image ImageLoader.loadImage(mainImage, "_velo", 800,800); } private int dpToPx(int dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } 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."); } } /** * 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); if (files != null) { // For every image for (String fileName : files) { // Create source path String assetPath = assetDirName + "/" + fileName; // Create dest path File destFile = new File(destDir, fileName); // Copy copyFileFromAssets(assetPath, destFile); } } } catch (IOException e) { Log.e("MainActivity", "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[8192]; int read; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } Log.d("MainActivity", "Success : " + assetName + " copied."); } catch (IOException 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 interfaces = java.net.NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { java.net.NetworkInterface iface = interfaces.nextElement(); java.util.Enumeration 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"; } }