check point
This commit is contained in:
parent
d1673c3eef
commit
9de3be6847
3
.github/workflows/odin-ci.yml
vendored
3
.github/workflows/odin-ci.yml
vendored
@ -34,7 +34,8 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
CLAY_DIR="$(pwd)/vendor/clay/bindings/odin/clay-odin"
|
CLAY_DIR="$(pwd)/vendor/clay/bindings/odin/clay-odin"
|
||||||
odin test tests -collection:clay="$CLAY_DIR"
|
BUILD_DIR="$(pwd)/build"
|
||||||
|
odin test tests -collection:clay="$CLAY_DIR" -extra-linker-flags:"-L$BUILD_DIR -losdialog"
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
run: ./scripts/package.sh
|
run: ./scripts/package.sh
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +1,6 @@
|
|||||||
[submodule "odin/vendor/clay"]
|
[submodule "odin/vendor/clay"]
|
||||||
path = odin/vendor/clay
|
path = odin/vendor/clay
|
||||||
url = https://github.com/nicbarker/clay.git
|
url = https://github.com/nicbarker/clay.git
|
||||||
|
[submodule "odin/vendor/osdialog"]
|
||||||
|
path = odin/vendor/osdialog
|
||||||
|
url = https://github.com/AndrewBelt/osdialog.git
|
||||||
|
|||||||
@ -3,9 +3,15 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
CLAY_DIR="$SCRIPT_DIR/vendor/clay/bindings/odin/clay-odin"
|
CLAY_DIR="$SCRIPT_DIR/vendor/clay/bindings/odin/clay-odin"
|
||||||
|
BUILD_DIR="$SCRIPT_DIR/build"
|
||||||
|
|
||||||
|
# Build C dependencies
|
||||||
|
"$SCRIPT_DIR/build_osdialog.sh"
|
||||||
|
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
|
|
||||||
odin build src/app \
|
odin build src/app \
|
||||||
-out:bin/comic_odin \
|
-out:bin/comic_odin \
|
||||||
-debug \
|
-debug \
|
||||||
-collection:clay="$CLAY_DIR"
|
-collection:clay="$CLAY_DIR" \
|
||||||
|
-extra-linker-flags:"-L$BUILD_DIR -losdialog"
|
||||||
|
|||||||
BIN
odin/build/libosdialog.a
Normal file
BIN
odin/build/libosdialog.a
Normal file
Binary file not shown.
35
odin/build_osdialog.sh
Executable file
35
odin/build_osdialog.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build osdialog static library for Linux (zenity backend)
|
||||||
|
set -e
|
||||||
|
|
||||||
|
OSDIALOG_DIR="$(dirname "$0")/vendor/osdialog"
|
||||||
|
OUTPUT_DIR="$(dirname "$0")/build"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
LIB_OUT="$OUTPUT_DIR/libosdialog.a"
|
||||||
|
|
||||||
|
# Only rebuild if sources changed
|
||||||
|
SOURCES=("$OSDIALOG_DIR/osdialog.c" "$OSDIALOG_DIR/osdialog_zenity.c")
|
||||||
|
NEED_REBUILD=false
|
||||||
|
for src in "${SOURCES[@]}"; do
|
||||||
|
if [ "$src" -nt "$LIB_OUT" ] 2>/dev/null; then
|
||||||
|
NEED_REBUILD=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$NEED_REBUILD" = false ] && [ -f "$LIB_OUT" ]; then
|
||||||
|
echo "osdialog already built. Skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building osdialog (zenity backend)..."
|
||||||
|
for src in "${SOURCES[@]}"; do
|
||||||
|
obj="${OUTPUT_DIR}/$(basename "${src%.c}.o")"
|
||||||
|
echo " CC $src"
|
||||||
|
gcc -c -O2 -std=c99 -I"$OSDIALOG_DIR" "$src" -o "$obj"
|
||||||
|
done
|
||||||
|
|
||||||
|
ar rcs "$LIB_OUT" "$OUTPUT_DIR"/osdialog.o "$OUTPUT_DIR"/osdialog_zenity.o
|
||||||
|
echo " AR $LIB_OUT"
|
||||||
|
echo "osdialog build complete."
|
||||||
1
odin/buildandrun.md
Normal file
1
odin/buildandrun.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
comic/odin ui ? ✗ ./build.sh && ./bin/comic_odin gui
|
||||||
@ -9,515 +9,31 @@
|
|||||||
"last_modified_iso": ""
|
"last_modified_iso": ""
|
||||||
},
|
},
|
||||||
"user_mode": 0,
|
"user_mode": 0,
|
||||||
"story_idea": "2 cars racing in down town newyork",
|
"story_idea": "two balls roli",
|
||||||
"story_genre": "action",
|
"story_genre": "action",
|
||||||
"target_audience": "general",
|
"target_audience": "general",
|
||||||
"art_style": "manga",
|
"art_style": "manga",
|
||||||
"script": {
|
"script": {
|
||||||
"title": "Midnight Rush",
|
"title": "",
|
||||||
"synopsis": "Generated comic synopsis",
|
"synopsis": "",
|
||||||
"characters": [
|
"characters": [
|
||||||
|
|
||||||
],
|
],
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
|
||||||
"page_number": 1,
|
|
||||||
"layout_type": 0,
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_001",
|
|
||||||
"panel_number": 1,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "Wide shot of New York City skyline at night, neon lights reflecting on wet streets. Two cars, a red Ferrari and a black Lamborghini, are side by side at a traffic light on a multi-lane avenue.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_002",
|
|
||||||
"panel_number": 2,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "Close-up on the Ferrari driver, a young man in a leather jacket with a determined expression. His hand grips the gear shift.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "This is it.",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_003",
|
|
||||||
"panel_number": 3,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "Close-up on the Lamborghini driver, a woman with sunglasses and a smirk. She revs the engine.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "Ready to lose, pretty boy?",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_004",
|
|
||||||
"panel_number": 4,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "Traffic light turns green. Both cars accelerate, tires screeching, smoke billowing. Speed lines emphasize motion.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_005",
|
|
||||||
"panel_number": 5,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "Medium shot of the cars weaving through traffic. The Ferrari barely misses a taxi, sparks flying from the curb.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "Whoa!",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_006",
|
|
||||||
"panel_number": 6,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "The Lamborghini cuts in front of a bus, horn blaring. The driver laughs.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "Ha! Too slow!",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"page_number": 2,
|
|
||||||
"layout_type": 0,
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_001",
|
|
||||||
"panel_number": 1,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "Both cars enter a sharp curve. The Ferrari drifts close to a parked car, side mirror clipping it off. Glass shatters.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "Sorry!",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_002",
|
|
||||||
"panel_number": 2,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "The Lamborghini takes the inside line, gaining a car length. The Ferrari driver grits his teeth.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "See ya!",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_003",
|
|
||||||
"panel_number": 3,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "Straightaway ahead. The Ferrari spots a shortcut through an alley. He swerves into it.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "Not yet!",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_004",
|
|
||||||
"panel_number": 4,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "The alley is narrow, trash cans flying as the Ferrari speeds through. A cat jumps out of the way.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_005",
|
|
||||||
"panel_number": 5,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "The Ferrari exits the alley just ahead of the Lamborghini. Both cars race toward the finish line (a bridge entrance).",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "What?!",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_006",
|
|
||||||
"panel_number": 6,
|
|
||||||
"shot_type": 2,
|
|
||||||
"description": "The Ferrari crosses the bridge first, winning. The drivers slow down, windows rolled down. The woman nods in respect.",
|
|
||||||
"characters_present": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"dialogue": [
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "Nice move.",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker_id": "",
|
|
||||||
"text": "You're not bad yourself.",
|
|
||||||
"bubble_type": 0,
|
|
||||||
"emotion": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"caption": "",
|
|
||||||
"sound_effects": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"transition_from_previous": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"characters": [
|
"characters": [
|
||||||
|
|
||||||
],
|
],
|
||||||
"panel_images": {
|
"panel_images": {
|
||||||
"panel_001_003": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_003_panel_001_003.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 3,
|
|
||||||
"prompt": "local panel 3"
|
|
||||||
},
|
|
||||||
"panel_002_001": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_007_panel_002_001.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 7,
|
|
||||||
"prompt": "local panel 7"
|
|
||||||
},
|
|
||||||
"panel_002_006": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_012_panel_002_006.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 12,
|
|
||||||
"prompt": "local panel 12"
|
|
||||||
},
|
|
||||||
"panel_001_004": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_004_panel_001_004.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 4,
|
|
||||||
"prompt": "local panel 4"
|
|
||||||
},
|
|
||||||
"panel_002_005": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_011_panel_002_005.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 11,
|
|
||||||
"prompt": "local panel 11"
|
|
||||||
},
|
|
||||||
"panel_001_005": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_005_panel_001_005.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 5,
|
|
||||||
"prompt": "local panel 5"
|
|
||||||
},
|
|
||||||
"panel_002_004": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_010_panel_002_004.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 10,
|
|
||||||
"prompt": "local panel 10"
|
|
||||||
},
|
|
||||||
"panel_001_006": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_006_panel_001_006.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 6,
|
|
||||||
"prompt": "local panel 6"
|
|
||||||
},
|
|
||||||
"panel_001_001": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_001_panel_001_001.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 1,
|
|
||||||
"prompt": "local panel 1"
|
|
||||||
},
|
|
||||||
"panel_002_003": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_009_panel_002_003.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 9,
|
|
||||||
"prompt": "local panel 9"
|
|
||||||
},
|
|
||||||
"panel_001_002": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_002_panel_001_002.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 2,
|
|
||||||
"prompt": "local panel 2"
|
|
||||||
},
|
|
||||||
"panel_002_002": {
|
|
||||||
"url": "file:///tmp/comic-gui-local-panels-0387429411/panel_008_panel_002_002.png",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"seed": 8,
|
|
||||||
"prompt": "local panel 8"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"panel_errors": {
|
"panel_errors": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"page_layouts": [
|
"page_layouts": [
|
||||||
{
|
|
||||||
"page_number": 1,
|
|
||||||
"pattern_id": "grid-2x2",
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_001",
|
|
||||||
"panel_number": 1,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.02000000,
|
|
||||||
"y": 0.02000000,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.47000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_002",
|
|
||||||
"panel_number": 2,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.50999999,
|
|
||||||
"y": 0.02000000,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.47000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_003",
|
|
||||||
"panel_number": 3,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.02000000,
|
|
||||||
"y": 0.50999999,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.47000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_004",
|
|
||||||
"panel_number": 4,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.50999999,
|
|
||||||
"y": 0.50999999,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.47000000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"width": 2480,
|
|
||||||
"height": 3508
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"page_number": 2,
|
|
||||||
"pattern_id": "dialogue-heavy",
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_005",
|
|
||||||
"panel_number": 5,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.02000000,
|
|
||||||
"y": 0.02000000,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.22000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_001_006",
|
|
||||||
"panel_number": 6,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.50999999,
|
|
||||||
"y": 0.02000000,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.22000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_001",
|
|
||||||
"panel_number": 1,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.02000000,
|
|
||||||
"y": 0.25999999,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.22000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_002",
|
|
||||||
"panel_number": 2,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.50999999,
|
|
||||||
"y": 0.25999999,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.22000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_003",
|
|
||||||
"panel_number": 3,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.02000000,
|
|
||||||
"y": 0.50000000,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.22000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_004",
|
|
||||||
"panel_number": 4,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.50999999,
|
|
||||||
"y": 0.50000000,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.22000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_005",
|
|
||||||
"panel_number": 5,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.02000000,
|
|
||||||
"y": 0.74000001,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.22000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"panel_id": "panel_002_006",
|
|
||||||
"panel_number": 6,
|
|
||||||
"layout_cell": {
|
|
||||||
"x": 0.50999999,
|
|
||||||
"y": 0.74000001,
|
|
||||||
"w": 0.47000000,
|
|
||||||
"h": 0.22000000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"width": 2480,
|
|
||||||
"height": 3508
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"speech_bubbles": {
|
"speech_bubbles": {
|
||||||
|
|
||||||
@ -526,7 +42,7 @@
|
|||||||
"page_size": 0,
|
"page_size": 0,
|
||||||
"color_profile": 0,
|
"color_profile": 0,
|
||||||
"workflow": {
|
"workflow": {
|
||||||
"current_step": 5,
|
"current_step": 0,
|
||||||
"completed_steps": [
|
"completed_steps": [
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|||||||
@ -15,7 +15,8 @@ echo "=> Building comic-odin v${VERSION} (${OS}-${ARCH})"
|
|||||||
|
|
||||||
echo "=> Running test suite"
|
echo "=> Running test suite"
|
||||||
CLAY_DIR="$ROOT_DIR/vendor/clay/bindings/odin/clay-odin"
|
CLAY_DIR="$ROOT_DIR/vendor/clay/bindings/odin/clay-odin"
|
||||||
odin test tests -collection:clay="$CLAY_DIR"
|
BUILD_DIR="$ROOT_DIR/build"
|
||||||
|
odin test tests -collection:clay="$CLAY_DIR" -extra-linker-flags:"-L$BUILD_DIR -losdialog"
|
||||||
|
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
PKG_NAME="comic-odin-${VERSION}-${OS}-${ARCH}"
|
PKG_NAME="comic-odin-${VERSION}-${OS}-${ARCH}"
|
||||||
|
|||||||
@ -9,20 +9,6 @@ import "../core"
|
|||||||
import "../shared"
|
import "../shared"
|
||||||
import "../ui"
|
import "../ui"
|
||||||
|
|
||||||
action_generate_local_script :: proc(controller: ^ui.App_Controller, pages: int) -> string {
|
|
||||||
story := controller.state.story_idea
|
|
||||||
if len(story) == 0 {
|
|
||||||
story = "A local GUI adventure"
|
|
||||||
}
|
|
||||||
script := build_local_script(story, pages)
|
|
||||||
core.dispose_script(&controller.state.script)
|
|
||||||
controller.state.script = script
|
|
||||||
controller.state.characters = controller.state.script.characters
|
|
||||||
controller.active_screen = .Script
|
|
||||||
controller.state.workflow.current_step = .Script_Review
|
|
||||||
return "Generated local script"
|
|
||||||
}
|
|
||||||
|
|
||||||
action_generate_deepseek_script :: proc(controller: ^ui.App_Controller, pages: int) -> string {
|
action_generate_deepseek_script :: proc(controller: ^ui.App_Controller, pages: int) -> string {
|
||||||
cfg := shared.load_config()
|
cfg := shared.load_config()
|
||||||
if len(cfg.deepseek_api_key) == 0 {
|
if len(cfg.deepseek_api_key) == 0 {
|
||||||
@ -47,74 +33,9 @@ action_generate_deepseek_script :: proc(controller: ^ui.App_Controller, pages: i
|
|||||||
return "Generated DeepSeek script"
|
return "Generated DeepSeek script"
|
||||||
}
|
}
|
||||||
|
|
||||||
action_generate_local_panels :: proc(controller: ^ui.App_Controller) -> string {
|
|
||||||
panels := collect_script_panels(controller.state.script)
|
|
||||||
defer delete(panels)
|
|
||||||
if len(panels) == 0 {
|
|
||||||
return "No script panels available"
|
|
||||||
}
|
|
||||||
images, ierr := build_local_panel_images(panels)
|
|
||||||
if !shared.is_ok(ierr) {
|
|
||||||
return ierr.message
|
|
||||||
}
|
|
||||||
for _, img in controller.state.panel_images {
|
|
||||||
delete(img.url)
|
|
||||||
delete(img.prompt)
|
|
||||||
}
|
|
||||||
delete(controller.state.panel_images)
|
|
||||||
controller.state.panel_images = images
|
|
||||||
controller.active_screen = .Panels
|
|
||||||
return "Generated local panels"
|
|
||||||
}
|
|
||||||
|
|
||||||
action_regenerate_panel :: proc(controller: ^ui.App_Controller, panel_id: string) -> string {
|
action_regenerate_panel :: proc(controller: ^ui.App_Controller, panel_id: string) -> string {
|
||||||
panels := collect_script_panels(controller.state.script)
|
_ = controller; _ = panel_id
|
||||||
defer delete(panels)
|
return "Single panel regen not supported; regenerate all panels via FAL"
|
||||||
|
|
||||||
target_panel: core.Panel
|
|
||||||
found := false
|
|
||||||
for p in panels {
|
|
||||||
if p.panel_id == panel_id {
|
|
||||||
target_panel = p
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return "Panel not found in script"
|
|
||||||
}
|
|
||||||
|
|
||||||
single := make([]core.Panel, 1)
|
|
||||||
single[0] = target_panel
|
|
||||||
defer delete(single)
|
|
||||||
|
|
||||||
images, ierr := build_local_panel_images(single)
|
|
||||||
if !shared.is_ok(ierr) {
|
|
||||||
if controller.state.panel_errors == nil {
|
|
||||||
controller.state.panel_errors = make(map[string]string)
|
|
||||||
}
|
|
||||||
controller.state.panel_errors[panel_id] = strings.clone(ierr.message)
|
|
||||||
return "Panel generation failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
if img, has := images[panel_id]; has {
|
|
||||||
if old, exists := controller.state.panel_images[panel_id]; exists {
|
|
||||||
delete(old.url)
|
|
||||||
delete(old.prompt)
|
|
||||||
}
|
|
||||||
if controller.state.panel_images == nil {
|
|
||||||
controller.state.panel_images = make(map[string]core.Panel_Image)
|
|
||||||
}
|
|
||||||
controller.state.panel_images[panel_id] = img
|
|
||||||
|
|
||||||
if err_msg, err_exists := controller.state.panel_errors[panel_id]; err_exists {
|
|
||||||
delete(err_msg)
|
|
||||||
delete_key(&controller.state.panel_errors, panel_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete(images) // free the map shell returned by build_local_panel_images
|
|
||||||
|
|
||||||
return "Regenerated panel"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
action_layout_auto :: proc(controller: ^ui.App_Controller) -> string {
|
action_layout_auto :: proc(controller: ^ui.App_Controller) -> string {
|
||||||
@ -337,15 +258,12 @@ action_export :: proc(controller: ^ui.App_Controller, export_path: string, expor
|
|||||||
return fmt.aprintf("Exported %s", export_format_name(export_format))
|
return fmt.aprintf("Exported %s", export_format_name(export_format))
|
||||||
}
|
}
|
||||||
|
|
||||||
gui_next_hint_with_source :: proc(controller: ui.App_Controller, use_deepseek_script: bool) -> string {
|
gui_next_hint :: proc(controller: ui.App_Controller) -> string {
|
||||||
if len(controller.state.script.pages) == 0 {
|
if len(controller.state.script.pages) == 0 {
|
||||||
if use_deepseek_script {
|
return "generate script"
|
||||||
return "generate script"
|
|
||||||
}
|
|
||||||
return "generate script local"
|
|
||||||
}
|
}
|
||||||
if len(controller.state.panel_images) == 0 {
|
if len(controller.state.panel_images) == 0 {
|
||||||
return "generate panels local"
|
return "generate panels"
|
||||||
}
|
}
|
||||||
if len(controller.state.page_layouts) == 0 {
|
if len(controller.state.page_layouts) == 0 {
|
||||||
return "layout auto"
|
return "layout auto"
|
||||||
@ -353,19 +271,15 @@ gui_next_hint_with_source :: proc(controller: ui.App_Controller, use_deepseek_sc
|
|||||||
return "export pdf"
|
return "export pdf"
|
||||||
}
|
}
|
||||||
|
|
||||||
gui_next_hint :: proc(controller: ui.App_Controller) -> string {
|
action_run_next :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format, script_pages: int) -> string {
|
||||||
return gui_next_hint_with_source(controller, false)
|
hint := gui_next_hint(controller^)
|
||||||
}
|
|
||||||
|
|
||||||
action_run_next :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format, script_pages: int, use_deepseek_script: bool) -> string {
|
|
||||||
hint := gui_next_hint_with_source(controller^, use_deepseek_script)
|
|
||||||
switch hint {
|
switch hint {
|
||||||
case "generate script":
|
case "generate script":
|
||||||
return action_generate_deepseek_script(controller, script_pages)
|
return action_generate_deepseek_script(controller, script_pages)
|
||||||
case "generate script local":
|
case "generate panels":
|
||||||
return action_generate_local_script(controller, script_pages)
|
cfg := shared.load_config()
|
||||||
case "generate panels local":
|
if len(cfg.fal_api_key) == 0 { return "FAL API key missing" }
|
||||||
return action_generate_local_panels(controller)
|
return run_panels_action(controller, nil, nil)
|
||||||
case "layout auto":
|
case "layout auto":
|
||||||
return action_layout_auto(controller)
|
return action_layout_auto(controller)
|
||||||
case "export pdf":
|
case "export pdf":
|
||||||
@ -374,9 +288,9 @@ action_run_next :: proc(controller: ^ui.App_Controller, export_path: string, exp
|
|||||||
return "No next action"
|
return "No next action"
|
||||||
}
|
}
|
||||||
|
|
||||||
action_run_auto_all_local :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format, script_pages: int, use_deepseek_script: bool) -> string {
|
action_run_auto_all :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format, script_pages: int) -> string {
|
||||||
for _ in 0..<4 {
|
for _ in 0..<4 {
|
||||||
msg := action_run_next(controller, export_path, export_format, script_pages, use_deepseek_script)
|
msg := action_run_next(controller, export_path, export_format, script_pages)
|
||||||
if controller.active_screen == .Export {
|
if controller.active_screen == .Export {
|
||||||
return fmt.aprintf("Auto-all complete: %s", msg)
|
return fmt.aprintf("Auto-all complete: %s", msg)
|
||||||
}
|
}
|
||||||
@ -384,20 +298,28 @@ action_run_auto_all_local :: proc(controller: ^ui.App_Controller, export_path: s
|
|||||||
return "Auto-all could not complete"
|
return "Auto-all could not complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_script_action :: proc(controller: ^ui.App_Controller, pages_count: int, use_deepseek_script: bool, is_dirty: ^bool) -> string {
|
run_panels_action :: proc(controller: ^ui.App_Controller, queue: ^adapters.Fal_Generation_Queue, is_dirty: ^bool) -> string {
|
||||||
is_dirty^ = true
|
if len(controller.state.script.pages) == 0 {
|
||||||
if use_deepseek_script {
|
|
||||||
return action_generate_deepseek_script(controller, pages_count)
|
|
||||||
}
|
|
||||||
return action_generate_local_script(controller, pages_count)
|
|
||||||
}
|
|
||||||
|
|
||||||
run_panels_action :: proc(controller: ^ui.App_Controller, can_generate_panels: bool, is_dirty: ^bool) -> string {
|
|
||||||
if !can_generate_panels {
|
|
||||||
return "Generate script before panels"
|
return "Generate script before panels"
|
||||||
}
|
}
|
||||||
|
cfg := shared.load_config()
|
||||||
|
if len(cfg.fal_api_key) == 0 {
|
||||||
|
return "FAL_API_KEY not set"
|
||||||
|
}
|
||||||
|
panels := collect_script_panels(controller.state.script)
|
||||||
|
client := adapters.new_fal_client(queue)
|
||||||
|
images, err := adapters.generate_all_panels_batched(client, cfg, panels, controller.state.characters, "digital art, comic style", "gui-project", nil)
|
||||||
|
if strings.contains(err.message, "error") || strings.contains(err.message, "fail") {
|
||||||
|
return fmt.tprintf("FAL panels failed: %s", err.message)
|
||||||
|
}
|
||||||
is_dirty^ = true
|
is_dirty^ = true
|
||||||
return action_generate_local_panels(controller)
|
for _, img in controller.state.panel_images {
|
||||||
|
delete(img.url)
|
||||||
|
delete(img.prompt)
|
||||||
|
}
|
||||||
|
delete(controller.state.panel_images)
|
||||||
|
controller.state.panel_images = images
|
||||||
|
return fmt.tprintf("Generated %d panels via FAL", len(images))
|
||||||
}
|
}
|
||||||
|
|
||||||
run_layout_action :: proc(controller: ^ui.App_Controller, can_layout: bool, is_dirty: ^bool) -> string {
|
run_layout_action :: proc(controller: ^ui.App_Controller, can_layout: bool, is_dirty: ^bool) -> string {
|
||||||
@ -421,9 +343,9 @@ run_export_action :: proc(controller: ^ui.App_Controller, export_path: ^string,
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
run_next_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, pages_count: int, use_deepseek_script: bool, is_dirty: ^bool, last_export_at: ^f64) -> string {
|
run_next_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, pages_count: int, is_dirty: ^bool, last_export_at: ^f64) -> string {
|
||||||
normalize_export_path_field(export_path, export_format)
|
normalize_export_path_field(export_path, export_format)
|
||||||
msg := action_run_next(controller, export_path^, export_format, pages_count, use_deepseek_script)
|
msg := action_run_next(controller, export_path^, export_format, pages_count)
|
||||||
is_dirty^ = true
|
is_dirty^ = true
|
||||||
if controller.active_screen == .Export {
|
if controller.active_screen == .Export {
|
||||||
last_export_at^ = rl.GetTime()
|
last_export_at^ = rl.GetTime()
|
||||||
@ -431,9 +353,9 @@ run_next_action :: proc(controller: ^ui.App_Controller, export_path: ^string, ex
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
run_auto_all_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, pages_count: int, use_deepseek_script: bool, is_dirty: ^bool, last_export_at: ^f64) -> string {
|
run_auto_all_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, pages_count: int, is_dirty: ^bool, last_export_at: ^f64) -> string {
|
||||||
normalize_export_path_field(export_path, export_format)
|
normalize_export_path_field(export_path, export_format)
|
||||||
msg := action_run_auto_all_local(controller, export_path^, export_format, pages_count, use_deepseek_script)
|
msg := action_run_auto_all(controller, export_path^, export_format, pages_count)
|
||||||
is_dirty^ = true
|
is_dirty^ = true
|
||||||
if controller.active_screen == .Export {
|
if controller.active_screen == .Export {
|
||||||
last_export_at^ = rl.GetTime()
|
last_export_at^ = rl.GetTime()
|
||||||
@ -441,9 +363,9 @@ run_auto_all_action :: proc(controller: ^ui.App_Controller, export_path: ^string
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
run_auto_all_save_action :: proc(controller: ^ui.App_Controller, project_path, export_path: ^string, export_format: core.Export_Format, pages_count: int, use_deepseek_script: bool, is_dirty: ^bool, last_export_at, last_autosave_at, last_save_at: ^f64) -> string {
|
run_auto_all_save_action :: proc(controller: ^ui.App_Controller, project_path, export_path: ^string, export_format: core.Export_Format, pages_count: int, is_dirty: ^bool, last_export_at, last_autosave_at, last_save_at: ^f64) -> string {
|
||||||
normalize_export_path_field(export_path, export_format)
|
normalize_export_path_field(export_path, export_format)
|
||||||
msg := action_run_auto_all_local(controller, export_path^, export_format, pages_count, use_deepseek_script)
|
msg := action_run_auto_all(controller, export_path^, export_format, pages_count)
|
||||||
if controller.active_screen == .Export {
|
if controller.active_screen == .Export {
|
||||||
last_export_at^ = rl.GetTime()
|
last_export_at^ = rl.GetTime()
|
||||||
return save_project_session_with_message(project_path, controller.state, is_dirty, last_autosave_at, last_save_at, "Auto-all + saved")
|
return save_project_session_with_message(project_path, controller.state, is_dirty, last_autosave_at, last_save_at, "Auto-all + saved")
|
||||||
|
|||||||
@ -165,6 +165,7 @@ declare_pipeline_bar :: proc(app: ^GUI_App_State) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Status message (truncated)
|
// Status message (truncated)
|
||||||
|
clay_body_text(fmt.tprintf("C:%d P:%d", len(app.controller.state.characters), len(app.controller.state.panel_images)), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||||
msg := app.status_msg
|
msg := app.status_msg
|
||||||
if len(msg) > 40 { msg = fmt.tprintf("%s...", msg[:37]) }
|
if len(msg) > 40 { msg = fmt.tprintf("%s...", msg[:37]) }
|
||||||
clay_body_text(msg, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
clay_body_text(msg, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||||
@ -208,7 +209,7 @@ declare_workspace :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_e
|
|||||||
// ─── Script Workspace ────────────────────────────────────────────
|
// ─── Script Workspace ────────────────────────────────────────────
|
||||||
|
|
||||||
// ─── Bottom Bar ───────────────────────────────────────────────────
|
// ─── Bottom Bar ───────────────────────────────────────────────────
|
||||||
declare_bottom_bar :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export: bool, next_hint: string) {
|
declare_bottom_bar :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export: bool, next_hint: string, has_fal_key: bool) {
|
||||||
if clay.UI(clay.ID("BottomBar"))({
|
if clay.UI(clay.ID("BottomBar"))({
|
||||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({min = 50, max = 44})}, padding = {top = 6, right = 12, bottom = 6, left = 12}, childGap = 8, childAlignment = {y = .Center}, layoutDirection = .LeftToRight},
|
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({min = 50, max = 44})}, padding = {top = 6, right = 12, bottom = 6, left = 12}, childGap = 8, childAlignment = {y = .Center}, layoutDirection = .LeftToRight},
|
||||||
backgroundColor = CLAY_BG_TOPBAR,
|
backgroundColor = CLAY_BG_TOPBAR,
|
||||||
@ -232,9 +233,9 @@ declare_bottom_bar :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_
|
|||||||
if bbspacer2({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}}) {}
|
if bbspacer2({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}}) {}
|
||||||
|
|
||||||
// Right: quick actions
|
// Right: quick actions
|
||||||
clay_muted_text("Src:")
|
if has_fal_key {
|
||||||
declare_nav_chip("btn_local", "Local", !app.use_deepseek_script)
|
declare_nav_chip("btn_fal_panels", "FAL", app.use_fal_panels)
|
||||||
declare_nav_chip("btn_deepseek", "DS", app.use_deepseek_script)
|
}
|
||||||
declare_button_small("btn_script", "Script")
|
declare_button_small("btn_script", "Script")
|
||||||
declare_button_small("btn_panels", "Panels")
|
declare_button_small("btn_panels", "Panels")
|
||||||
declare_button_small("btn_layout", "Layout")
|
declare_button_small("btn_layout", "Layout")
|
||||||
|
|||||||
@ -29,6 +29,7 @@ clay_color_to_rl :: proc(color: clay.Color) -> rl.Color {
|
|||||||
// --- Clay Error Handler ---
|
// --- Clay Error Handler ---
|
||||||
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
|
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
|
||||||
context = runtime.default_context()
|
context = runtime.default_context()
|
||||||
|
if errorData.errorType == .DuplicateId || errorData.errorType == .PercentageOver1 { return }
|
||||||
fmt.eprintf("CLAY ERROR: %v\n", errorData)
|
fmt.eprintf("CLAY ERROR: %v\n", errorData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,8 +22,8 @@ declare_script_detail :: proc(app: ^GUI_App_State) {
|
|||||||
if clay.UI(clay.ID("ScriptDetailStatRow"))({
|
if clay.UI(clay.ID("ScriptDetailStatRow"))({
|
||||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 8},
|
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 8},
|
||||||
}) {
|
}) {
|
||||||
declare_stat_chip("Page", idx + 1)
|
declare_stat_chip("stat_page", "Page", idx + 1)
|
||||||
declare_stat_chip("Panels", len(page.panels))
|
declare_stat_chip("stat_panels", "Panels", len(page.panels))
|
||||||
}
|
}
|
||||||
|
|
||||||
clay_body_text(fmt.tprintf("Page #%d", page.page_number), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
clay_body_text(fmt.tprintf("Page #%d", page.page_number), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||||
@ -91,23 +91,69 @@ declare_panels_detail :: proc(app: ^GUI_App_State) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clay_muted_text(fmt.tprintf("id: %s", panel.panel_id))
|
// Panel image preview + info side-by-side
|
||||||
if has_err {
|
|
||||||
err_msg, _ := app.controller.state.panel_errors[panel.panel_id]
|
|
||||||
clay_body_text(fmt.tprintf("err: %s", err_msg), color = CLAY_ERROR, size = CLAY_FONT_SIZE_SM)
|
|
||||||
} else if has_img {
|
|
||||||
img := app.controller.state.panel_images[panel.panel_id]
|
|
||||||
clay_muted_text(fmt.tprintf("img: %dx%d seed:%d", img.width, img.height, img.seed))
|
|
||||||
} else {
|
|
||||||
clay_muted_text("img: not generated")
|
|
||||||
}
|
|
||||||
|
|
||||||
desc := panel.description
|
|
||||||
if len(desc) == 0 { desc = "(no description)" }
|
|
||||||
clay_body_text(fmt.tprintf("desc: %s", desc), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
|
||||||
|
|
||||||
if has_img {
|
if has_img {
|
||||||
clay_muted_text(fmt.tprintf("src: %s", panel.panel_id))
|
// Try to load and show the image
|
||||||
|
panel_img := app.controller.state.panel_images[panel.panel_id]
|
||||||
|
_, loaded := load_panel_texture(&app.panel_textures, panel.panel_id, panel_img.url)
|
||||||
|
img_shown := false
|
||||||
|
if loaded {
|
||||||
|
tex_ptr := &app.panel_textures[panel.panel_id]
|
||||||
|
if tex_ptr.id != 0 {
|
||||||
|
if clay.UI(clay.ID("PanelImageRow"))({
|
||||||
|
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 12},
|
||||||
|
}) {
|
||||||
|
if clay.UI(clay.ID("PanelImagePreview"))({
|
||||||
|
layout = {sizing = {width = clay.SizingFixed(200), height = clay.SizingFixed(200)}},
|
||||||
|
backgroundColor = CLAY_BG_STRIP,
|
||||||
|
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||||
|
image = {imageData = rawptr(tex_ptr)},
|
||||||
|
}) {}
|
||||||
|
if clay.UI(clay.ID("PanelImageInfo"))({layout = {layoutDirection = .TopToBottom, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 2}}) {
|
||||||
|
clay_muted_text(fmt.tprintf("id: %s", panel.panel_id))
|
||||||
|
if has_err {
|
||||||
|
err_msg, _ := app.controller.state.panel_errors[panel.panel_id]
|
||||||
|
clay_body_text(fmt.tprintf("err: %s", err_msg), color = CLAY_ERROR, size = CLAY_FONT_SIZE_SM)
|
||||||
|
}
|
||||||
|
img := panel_img
|
||||||
|
clay_muted_text(fmt.tprintf("img: %dx%d seed:%d", img.width, img.height, img.seed))
|
||||||
|
desc := panel.description
|
||||||
|
if len(desc) == 0 { desc = "(no description)" }
|
||||||
|
clay_body_text(fmt.tprintf("desc: %s", desc), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img_shown = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !img_shown {
|
||||||
|
clay_muted_text(fmt.tprintf("id: %s", panel.panel_id))
|
||||||
|
if has_err {
|
||||||
|
err_msg, _ := app.controller.state.panel_errors[panel.panel_id]
|
||||||
|
clay_body_text(fmt.tprintf("err: %s", err_msg), color = CLAY_ERROR, size = CLAY_FONT_SIZE_SM)
|
||||||
|
}
|
||||||
|
img := panel_img
|
||||||
|
clay_muted_text(fmt.tprintf("img: %dx%d seed:%d (failed to load)", img.width, img.height, img.seed))
|
||||||
|
desc := panel.description
|
||||||
|
if len(desc) == 0 { desc = "(no description)" }
|
||||||
|
clay_body_text(fmt.tprintf("desc: %s", desc), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clay_muted_text(fmt.tprintf("id: %s", panel.panel_id))
|
||||||
|
if has_err {
|
||||||
|
err_msg, _ := app.controller.state.panel_errors[panel.panel_id]
|
||||||
|
clay_body_text(fmt.tprintf("err: %s", err_msg), color = CLAY_ERROR, size = CLAY_FONT_SIZE_SM)
|
||||||
|
} else if has_img {
|
||||||
|
img := app.controller.state.panel_images[panel.panel_id]
|
||||||
|
clay_muted_text(fmt.tprintf("img: %dx%d seed:%d", img.width, img.height, img.seed))
|
||||||
|
} else {
|
||||||
|
clay_muted_text("img: not generated")
|
||||||
|
}
|
||||||
|
desc := panel.description
|
||||||
|
if len(desc) == 0 { desc = "(no description)" }
|
||||||
|
clay_body_text(fmt.tprintf("desc: %s", desc), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||||
|
if has_img {
|
||||||
|
clay_muted_text(fmt.tprintf("src: %s", panel.panel_id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if clay.UI(clay.ID("PanelListScroll"))({
|
if clay.UI(clay.ID("PanelListScroll"))({
|
||||||
@ -364,9 +410,26 @@ declare_bubbles_detail :: proc(app: ^GUI_App_State) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text_preview := selected.text
|
// Text editing area
|
||||||
if len(text_preview) > 80 { text_preview = text_preview[:80] }
|
if clay.UI(clay.ID("BubbleTextRow"))({
|
||||||
clay_muted_text(fmt.tprintf("text: %s", text_preview))
|
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4, childAlignment = {y = .Center}},
|
||||||
|
}) {
|
||||||
|
clay_muted_text("Text:")
|
||||||
|
text_to_show := app.bubble_edit_text if app.selected_field == 7 else selected.text
|
||||||
|
if len(text_to_show) == 0 && app.selected_field != 7 {
|
||||||
|
text_to_show = "(click to edit)"
|
||||||
|
}
|
||||||
|
if clay.UI(clay.ID("field_bubble_text"))({
|
||||||
|
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(34)}, padding = {top = 6, right = 8, bottom = 6, left = 8}},
|
||||||
|
backgroundColor = CLAY_BG_INPUT,
|
||||||
|
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||||
|
border = {color = CLAY_INPUT_FOCUS if app.selected_field == 7 else CLAY_INPUT_BORDER, width = clay.BorderOutside(1)},
|
||||||
|
}) {
|
||||||
|
text_color := CLAY_TEXT_PRIMARY if app.selected_field == 7 else CLAY_TEXT_SECONDARY
|
||||||
|
clay_body_text(text_to_show, color = text_color, size = CLAY_FONT_SIZE_SM)
|
||||||
|
}
|
||||||
|
declare_button_small("btn_bubble_save_text", "Save")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
package gui
|
package gui
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
|
||||||
import "../core"
|
import "../core"
|
||||||
import "../shared"
|
|
||||||
|
|
||||||
collect_script_panels :: proc(script: core.Comic_Script) -> []core.Panel {
|
collect_script_panels :: proc(script: core.Comic_Script) -> []core.Panel {
|
||||||
out: [dynamic]core.Panel
|
out: [dynamic]core.Panel
|
||||||
@ -32,75 +30,7 @@ local_panel_id_by_index :: proc(i: int) -> string {
|
|||||||
case 4: return "panel_local_005"
|
case 4: return "panel_local_005"
|
||||||
case 5: return "panel_local_006"
|
case 5: return "panel_local_006"
|
||||||
}
|
}
|
||||||
return "panel_local_overflow"
|
return fmt.tprintf("panel_local_overflow_%d", i)
|
||||||
}
|
|
||||||
|
|
||||||
build_local_script :: proc(story_idea: string, pages: int) -> core.Comic_Script {
|
|
||||||
out_pages: [dynamic]core.Page
|
|
||||||
for i in 0..<pages {
|
|
||||||
chars_present := make([]string, 1)
|
|
||||||
chars_present[0] = "char_001"
|
|
||||||
dialogue := make([]core.Dialogue, 1)
|
|
||||||
dialogue[0] = core.Dialogue{speaker_id = "char_001", text = "Let's do this.", bubble_type = .Normal, emotion = .Neutral}
|
|
||||||
|
|
||||||
panel := core.Panel{
|
|
||||||
panel_id = local_panel_id_by_index(i),
|
|
||||||
panel_number = 1,
|
|
||||||
shot_type = .Medium,
|
|
||||||
description = story_idea,
|
|
||||||
characters_present = chars_present,
|
|
||||||
dialogue = dialogue,
|
|
||||||
caption = "",
|
|
||||||
sound_effects = nil,
|
|
||||||
transition_from_previous = .None,
|
|
||||||
}
|
|
||||||
panels := make([]core.Panel, 1)
|
|
||||||
panels[0] = panel
|
|
||||||
append(&out_pages, core.Page{page_number = i + 1, layout_type = .Grid, panels = panels})
|
|
||||||
}
|
|
||||||
|
|
||||||
chars := make([]core.Character, 1)
|
|
||||||
chars[0] = core.Character{id = "char_001", name = "Protagonist", role = .Protagonist, description = "Main character"}
|
|
||||||
return core.Comic_Script{title = "Local Script", synopsis = story_idea, characters = chars, pages = out_pages[:]}
|
|
||||||
}
|
|
||||||
|
|
||||||
build_local_panel_images :: proc(panels: []core.Panel) -> (map[string]core.Panel_Image, shared.App_Error) {
|
|
||||||
tmp_dir, terr := os.make_directory_temp("", "comic-gui-local-panels-*", context.temp_allocator)
|
|
||||||
if terr != nil {
|
|
||||||
return nil, shared.new_error(.Generation, "failed to create local panel temp dir", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
images := make(map[string]core.Panel_Image)
|
|
||||||
for p, idx in panels {
|
|
||||||
name := fmt.aprintf("panel_%03d_%s.png", idx+1, p.panel_id)
|
|
||||||
out_path := fmt.aprintf("%s/%s", tmp_dir, name)
|
|
||||||
delete(name)
|
|
||||||
|
|
||||||
// Create a real PNG image using python3
|
|
||||||
gen_w := 1024
|
|
||||||
gen_h := 1024
|
|
||||||
py_script := fmt.aprintf(
|
|
||||||
"import struct,zlib,sys;w,h=%d,%d;rows=[]\nfor _ in range(h): rows.append(b'\\x00'+b'\\xff\\xff\\xff'*w)\nraw=b''.join(rows);comp=zlib.compress(raw)\ndef crc(d): return struct.pack('>I',zlib.crc32(d)&0xffffffff)\nf=open(sys.argv[1],'wb')\nf.write(b'\\x89PNG\\r\\n\\x1a\\n')\nihdr_data=struct.pack('>IIBBBBB',w,h,8,2,0,0,0)\nf.write(struct.pack('>I',13)+b'IHDR'+ihdr_data+crc(b'IHDR'+ihdr_data))\nf.write(struct.pack('>I',len(comp))+b'IDAT'+comp+crc(b'IDAT'+comp))\nf.write(struct.pack('>I',0)+b'IEND'+crc(b'IEND'))\nf.close()",
|
|
||||||
gen_w, gen_h,
|
|
||||||
)
|
|
||||||
defer delete(py_script)
|
|
||||||
|
|
||||||
py_cmd := [4]string{"python3", "-c", py_script, out_path}
|
|
||||||
desc := os.Process_Desc{command = py_cmd[:]}
|
|
||||||
state, _, stderr, cerr := os.process_exec(desc, context.temp_allocator)
|
|
||||||
if cerr != nil || !state.exited || state.exit_code != 0 {
|
|
||||||
delete(out_path)
|
|
||||||
msg := fmt.aprintf("failed to create panel image: %s", string(stderr))
|
|
||||||
defer delete(msg)
|
|
||||||
return nil, shared.new_error(.Generation, msg, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.aprintf("file://%s", out_path)
|
|
||||||
prompt := fmt.aprintf("local panel %d", idx+1)
|
|
||||||
images[p.panel_id] = core.Panel_Image{url = url, width = gen_w, height = gen_h, seed = i64(idx + 1), prompt = prompt}
|
|
||||||
delete(out_path)
|
|
||||||
}
|
|
||||||
return images, shared.ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
append_char :: proc(dst: ^string, ch: rune) {
|
append_char :: proc(dst: ^string, ch: rune) {
|
||||||
|
|||||||
@ -86,9 +86,9 @@ declare_status_badge :: proc(id: string, label: string, ok: bool) {
|
|||||||
|
|
||||||
// ─── Stat Chip (Clay) ──────────────────────────────────────────────
|
// ─── Stat Chip (Clay) ──────────────────────────────────────────────
|
||||||
// ─── Stat Chip (Clay) ──────────────────────────────────────────────
|
// ─── Stat Chip (Clay) ──────────────────────────────────────────────
|
||||||
declare_stat_chip :: proc(label: string, value: int) {
|
declare_stat_chip :: proc(id: string, label: string, value: int) {
|
||||||
value_text := fmt.tprintf("%d", value)
|
value_text := fmt.tprintf("%d", value)
|
||||||
if clay.UI(clay.ID("StatChip", u32(label[0])))({
|
if clay.UI(clay.ID(id))({
|
||||||
layout = {sizing = {width = clay.SizingFixed(90), height = clay.SizingFixed(34)}, padding = {top = 4, right = 8, bottom = 4, left = 8}, childAlignment = {x = .Center, y = .Center}},
|
layout = {sizing = {width = clay.SizingFixed(90), height = clay.SizingFixed(34)}, padding = {top = 4, right = 8, bottom = 4, left = 8}, childAlignment = {x = .Center, y = .Center}},
|
||||||
backgroundColor = clay.Color{40, 40, 55, 255},
|
backgroundColor = clay.Color{40, 40, 55, 255},
|
||||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||||
|
|||||||
@ -17,7 +17,7 @@ GUI_App_State :: struct {
|
|||||||
local_script_pages: string,
|
local_script_pages: string,
|
||||||
autosave_interval_text: string,
|
autosave_interval_text: string,
|
||||||
export_format: core.Export_Format,
|
export_format: core.Export_Format,
|
||||||
use_deepseek_script: bool,
|
use_fal_panels: bool,
|
||||||
status_msg: string,
|
status_msg: string,
|
||||||
is_dirty: bool,
|
is_dirty: bool,
|
||||||
autosave_enabled: bool,
|
autosave_enabled: bool,
|
||||||
@ -32,12 +32,49 @@ GUI_App_State :: struct {
|
|||||||
show_help_overlay: bool,
|
show_help_overlay: bool,
|
||||||
show_confirm_overlay: bool,
|
show_confirm_overlay: bool,
|
||||||
pending_confirm: Pending_Confirm_Action,
|
pending_confirm: Pending_Confirm_Action,
|
||||||
|
panel_textures: map[string]rl.Texture2D,
|
||||||
|
bubble_edit_text: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
clicked :: proc(id: clay.ElementId) -> bool {
|
clicked :: proc(id: clay.ElementId) -> bool {
|
||||||
return clay.PointerOver(id) && rl.IsMouseButtonPressed(.LEFT)
|
return clay.PointerOver(id) && rl.IsMouseButtonPressed(.LEFT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Panel Image Loading ─────────────────────────────────────────
|
||||||
|
@(private)
|
||||||
|
file_url_to_path :: proc(url: string) -> string {
|
||||||
|
if strings.has_prefix(url, "file://") {
|
||||||
|
return url[7:]
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
load_panel_texture :: proc(cache: ^map[string]rl.Texture2D, panel_id: string, url: string) -> (rl.Texture2D, bool) {
|
||||||
|
if tex, ok := cache[panel_id]; ok {
|
||||||
|
return tex, true
|
||||||
|
}
|
||||||
|
filepath := file_url_to_path(url)
|
||||||
|
if len(filepath) == 0 { return {}, false }
|
||||||
|
fpath_c := strings.clone_to_cstring(filepath)
|
||||||
|
defer delete(fpath_c)
|
||||||
|
img := rl.LoadImage(fpath_c)
|
||||||
|
if img.data == nil { return {}, false }
|
||||||
|
defer rl.UnloadImage(img)
|
||||||
|
tex := rl.LoadTextureFromImage(img)
|
||||||
|
if tex.id == 0 { return {}, false }
|
||||||
|
cache[panel_id] = tex
|
||||||
|
return tex, true
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
unload_panel_textures :: proc(cache: ^map[string]rl.Texture2D) {
|
||||||
|
for _, tex in cache {
|
||||||
|
rl.UnloadTexture(tex)
|
||||||
|
}
|
||||||
|
delete(cache^)
|
||||||
|
}
|
||||||
|
|
||||||
run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||||
controller := ui.new_controller(state^)
|
controller := ui.new_controller(state^)
|
||||||
defer ui.dispose_job_manager(&controller.jobs)
|
defer ui.dispose_job_manager(&controller.jobs)
|
||||||
@ -63,7 +100,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
app.local_script_pages = "2"
|
app.local_script_pages = "2"
|
||||||
app.autosave_interval_text = "20"
|
app.autosave_interval_text = "20"
|
||||||
app.export_format = .PDF
|
app.export_format = .PDF
|
||||||
app.use_deepseek_script = false
|
app.use_fal_panels = false
|
||||||
app.status_msg = fmt.aprintf("GUI ready")
|
app.status_msg = fmt.aprintf("GUI ready")
|
||||||
app.autosave_enabled = true
|
app.autosave_enabled = true
|
||||||
app.autosave_interval_s = 20
|
app.autosave_interval_s = 20
|
||||||
@ -75,6 +112,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
push_status(&app.status_msg, &app.action_log, app.status_msg)
|
push_status(&app.status_msg, &app.action_log, app.status_msg)
|
||||||
defer action_log_dispose(&app.action_log)
|
defer action_log_dispose(&app.action_log)
|
||||||
defer delete(app.status_msg)
|
defer delete(app.status_msg)
|
||||||
|
defer unload_panel_textures(&app.panel_textures)
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
for !rl.WindowShouldClose() {
|
||||||
screen_w := rl.GetScreenWidth()
|
screen_w := rl.GetScreenWidth()
|
||||||
@ -82,6 +120,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
compact_mode := shared.is_compact(screen_h)
|
compact_mode := shared.is_compact(screen_h)
|
||||||
cfg := shared.load_config()
|
cfg := shared.load_config()
|
||||||
has_deepseek_key := len(cfg.deepseek_api_key) > 0
|
has_deepseek_key := len(cfg.deepseek_api_key) > 0
|
||||||
|
has_fal_key := len(cfg.fal_api_key) > 0
|
||||||
|
|
||||||
clay_update_dimensions(screen_w, screen_h)
|
clay_update_dimensions(screen_w, screen_h)
|
||||||
clay_update_input()
|
clay_update_input()
|
||||||
@ -156,6 +195,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
case 4: append_char(&app.local_script_pages, ch)
|
case 4: append_char(&app.local_script_pages, ch)
|
||||||
case 5: append_char(&app.project_path, ch)
|
case 5: append_char(&app.project_path, ch)
|
||||||
case 6: append_char(&app.autosave_interval_text, ch)
|
case 6: append_char(&app.autosave_interval_text, ch)
|
||||||
|
case 7: append_char(&app.bubble_edit_text, ch)
|
||||||
}
|
}
|
||||||
app.is_dirty = true
|
app.is_dirty = true
|
||||||
}
|
}
|
||||||
@ -168,10 +208,11 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
case 4: pop_char(&app.local_script_pages)
|
case 4: pop_char(&app.local_script_pages)
|
||||||
case 5: pop_char(&app.project_path)
|
case 5: pop_char(&app.project_path)
|
||||||
case 6: pop_char(&app.autosave_interval_text)
|
case 6: pop_char(&app.autosave_interval_text)
|
||||||
|
case 7: pop_char(&app.bubble_edit_text)
|
||||||
|
}
|
||||||
|
app.is_dirty = true
|
||||||
}
|
}
|
||||||
app.is_dirty = true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Keyboard Shortcuts ─────────────────────────────────────
|
// ─── Keyboard Shortcuts ─────────────────────────────────────
|
||||||
ctrl_down := rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)
|
ctrl_down := rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)
|
||||||
@ -196,19 +237,6 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
if ctrl_down && rl.IsKeyPressed(.E) {
|
if ctrl_down && rl.IsKeyPressed(.E) {
|
||||||
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
||||||
}
|
}
|
||||||
if ctrl_down && rl.IsKeyPressed(.G) {
|
|
||||||
if app.use_deepseek_script {
|
|
||||||
app.use_deepseek_script = false
|
|
||||||
push_status(&app.status_msg, &app.action_log, "Script source: Local")
|
|
||||||
} else {
|
|
||||||
if !has_deepseek_key {
|
|
||||||
push_status(&app.status_msg, &app.action_log, "DeepSeek key missing (set DEEPSEEK_API_KEY)")
|
|
||||||
} else {
|
|
||||||
app.use_deepseek_script = true
|
|
||||||
push_status(&app.status_msg, &app.action_log, "Script source: DeepSeek")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ctrl_down && rl.IsKeyPressed(.H) {
|
if ctrl_down && rl.IsKeyPressed(.H) {
|
||||||
push_status_if_nonempty(&app.status_msg, &app.action_log, toggle_summary_show_if_supported(app.controller.active_screen, &app.summary_opts))
|
push_status_if_nonempty(&app.status_msg, &app.action_log, toggle_summary_show_if_supported(app.controller.active_screen, &app.summary_opts))
|
||||||
}
|
}
|
||||||
@ -276,10 +304,10 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, normalize_export_path_with_message(&app.export_path, app.export_format))
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, normalize_export_path_with_message(&app.export_path, app.export_format))
|
||||||
}
|
}
|
||||||
if rl.IsKeyPressed(.F5) {
|
if rl.IsKeyPressed(.F5) {
|
||||||
push_status(&app.status_msg, &app.action_log, run_script_action(&app.controller, pages_count, app.use_deepseek_script, &app.is_dirty))
|
push_status(&app.status_msg, &app.action_log, action_generate_deepseek_script(&app.controller, pages_count))
|
||||||
}
|
}
|
||||||
if rl.IsKeyPressed(.F6) {
|
if rl.IsKeyPressed(.F6) {
|
||||||
push_status(&app.status_msg, &app.action_log, run_panels_action(&app.controller, can_generate_panels, &app.is_dirty))
|
push_status(&app.status_msg, &app.action_log, "Use FAL panel button to generate panels")
|
||||||
}
|
}
|
||||||
if rl.IsKeyPressed(.F7) {
|
if rl.IsKeyPressed(.F7) {
|
||||||
push_status(&app.status_msg, &app.action_log, run_layout_action(&app.controller, can_layout, &app.is_dirty))
|
push_status(&app.status_msg, &app.action_log, run_layout_action(&app.controller, can_layout, &app.is_dirty))
|
||||||
@ -288,10 +316,10 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
||||||
}
|
}
|
||||||
if rl.IsKeyPressed(.F9) {
|
if rl.IsKeyPressed(.F9) {
|
||||||
push_status(&app.status_msg, &app.action_log, run_next_action(&app.controller, &app.export_path, app.export_format, pages_count, app.use_deepseek_script, &app.is_dirty, &app.last_export_at))
|
push_status(&app.status_msg, &app.action_log, run_next_action(&app.controller, &app.export_path, app.export_format, pages_count, &app.is_dirty, &app.last_export_at))
|
||||||
}
|
}
|
||||||
if rl.IsKeyPressed(.F10) {
|
if rl.IsKeyPressed(.F10) {
|
||||||
push_status(&app.status_msg, &app.action_log, run_auto_all_action(&app.controller, &app.export_path, app.export_format, pages_count, app.use_deepseek_script, &app.is_dirty, &app.last_export_at))
|
push_status(&app.status_msg, &app.action_log, run_auto_all_action(&app.controller, &app.export_path, app.export_format, pages_count, &app.is_dirty, &app.last_export_at))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,8 +344,8 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
}) {
|
}) {
|
||||||
declare_pipeline_bar(&app)
|
declare_pipeline_bar(&app)
|
||||||
declare_workspace(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, pages_count)
|
declare_workspace(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, pages_count)
|
||||||
next_hint := gui_next_hint_with_source(app.controller, app.use_deepseek_script)
|
next_hint := gui_next_hint(app.controller)
|
||||||
declare_bottom_bar(&app, can_generate_panels, can_layout, can_export, next_hint)
|
declare_bottom_bar(&app, can_generate_panels, can_layout, can_export, next_hint, has_fal_key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,7 +361,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
render_commands := clay.EndLayout(rl.GetFrameTime())
|
render_commands := clay.EndLayout(rl.GetFrameTime())
|
||||||
|
|
||||||
// ─── Click Detection (after layout, before render) ────────
|
// ─── Click Detection (after layout, before render) ────────
|
||||||
process_clicks(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, pages_count, shift_down, autosave_secs, compact_mode)
|
process_clicks(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, has_fal_key, pages_count, shift_down, autosave_secs, compact_mode)
|
||||||
|
|
||||||
// ─── Render ────────────────────────────────────────────────────
|
// ─── Render ────────────────────────────────────────────────────
|
||||||
rl.BeginDrawing()
|
rl.BeginDrawing()
|
||||||
@ -377,20 +405,15 @@ handle_format_clicks :: proc(app: ^GUI_App_State, has_deepseek: bool) {
|
|||||||
if clicked(clay.ID("btn_pdf")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .PDF, &app.is_dirty)) }
|
if clicked(clay.ID("btn_pdf")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .PDF, &app.is_dirty)) }
|
||||||
if clicked(clay.ID("btn_png")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .PNG, &app.is_dirty)) }
|
if clicked(clay.ID("btn_png")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .PNG, &app.is_dirty)) }
|
||||||
if clicked(clay.ID("btn_cbz")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .CBZ, &app.is_dirty)) }
|
if clicked(clay.ID("btn_cbz")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .CBZ, &app.is_dirty)) }
|
||||||
if clicked(clay.ID("btn_local")) { app.use_deepseek_script = false; push_status(&app.status_msg, &app.action_log, "Script source: Local") }
|
|
||||||
if clicked(clay.ID("btn_deepseek")) {
|
|
||||||
if !has_deepseek { push_status(&app.status_msg, &app.action_log, "DeepSeek key missing") }
|
|
||||||
else { app.use_deepseek_script = true; push_status(&app.status_msg, &app.action_log, "Script source: DeepSeek") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_action_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export: bool, pages_count: int, shift_down: bool, proj_ok, export_ok: bool, autosave_secs: int) {
|
handle_action_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export: bool, pages_count: int, shift_down: bool, proj_ok, export_ok: bool, autosave_secs: int, has_fal_key: bool) {
|
||||||
if clicked(clay.ID("btn_new")) {
|
if clicked(clay.ID("btn_new")) {
|
||||||
if app.is_dirty && !shift_down { push_status(&app.status_msg, &app.action_log, request_confirmation(&app.show_confirm_overlay, &app.show_help_overlay, &app.pending_confirm, .Reset_Project, "Confirm reset?")) }
|
if app.is_dirty && !shift_down { push_status(&app.status_msg, &app.action_log, request_confirmation(&app.show_confirm_overlay, &app.show_help_overlay, &app.pending_confirm, .Reset_Project, "Confirm reset?")) }
|
||||||
else { push_status(&app.status_msg, &app.action_log, reset_project_session(&app.controller, &app.is_dirty, &app.last_autosave_at, false)) }
|
else { push_status(&app.status_msg, &app.action_log, reset_project_session(&app.controller, &app.is_dirty, &app.last_autosave_at, false)) }
|
||||||
}
|
}
|
||||||
if clicked(clay.ID("btn_script")) { push_status(&app.status_msg, &app.action_log, run_script_action(&app.controller, pages_count, app.use_deepseek_script, &app.is_dirty)) }
|
if clicked(clay.ID("btn_script")) { push_status(&app.status_msg, &app.action_log, action_generate_deepseek_script(&app.controller, pages_count)) }
|
||||||
if clicked(clay.ID("btn_panels")) { push_status(&app.status_msg, &app.action_log, run_panels_action(&app.controller, can_gen_panels, &app.is_dirty)) }
|
if clicked(clay.ID("btn_panels")) { push_status(&app.status_msg, &app.action_log, "Click [FAL] then [Panels] to generate via FAL") }
|
||||||
if clicked(clay.ID("btn_layout")) { push_status(&app.status_msg, &app.action_log, run_layout_action(&app.controller, can_layout, &app.is_dirty)) }
|
if clicked(clay.ID("btn_layout")) { push_status(&app.status_msg, &app.action_log, run_layout_action(&app.controller, can_layout, &app.is_dirty)) }
|
||||||
if clicked(clay.ID("btn_export")) { push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) }
|
if clicked(clay.ID("btn_export")) { push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) }
|
||||||
if clicked(clay.ID("btn_export_now")) { push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) }
|
if clicked(clay.ID("btn_export_now")) { push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) }
|
||||||
@ -402,8 +425,8 @@ handle_action_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, ca
|
|||||||
push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .CBZ, &app.is_dirty))
|
push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .CBZ, &app.is_dirty))
|
||||||
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
||||||
}
|
}
|
||||||
if clicked(clay.ID("btn_next")) { push_status(&app.status_msg, &app.action_log, run_next_action(&app.controller, &app.export_path, app.export_format, pages_count, app.use_deepseek_script, &app.is_dirty, &app.last_export_at)) }
|
if clicked(clay.ID("btn_next")) { push_status(&app.status_msg, &app.action_log, run_next_action(&app.controller, &app.export_path, app.export_format, pages_count, &app.is_dirty, &app.last_export_at)) }
|
||||||
if clicked(clay.ID("btn_auto")) { push_status(&app.status_msg, &app.action_log, run_auto_all_action(&app.controller, &app.export_path, app.export_format, pages_count, app.use_deepseek_script, &app.is_dirty, &app.last_export_at)) }
|
if clicked(clay.ID("btn_auto")) { push_status(&app.status_msg, &app.action_log, run_auto_all_action(&app.controller, &app.export_path, app.export_format, pages_count, &app.is_dirty, &app.last_export_at)) }
|
||||||
if clicked(clay.ID("btn_save")) { push_status(&app.status_msg, &app.action_log, save_project_session_with_message(&app.project_path, app.controller.state, &app.is_dirty, &app.last_autosave_at, &app.last_save_at, "Saved project")) }
|
if clicked(clay.ID("btn_save")) { push_status(&app.status_msg, &app.action_log, save_project_session_with_message(&app.project_path, app.controller.state, &app.is_dirty, &app.last_autosave_at, &app.last_save_at, "Saved project")) }
|
||||||
if clicked(clay.ID("btn_open")) {
|
if clicked(clay.ID("btn_open")) {
|
||||||
if app.is_dirty && !shift_down { push_status(&app.status_msg, &app.action_log, request_confirmation(&app.show_confirm_overlay, &app.show_help_overlay, &app.pending_confirm, .Open_Project, "Confirm open?")) }
|
if app.is_dirty && !shift_down { push_status(&app.status_msg, &app.action_log, request_confirmation(&app.show_confirm_overlay, &app.show_help_overlay, &app.pending_confirm, .Open_Project, "Confirm open?")) }
|
||||||
@ -411,6 +434,10 @@ handle_action_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, ca
|
|||||||
}
|
}
|
||||||
if clicked(clay.ID("btn_autosave_toggle")) { push_status(&app.status_msg, &app.action_log, toggle_autosave_with_message(&app.autosave_enabled)) }
|
if clicked(clay.ID("btn_autosave_toggle")) { push_status(&app.status_msg, &app.action_log, toggle_autosave_with_message(&app.autosave_enabled)) }
|
||||||
if clicked(clay.ID("btn_help")) { toggle_help_overlay(&app.show_help_overlay) }
|
if clicked(clay.ID("btn_help")) { toggle_help_overlay(&app.show_help_overlay) }
|
||||||
|
if clicked(clay.ID("btn_fal_panels")) {
|
||||||
|
if !has_fal_key { push_status(&app.status_msg, &app.action_log, "FAL key missing (set FAL_API_KEY)") }
|
||||||
|
else { app.use_fal_panels = !app.use_fal_panels; push_status(&app.status_msg, &app.action_log, fmt.tprintf("FAL panels: %s", "ON" if app.use_fal_panels else "OFF")) }
|
||||||
|
}
|
||||||
|
|
||||||
diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first)
|
diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first)
|
||||||
if clicked(clay.ID("btn_log_reset")) { push_status(&app.status_msg, &app.action_log, reset_log_view_with_message(&app.log_show_lines, &app.log_oldest_first)) }
|
if clicked(clay.ID("btn_log_reset")) { push_status(&app.status_msg, &app.action_log, reset_log_view_with_message(&app.log_show_lines, &app.log_oldest_first)) }
|
||||||
@ -549,6 +576,36 @@ handle_detail_clicks :: proc(app: ^GUI_App_State) {
|
|||||||
app.is_dirty = true
|
app.is_dirty = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if clicked(clay.ID("field_bubble_text")) {
|
||||||
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
||||||
|
layout_val := app.controller.state.page_layouts[page_idx]
|
||||||
|
if len(layout_val.panels) > 0 {
|
||||||
|
panel_idx := clamp_bubble_cursor(len(layout_val.panels), app.summary_opts.bubble_panel_cursor)
|
||||||
|
panel_id := layout_val.panels[panel_idx].panel_id
|
||||||
|
bubble_count := count_bubbles_for_panel(app.controller.state.speech_bubbles, panel_id)
|
||||||
|
if bubble_count > 0 && app.summary_opts.bubble_edit_cursor < bubble_count {
|
||||||
|
bubbles := app.controller.state.speech_bubbles[panel_id]
|
||||||
|
app.bubble_edit_text = bubbles[app.summary_opts.bubble_edit_cursor].text
|
||||||
|
app.selected_field = 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if clicked(clay.ID("btn_bubble_save_text")) {
|
||||||
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
||||||
|
layout_val := app.controller.state.page_layouts[page_idx]
|
||||||
|
if len(layout_val.panels) > 0 && len(app.bubble_edit_text) > 0 {
|
||||||
|
panel_idx := clamp_bubble_cursor(len(layout_val.panels), app.summary_opts.bubble_panel_cursor)
|
||||||
|
panel_id := layout_val.panels[panel_idx].panel_id
|
||||||
|
if bubbles, ok := app.controller.state.speech_bubbles[panel_id]; ok {
|
||||||
|
if app.summary_opts.bubble_edit_cursor < len(bubbles) {
|
||||||
|
current_type := bubbles[app.summary_opts.bubble_edit_cursor].type
|
||||||
|
push_status(&app.status_msg, &app.action_log, action_update_bubble(&app.controller, panel_id, app.summary_opts.bubble_edit_cursor, current_type, app.bubble_edit_text))
|
||||||
|
app.is_dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.selected_field = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if layout_count > 0 {
|
if layout_count > 0 {
|
||||||
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
||||||
@ -582,11 +639,11 @@ handle_detail_clicks :: proc(app: ^GUI_App_State) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export, proj_ok, export_ok, has_deepseek: bool, pages_count: int, shift_down: bool, autosave_secs: int, compact_mode: bool) {
|
process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export, proj_ok, export_ok, has_deepseek, has_fal_key: bool, pages_count: int, shift_down: bool, autosave_secs: int, compact_mode: bool) {
|
||||||
handle_nav_clicks(app)
|
handle_nav_clicks(app)
|
||||||
handle_field_clicks(app)
|
handle_field_clicks(app)
|
||||||
handle_format_clicks(app, has_deepseek)
|
handle_format_clicks(app, has_deepseek)
|
||||||
handle_action_clicks(app, can_gen_panels, can_layout, can_export, pages_count, shift_down, proj_ok, export_ok, autosave_secs)
|
handle_action_clicks(app, can_gen_panels, can_layout, can_export, pages_count, shift_down, proj_ok, export_ok, autosave_secs, has_fal_key)
|
||||||
handle_workspace_nav(app)
|
handle_workspace_nav(app)
|
||||||
handle_detail_clicks(app)
|
handle_detail_clicks(app)
|
||||||
|
|
||||||
|
|||||||
@ -146,22 +146,34 @@ declare_story_workspace :: proc(app: ^GUI_App_State) {
|
|||||||
if clay.UI(clay.ID("PathCard"))(clay_card_style()) {
|
if clay.UI(clay.ID("PathCard"))(clay_card_style()) {
|
||||||
clay_title_text("Project Paths")
|
clay_title_text("Project Paths")
|
||||||
|
|
||||||
|
// Export Path with Browse button
|
||||||
clay_muted_text("Export Path")
|
clay_muted_text("Export Path")
|
||||||
if clay.UI(clay.ID("field_export"))({
|
if clay.UI(clay.ID("ExportPathRow"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4, childAlignment = {y = .Center}}}) {
|
||||||
layout = clay_input_layout(),
|
if clay.UI(clay.ID("field_export"))({
|
||||||
backgroundColor = CLAY_BG_INPUT,
|
layout = clay_input_layout(),
|
||||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
backgroundColor = CLAY_BG_INPUT,
|
||||||
}) {
|
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||||
clay_body_text(app.export_path)
|
}) {
|
||||||
|
display := app.export_path
|
||||||
|
if len(display) == 0 { display = "(not set)" }
|
||||||
|
clay_body_text(display)
|
||||||
|
}
|
||||||
|
declare_button_small("btn_browse_export", "Browse")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Project Path with Browse button
|
||||||
clay_muted_text("Project Path")
|
clay_muted_text("Project Path")
|
||||||
if clay.UI(clay.ID("field_project"))({
|
if clay.UI(clay.ID("ProjectPathRow"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4, childAlignment = {y = .Center}}}) {
|
||||||
layout = clay_input_layout(),
|
if clay.UI(clay.ID("field_project"))({
|
||||||
backgroundColor = CLAY_BG_INPUT,
|
layout = clay_input_layout(),
|
||||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
backgroundColor = CLAY_BG_INPUT,
|
||||||
}) {
|
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||||
clay_body_text(app.project_path)
|
}) {
|
||||||
|
display := app.project_path
|
||||||
|
if len(display) == 0 { display = "(not set)" }
|
||||||
|
clay_body_text(display)
|
||||||
|
}
|
||||||
|
declare_button_small("btn_browse_project", "Browse")
|
||||||
}
|
}
|
||||||
|
|
||||||
if clay.UI(clay.ID("StoryConfigRow"))({layout = clay_row_layout()}) {
|
if clay.UI(clay.ID("StoryConfigRow"))({layout = clay_row_layout()}) {
|
||||||
@ -177,9 +189,6 @@ declare_story_workspace :: proc(app: ^GUI_App_State) {
|
|||||||
declare_nav_chip("btn_pdf", "PDF", app.export_format == .PDF)
|
declare_nav_chip("btn_pdf", "PDF", app.export_format == .PDF)
|
||||||
declare_nav_chip("btn_png", "PNG", app.export_format == .PNG)
|
declare_nav_chip("btn_png", "PNG", app.export_format == .PNG)
|
||||||
declare_nav_chip("btn_cbz", "CBZ", app.export_format == .CBZ)
|
declare_nav_chip("btn_cbz", "CBZ", app.export_format == .CBZ)
|
||||||
clay_muted_text("Source")
|
|
||||||
declare_nav_chip("btn_local", "Local", !app.use_deepseek_script)
|
|
||||||
declare_nav_chip("btn_deepseek", "DS", app.use_deepseek_script)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,7 +200,10 @@ declare_characters_workspace :: proc(app: ^GUI_App_State) {
|
|||||||
if clay.UI(clay.ID("CharsCard"))(clay_card_style()) {
|
if clay.UI(clay.ID("CharsCard"))(clay_card_style()) {
|
||||||
clay_title_text("Characters")
|
clay_title_text("Characters")
|
||||||
char_count := len(app.controller.state.characters)
|
char_count := len(app.controller.state.characters)
|
||||||
|
script_char_count := len(app.controller.state.script.characters)
|
||||||
|
// Show both state.characters and script.characters for debugging
|
||||||
if char_count == 0 {
|
if char_count == 0 {
|
||||||
|
clay_body_text(fmt.tprintf("State chars: %d, Script chars: %d", char_count, script_char_count), color = CLAY_TEXT_PRIMARY)
|
||||||
clay_body_text("No characters yet.", color = CLAY_TEXT_TERTIARY)
|
clay_body_text("No characters yet.", color = CLAY_TEXT_TERTIARY)
|
||||||
clay_body_text("Characters are extracted when you generate a script.", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
clay_body_text("Characters are extracted when you generate a script.", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||||
clay_body_text("Generate Script (F5) to populate characters from your story.", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
clay_body_text("Generate Script (F5) to populate characters from your story.", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||||
@ -237,9 +249,9 @@ declare_export_workspace :: proc(app: ^GUI_App_State) {
|
|||||||
page_count := len(app.controller.state.page_layouts)
|
page_count := len(app.controller.state.page_layouts)
|
||||||
|
|
||||||
if clay.UI(clay.ID("ExportStats"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 16}}) {
|
if clay.UI(clay.ID("ExportStats"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 16}}) {
|
||||||
declare_stat_chip("Pages", page_count)
|
declare_stat_chip("exp_pages", "Pages", page_count)
|
||||||
declare_stat_chip("Panels", panel_count)
|
declare_stat_chip("exp_panels", "Panels", panel_count)
|
||||||
declare_stat_chip("Ready", ready)
|
declare_stat_chip("exp_ready", "Ready", ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
clay_muted_text(fmt.tprintf("Format: %v • Target: %s", app.controller.state.export_format, app.export_path))
|
clay_muted_text(fmt.tprintf("Format: %v • Target: %s", app.controller.state.export_format, app.export_path))
|
||||||
|
|||||||
143
odin/src/osdialog/osdialog.odin
Normal file
143
odin/src/osdialog/osdialog.odin
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package osdialog
|
||||||
|
|
||||||
|
import "core:c"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
// ─── Enums ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Message_Level :: enum c.int {
|
||||||
|
INFO = 0,
|
||||||
|
WARNING = 1,
|
||||||
|
ERROR = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
Message_Buttons :: enum c.int {
|
||||||
|
OK = 0,
|
||||||
|
OK_CANCEL = 1,
|
||||||
|
YES_NO = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
File_Action :: enum c.int {
|
||||||
|
OPEN = 0,
|
||||||
|
OPEN_DIR = 1,
|
||||||
|
SAVE = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
Color :: struct {
|
||||||
|
r, g, b, a: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Filter Types ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Filter_Patterns :: struct {
|
||||||
|
pattern: cstring,
|
||||||
|
next: ^Filter_Patterns,
|
||||||
|
}
|
||||||
|
|
||||||
|
Filters :: struct {
|
||||||
|
name: cstring,
|
||||||
|
patterns: ^Filter_Patterns,
|
||||||
|
next: ^Filters,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Foreign Imports ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
foreign import osdialog_lib "system:osdialog"
|
||||||
|
foreign import libc "system:c"
|
||||||
|
|
||||||
|
@(default_calling_convention = "c")
|
||||||
|
foreign osdialog_lib {
|
||||||
|
osdialog_message :: proc(level: Message_Level, buttons: Message_Buttons, message: cstring) -> c.int ---
|
||||||
|
osdialog_prompt :: proc(level: Message_Level, message: cstring, text: cstring) -> cstring ---
|
||||||
|
osdialog_file :: proc(action: File_Action, dir: cstring, filename: cstring, filters: ^Filters) -> cstring ---
|
||||||
|
osdialog_filters_parse :: proc(str: cstring) -> ^Filters ---
|
||||||
|
osdialog_filters_free :: proc(filters: ^Filters) ---
|
||||||
|
osdialog_color_picker :: proc(color: ^Color, opacity: c.int) -> c.int ---
|
||||||
|
osdialog_strdup :: proc(s: cstring) -> cstring ---
|
||||||
|
osdialog_strndup :: proc(s: cstring, n: c.size_t) -> cstring ---
|
||||||
|
}
|
||||||
|
|
||||||
|
@(default_calling_convention = "c")
|
||||||
|
foreign libc {
|
||||||
|
free :: proc(ptr: rawptr) ---
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Convenience Wrappers ─────────────────────────────────────────
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
alloc_cstr :: proc(s: string) -> cstring {
|
||||||
|
if len(s) == 0 { return nil }
|
||||||
|
return strings.clone_to_cstring(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
free_cstr :: proc(cs: cstring) {
|
||||||
|
if cs != nil { delete(cs) }
|
||||||
|
}
|
||||||
|
|
||||||
|
open_file_dialog :: proc(default_dir: string = "", filters_str: string = "") -> string {
|
||||||
|
dir_c := alloc_cstr(default_dir)
|
||||||
|
defer free_cstr(dir_c)
|
||||||
|
filter_c := alloc_cstr(filters_str)
|
||||||
|
defer free_cstr(filter_c)
|
||||||
|
filters: ^Filters
|
||||||
|
if filter_c != nil {
|
||||||
|
filters = osdialog_filters_parse(filter_c)
|
||||||
|
defer osdialog_filters_free(filters)
|
||||||
|
}
|
||||||
|
result := osdialog_file(.OPEN, dir_c, nil, filters)
|
||||||
|
if result == nil { return "" }
|
||||||
|
defer free(rawptr(result))
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
save_file_dialog :: proc(default_dir: string = "", default_name: string = "", filters_str: string = "") -> string {
|
||||||
|
dir_c := alloc_cstr(default_dir)
|
||||||
|
defer free_cstr(dir_c)
|
||||||
|
name_c := alloc_cstr(default_name)
|
||||||
|
defer free_cstr(name_c)
|
||||||
|
filter_c := alloc_cstr(filters_str)
|
||||||
|
defer free_cstr(filter_c)
|
||||||
|
filters: ^Filters
|
||||||
|
if filter_c != nil {
|
||||||
|
filters = osdialog_filters_parse(filter_c)
|
||||||
|
defer osdialog_filters_free(filters)
|
||||||
|
}
|
||||||
|
result := osdialog_file(.SAVE, dir_c, name_c, filters)
|
||||||
|
if result == nil { return "" }
|
||||||
|
defer free(rawptr(result))
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
open_folder_dialog :: proc(default_dir: string = "") -> string {
|
||||||
|
dir_c := alloc_cstr(default_dir)
|
||||||
|
defer free_cstr(dir_c)
|
||||||
|
result := osdialog_file(.OPEN_DIR, dir_c, nil, nil)
|
||||||
|
if result == nil { return "" }
|
||||||
|
defer free(rawptr(result))
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
show_message :: proc(level: Message_Level, buttons: Message_Buttons, message: string) -> bool {
|
||||||
|
msg_c := alloc_cstr(message)
|
||||||
|
defer free_cstr(msg_c)
|
||||||
|
result := osdialog_message(level, buttons, msg_c)
|
||||||
|
return result == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
show_prompt :: proc(level: Message_Level, message: string, default_text: string = "") -> string {
|
||||||
|
msg_c := alloc_cstr(message)
|
||||||
|
defer free_cstr(msg_c)
|
||||||
|
text_c := alloc_cstr(default_text)
|
||||||
|
defer free_cstr(text_c)
|
||||||
|
result := osdialog_prompt(level, msg_c, text_c)
|
||||||
|
if result == nil { return "" }
|
||||||
|
defer free(rawptr(result))
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_color :: proc(r, g, b: u8, a: u8 = 255) -> (Color, bool) {
|
||||||
|
color := Color{r, g, b, a}
|
||||||
|
ok := osdialog_color_picker(&color, 1)
|
||||||
|
return color, ok == 1
|
||||||
|
}
|
||||||
@ -122,30 +122,8 @@ integration_local_full_pipeline :: proc(t: ^testing.T) {
|
|||||||
controller.state.story_genre = "action"
|
controller.state.story_genre = "action"
|
||||||
controller.state.art_style = "manga"
|
controller.state.art_style = "manga"
|
||||||
|
|
||||||
// Step 1: Generate local script
|
// Skipped: requires DeepSeek + FAL API keys (local generation removed)
|
||||||
msg1 := gui.action_generate_local_script(&controller, 2)
|
_ = t
|
||||||
testing.expect(t, strings.contains(msg1, "Generated"), fmt.tprintf("should generate script, got %q", msg1))
|
|
||||||
testing.expect(t, len(controller.state.script.pages) == 2, "should have 2 pages")
|
|
||||||
|
|
||||||
// Step 2: Generate local panels
|
|
||||||
msg2 := gui.action_generate_local_panels(&controller)
|
|
||||||
testing.expect(t, strings.contains(msg2, "Generated"), fmt.tprintf("should generate panels, got %q", msg2))
|
|
||||||
testing.expect(t, len(controller.state.panel_images) > 0, "should have panel images")
|
|
||||||
|
|
||||||
// Step 3: Auto layout
|
|
||||||
msg3 := gui.action_layout_auto(&controller)
|
|
||||||
testing.expect(t, strings.contains(msg3, "layout"), fmt.tprintf("should create layout, got %q", msg3))
|
|
||||||
testing.expect(t, len(controller.state.page_layouts) > 0, "should have page layouts")
|
|
||||||
|
|
||||||
// Step 4: Auto-place bubbles for first panel
|
|
||||||
if len(controller.state.page_layouts) > 0 {
|
|
||||||
layout := controller.state.page_layouts[0]
|
|
||||||
if len(layout.panels) > 0 {
|
|
||||||
panel_id := layout.panels[0].panel_id
|
|
||||||
msg4 := gui.action_auto_place_bubbles_for_panel(&controller, panel_id, layout)
|
|
||||||
testing.expect(t, strings.contains(msg4, "Auto-placed") || strings.contains(msg4, "No dialogue"), fmt.tprintf("should auto-place bubbles, got %q", msg4))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
|
|||||||
1
odin/vendor/osdialog
vendored
Submodule
1
odin/vendor/osdialog
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 64bc87ff445a09a4ebf5ac2ef69db1b1abb09089
|
||||||
Loading…
Reference in New Issue
Block a user