diff --git a/generate-viewer/README.md b/generate-viewer/README.md new file mode 100644 index 0000000..71d4184 --- /dev/null +++ b/generate-viewer/README.md @@ -0,0 +1,88 @@ +# DJI Returns Image Viewer + +A self-contained local image reference tool for CMS Distribution's DJI returns inspection workflow. Built for warehouse staff to browse categorised damage photos without needing an internet connection or a web server. + +## What it does + +Opens as a single HTML file in any browser. Staff can browse 300+ reference photos organised by damage type, search by filename or category, and open images in a full-screen lightbox. Adding new photos is a one-step process. + +## Files + +| File | Purpose | +|---|---| +| `DJI_Returns_Image_Viewer.html` | The viewer — open this in a browser | +| `viewer_template.html` | HTML/CSS/JS template used by the generator | +| `generate_viewer.py` | Scans the Images folder and rebuilds the viewer | +| `Refresh Image Viewer.bat` | Double-click shortcut to run the generator on Windows | +| `Images/` | All reference photos, organised into subfolders by category | + +## Adding new images + +1. Drop the new images into the relevant subfolder under `Images/` +2. Double-click **Refresh Image Viewer.bat** +3. Open `DJI_Returns_Image_Viewer.html` — done + +New subfolders are picked up automatically. If you add a folder that isn't in the category config, it will appear in the sidebar under the **Reference** section using the folder name as the label. No code changes required for basic additions. + +## Customising category labels and sections + +Open `generate_viewer.py` and edit the `CATEGORY_CONFIG` dictionary near the top of the file. Each entry looks like this: + +```python +'Folder Name': { + 'label': 'Display name shown in the sidebar', + 'section': 'accept', # accept | reject | borderline | reference + 'dot': '#2acc94', # colour of the dot indicator (hex) + 'desc': 'Short description shown in the category bar', +}, +``` + +The key (left side) must exactly match the folder path relative to `Images/`, using forward slashes for subfolders: + +```python +'Excessive Crash Damage': {'label': 'Excessive Crash Damage', 'section': 'reject', ...}, +'Retail Images/Action': {'label': 'Action Cameras', 'section': 'reference', ...}, +``` + +After editing, run the generator to rebuild the viewer. + +## Changing the design + +All HTML, CSS, and JavaScript lives in `viewer_template.html`. Edit that file, then run the generator to apply changes to the output. Do not edit `DJI_Returns_Image_Viewer.html` directly — it is overwritten every time the generator runs. + +## Requirements + +- Python 3 (any recent version) +- No third-party libraries — uses only the standard library +- Any modern browser (Chrome, Edge, Firefox) to view the HTML + +Python must be on the system PATH for the `.bat` file to work. If it isn't, run the script directly from a terminal: + +```bash +python generate_viewer.py +``` + +## Sidebar sections + +Images are grouped into four sections in the sidebar, controlled by the `section` field in the category config: + +| Section | Used for | +|---|---| +| `accept` | Damage that is acceptable — product can proceed | +| `reject` | Automatic reject criteria | +| `borderline` | Edge cases requiring judgement | +| `reference` | Retail product shots and other reference material | + +## Viewer features + +- **Search** — filters by filename or category name +- **Grid sizes** — three density options (large / medium / small) +- **Lightbox** — click any image to open full screen; navigate with arrow keys or Prev/Next buttons; close with Escape +- **Category bar** — shows the active category name, description, and image count +- **No server required** — runs entirely from the local filesystem + +## Notes + +- The viewer uses `object-fit: contain` on all thumbnails to prevent portrait-orientation photos from being cropped +- Scrollbars use `overflow-y: scroll` (always visible) rather than `auto` to prevent layout jump when switching categories +- Images are not embedded in the HTML — they are referenced by relative path, so the `Images/` folder must stay in the same location as the HTML file diff --git a/generate-viewer/Refresh Image Viewer.bat b/generate-viewer/Refresh Image Viewer.bat new file mode 100644 index 0000000..6e7ef1b --- /dev/null +++ b/generate-viewer/Refresh Image Viewer.bat @@ -0,0 +1,3 @@ +@echo off +python "%~dp0generate_viewer.py" +pause diff --git a/generate-viewer/generate_viewer.py b/generate-viewer/generate_viewer.py new file mode 100644 index 0000000..c9866b4 --- /dev/null +++ b/generate-viewer/generate_viewer.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +DJI Returns Image Viewer -- Generator +Run this whenever you add new images to the Images/ folder. +New folders are auto-detected. Edit CATEGORY_CONFIG to customise labels. +""" +import os, json, sys + +BASE = os.path.dirname(os.path.abspath(__file__)) +IMAGES_DIR = os.path.join(BASE, 'Images') +OUTPUT_HTML = os.path.join(BASE, 'DJI_Returns_Image_Viewer.html') +TEMPLATE_FILE = os.path.join(BASE, 'viewer_template.html') + +CATEGORY_CONFIG = { + 'Acceptable Damage': {'label': 'Acceptable Damage', 'section': 'accept', 'dot': '#2acc94', 'desc': 'Minor wear -- acceptable for resale'}, + 'Acceptable Damage/RC': {'label': 'Acceptable -- RC Controllers','section': 'accept', 'dot': '#50f7bd', 'desc': 'Controller scuffs and minor marks'}, + 'Excessive Crash Damage': {'label': 'Excessive Crash Damage', 'section': 'reject', 'dot': '#ff4d4d', 'desc': 'Automatic reject'}, + 'Excessive Physical Damage': {'label': 'Excessive Physical Damage', 'section': 'reject', 'dot': '#ff6622', 'desc': 'Structural damage'}, + 'Fire': {'label': 'Fire / Burn Damage', 'section': 'reject', 'dot': '#ff7700', 'desc': 'Fire or thermal damage'}, + 'Mini 3-4Pro Cracked Gimbals': {'label': 'Cracked Gimbals', 'section': 'reject', 'dot': '#ff6b35', 'desc': 'Mini 3 / 4 Pro gimbal damage'}, + 'P3 Gimbal Damage': {'label': 'P3 Gimbal Damage', 'section': 'reject', 'dot': '#ff6b35', 'desc': 'Phantom 3 gimbal'}, + 'Subtle But Rejectable': {'label': 'Subtle But Rejectable', 'section': 'reject', 'dot': '#ffaa00', 'desc': 'Non-obvious -- still a reject'}, + 'Tampering': {'label': 'Tampering / Modified', 'section': 'reject', 'dot': '#cc44ff', 'desc': 'Signs of tampering'}, + 'Very Worn': {'label': 'Very Worn', 'section': 'borderline', 'dot': '#ffcc00', 'desc': 'Heavy use -- borderline case'}, + 'Water Damage': {'label': 'Water Damage', 'section': 'reject', 'dot': '#3eafff', 'desc': 'Water ingress -- automatic reject'}, + 'Strange': {'label': 'Strange / Unusual', 'section': 'reject', 'dot': '#9966cc', 'desc': 'Unusual damage patterns'}, + 'Strange/mb_removed': {'label': 'Mainboard Removed', 'section': 'reject', 'dot': '#cc66aa', 'desc': 'Components stripped'}, + 'Mavic Mini-4k': {'label': 'Mavic Mini 4K Ref', 'section': 'reference', 'dot': '#4488cc', 'desc': 'Reference images'}, + 'Retail Images': {'label': 'All Retail Images', 'section': 'reference', 'dot': '#239aee', 'desc': 'Product retail shots'}, + 'Retail Images/Action': {'label': 'Action Cameras', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Action 4 / 5 / 6'}, + 'Retail Images/Avata': {'label': 'Avata', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Avata 2 range'}, + 'Retail Images/Flip': {'label': 'Flip', 'section': 'reference', 'dot': '#2acc94', 'desc': 'DJI Flip range'}, + 'Retail Images/Lito': {'label': 'Lito 1 / X1', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Entry-level 249g drones'}, + 'Retail Images/Mini': {'label': 'Mini Series', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Mini 2 / 3 / 4 Pro / 5 Pro'}, + 'Retail Images/Neo': {'label': 'Neo', 'section': 'reference', 'dot': '#2acc94', 'desc': 'DJI Neo / Neo 2'}, + 'Retail Images/OM': {'label': 'Osmo Mobile', 'section': 'reference', 'dot': '#2acc94', 'desc': 'OM7 / OM7P / OM8 / OM8P'}, + 'Retail Images/Osmo 360': {'label': 'Osmo 360', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Osmo 360 camera'}, + 'Retail Images/Osmo Nano': {'label': 'Osmo Nano', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Osmo Nano camera'}, +} + +SECTION_ORDER = ['accept','reject','borderline','reference'] +IMAGE_EXTS = {'.jpg','.jpeg','.png','.webp','.gif','.bmp'} + +def scan(): + results = [] + if not os.path.isdir(IMAGES_DIR): + print(f'ERROR: Images folder not found at {IMAGES_DIR}'); sys.exit(1) + for dirpath, dirnames, filenames in os.walk(IMAGES_DIR): + dirnames.sort() + for fname in sorted(filenames): + if os.path.splitext(fname)[1].lower() in IMAGE_EXTS: + rel = os.path.relpath(os.path.join(dirpath, fname), BASE).replace('\\\\', '/') + cat_key = os.path.relpath(dirpath, IMAGES_DIR).replace('\\\\', '/') + if cat_key == '.': cat_key = '' + results.append({'path': rel, 'cat': cat_key}) + return results + +def build_cat_meta(images): + seen = {} + for img in images: + k = img['cat'] + if k not in seen: + cfg = CATEGORY_CONFIG.get(k) + if cfg: + seen[k] = dict(cfg, key=k, count=0) + else: + label = os.path.basename(k) if k else 'Uncategorised' + seen[k] = {'key':k,'label':label,'section':'reference','dot':'#888','desc':'Auto-detected folder','count':0} + seen[k]['count'] += 1 + def sort_key(c): + try: si = SECTION_ORDER.index(c['section']) + except ValueError: si = 99 + return (si, c['label']) + return sorted(seen.values(), key=sort_key) + +def main(): + print('Scanning images...') + images = scan() + print(f' Found {len(images)} images') + cats = build_cat_meta(images) + print(f' Found {len(cats)} categories') + for c in cats: + print(f' [{c["section"]:10}] {c["key"]} ({c["count"]} images)') + with open(TEMPLATE_FILE, 'r', encoding='utf-8') as f: + template = f.read() + html = template.replace('__IMAGE_DATA__', json.dumps(images, indent=2)) + html = html.replace('__CAT_DATA__', json.dumps(cats, indent=2)) + with open(OUTPUT_HTML, 'w', encoding='utf-8') as f: + f.write(html) + print(f'\nDone! Saved: {OUTPUT_HTML}') + print(f'Total: {len(images)} images across {len(cats)} categories') + +if __name__ == '__main__': + main() diff --git a/generate-viewer/viewer_template.html b/generate-viewer/viewer_template.html new file mode 100644 index 0000000..d3ea0d2 --- /dev/null +++ b/generate-viewer/viewer_template.html @@ -0,0 +1,387 @@ + + +
+ + +