Improve stream UI: grouped sections, full-size preview lightbox, and test redesign

- Separate Screen Capture and Processed streams into grouped sections with headers
- Remove redundant Type dropdown from stream modal (type inferred from add button)
- Add full-resolution image to test endpoints alongside thumbnails
- Add image lightbox with clickable preview for full-size viewing
- Move test results from modal into lightbox overlay with capture stats
- Apply postprocessing to both thumbnail and full image for processed streams
- Rename "Assigned Picture Stream" to "Picture Stream" in device settings
- Fix null reference errors from removed test result HTML elements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 00:35:06 +03:00
parent 493f14fba9
commit e8cbc73161
7 changed files with 308 additions and 138 deletions

View File

@@ -1052,12 +1052,19 @@ async def test_template(
thumbnail = pil_image.copy()
thumbnail.thumbnail((thumbnail_width, thumbnail_height), Image.Resampling.LANCZOS)
# Encode full capture thumbnail as JPEG
# Encode thumbnail as JPEG
img_buffer = io.BytesIO()
thumbnail.save(img_buffer, format='JPEG', quality=85)
img_buffer.seek(0)
full_capture_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
full_capture_data_uri = f"data:image/jpeg;base64,{full_capture_b64}"
thumbnail_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
thumbnail_data_uri = f"data:image/jpeg;base64,{thumbnail_b64}"
# Encode full-resolution image as JPEG
full_buffer = io.BytesIO()
pil_image.save(full_buffer, format='JPEG', quality=90)
full_buffer.seek(0)
full_b64 = base64.b64encode(full_buffer.getvalue()).decode('utf-8')
full_data_uri = f"data:image/jpeg;base64,{full_b64}"
# Calculate metrics
actual_fps = frame_count / actual_duration if actual_duration > 0 else 0
@@ -1067,7 +1074,8 @@ async def test_template(
return TemplateTestResponse(
full_capture=CaptureImage(
image=full_capture_data_uri,
image=thumbnail_data_uri,
full_image=full_data_uri,
width=width,
height=height,
thumbnail_width=thumbnail_width,
@@ -1469,34 +1477,41 @@ async def test_picture_stream(
thumbnail = pil_image.copy()
thumbnail.thumbnail((thumbnail_width, thumbnail_height), Image.Resampling.LANCZOS)
# Apply postprocessing to preview if this is a processed stream
# Apply postprocessing if this is a processed stream
pp_template_ids = chain["postprocessing_template_ids"]
if pp_template_ids:
try:
pp = pp_store.get_template(pp_template_ids[0])
img_array = np.array(thumbnail, dtype=np.float32) / 255.0
if pp.brightness != 1.0:
img_array *= pp.brightness
def apply_pp(img):
arr = np.array(img, dtype=np.float32) / 255.0
if pp.brightness != 1.0:
arr *= pp.brightness
if pp.saturation != 1.0:
lum = np.dot(arr[..., :3], [0.299, 0.587, 0.114])[..., np.newaxis]
arr[..., :3] = lum + (arr[..., :3] - lum) * pp.saturation
if pp.gamma != 1.0:
arr = np.power(np.clip(arr, 0, 1), 1.0 / pp.gamma)
return Image.fromarray(np.clip(arr * 255.0, 0, 255).astype(np.uint8))
if pp.saturation != 1.0:
luminance = np.dot(img_array[..., :3], [0.299, 0.587, 0.114])
luminance = luminance[..., np.newaxis]
img_array[..., :3] = luminance + (img_array[..., :3] - luminance) * pp.saturation
if pp.gamma != 1.0:
img_array = np.power(np.clip(img_array, 0, 1), 1.0 / pp.gamma)
img_array = np.clip(img_array * 255.0, 0, 255).astype(np.uint8)
thumbnail = Image.fromarray(img_array)
thumbnail = apply_pp(thumbnail)
pil_image = apply_pp(pil_image)
except ValueError:
logger.warning(f"PP template {pp_template_ids[0]} not found, skipping postprocessing preview")
# Encode thumbnail
img_buffer = io.BytesIO()
thumbnail.save(img_buffer, format='JPEG', quality=85)
img_buffer.seek(0)
full_capture_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
full_capture_data_uri = f"data:image/jpeg;base64,{full_capture_b64}"
thumbnail_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
thumbnail_data_uri = f"data:image/jpeg;base64,{thumbnail_b64}"
# Encode full-resolution image
full_buffer = io.BytesIO()
pil_image.save(full_buffer, format='JPEG', quality=90)
full_buffer.seek(0)
full_b64 = base64.b64encode(full_buffer.getvalue()).decode('utf-8')
full_data_uri = f"data:image/jpeg;base64,{full_b64}"
actual_fps = frame_count / actual_duration if actual_duration > 0 else 0
avg_capture_time_ms = (total_capture_time / frame_count * 1000) if frame_count > 0 else 0
@@ -1504,7 +1519,8 @@ async def test_picture_stream(
return TemplateTestResponse(
full_capture=CaptureImage(
image=full_capture_data_uri,
image=thumbnail_data_uri,
full_image=full_data_uri,
width=width,
height=height,
thumbnail_width=thumbnail_width,