优化模板选择的交互逻辑, 支持直接预览选择

This commit is contained in:
puke
2025-12-08 15:39:46 +08:00
parent f111ffc427
commit fb98254549
59 changed files with 234 additions and 112 deletions

View File

@@ -228,6 +228,46 @@ def render_style_config(pixelle_video):
# ====================================================================
# Storyboard Template Section
# ====================================================================
def get_template_preview_path(template_path: str, language: str = "zh_CN") -> str:
"""
Get the preview image path for a template based on language.
Args:
template_path: Template path like "1080x1920/image_default.html"
language: Language code, either "zh_CN" or "en"
Returns:
Path to preview image in docs/images/
"""
# Extract size and template name from path
# e.g., "1080x1920/image_default.html" -> size="1080x1920", name="image_default"
path_parts = template_path.split('/')
if len(path_parts) >= 2:
size = path_parts[0] # e.g., "1080x1920"
template_file = path_parts[1] # e.g., "image_default.html"
template_name = template_file.replace('.html', '') # e.g., "image_default"
# Build preview image path
# Format: docs/images/{size}/{template_name}.jpg or {template_name}_en.jpg
# Chinese uses Chinese preview, all other languages use English preview for better i18n
suffix = "" if language == "zh_CN" else "_en"
# Try different image extensions
for ext in ['.jpg', '.png']:
preview_path = f"docs/images/{size}/{template_name}{suffix}{ext}"
if os.path.exists(preview_path):
return preview_path
# Fallback: try without language suffix (for templates with only one version)
for ext in ['.jpg', '.png']:
preview_path = f"docs/images/{size}/{template_name}{ext}"
if os.path.exists(preview_path):
return preview_path
# If no preview found, return empty string
return ""
with st.container(border=True):
st.markdown(f"**{tr('section.template')}**")
@@ -284,18 +324,13 @@ def render_style_config(pixelle_video):
st.warning(f"No {template_type_options[selected_template_type]} templates found. Please select a different type or add templates.")
st.stop()
# Build display options with group separators
# Build orientation i18n mapping
ORIENTATION_I18N = {
'portrait': tr('orientation.portrait'),
'landscape': tr('orientation.landscape'),
'square': tr('orientation.square')
}
display_options = []
template_paths_ordered = [] # Use ordered list instead of dict to avoid key conflicts
default_index = 0
current_index = 0
# Get default template from config
template_config = pixelle_video.config.get("template", {})
config_default_template = template_config.get("default_template", "1080x1920/image_default.html")
@@ -312,68 +347,145 @@ def render_style_config(pixelle_video):
}
type_specific_default = type_default_templates.get(selected_template_type, config_default_template)
# Initialize selected template in session state if not exists
if 'selected_template' not in st.session_state:
st.session_state['selected_template'] = type_specific_default
# Collect size groups and prepare tabs
size_groups = []
size_labels = []
for size, templates in grouped_templates.items():
if not templates:
continue
# Filter templates to only include those with proper naming convention
# Only show templates starting with static_, image_, or video_
valid_templates = []
for template in templates:
template_name = template.display_info.name
if template_name.startswith(('static_', 'image_', 'video_')):
valid_templates.append(template)
# Skip if no valid templates after filtering
if not valid_templates:
continue
# Separate templates into two groups: with preview and without preview
templates_with_preview = []
templates_without_preview = []
for template in valid_templates:
preview_path = get_template_preview_path(template.template_path, current_lang)
if preview_path and os.path.exists(preview_path):
templates_with_preview.append(template)
else:
templates_without_preview.append(template)
# Skip this group if no templates at all
if not templates_with_preview and not templates_without_preview:
continue
# Combine: templates with preview first, then without preview
all_templates = templates_with_preview + templates_without_preview
# Get orientation from first template in group
orientation = ORIENTATION_I18N.get(
templates[0].display_info.orientation,
templates[0].display_info.orientation
all_templates[0].display_info.orientation,
all_templates[0].display_info.orientation
)
width = templates[0].display_info.width
height = templates[0].display_info.height
width = all_templates[0].display_info.width
height = all_templates[0].display_info.height
# Add group separator
separator = f"─── {orientation} {width}×{height} ───"
display_options.append(separator)
template_paths_ordered.append(None) # Separator has no template path
current_index += 1
# Create tab label
tab_label = f"{orientation} {width}×{height}"
size_labels.append(tab_label)
size_groups.append(all_templates)
# Create tabs for each size group (wrapped in expander)
with st.expander(tr("template.gallery_view"), expanded=True):
if size_groups:
tabs = st.tabs(size_labels)
for tab, all_templates in zip(tabs, size_groups):
with tab:
# Create grid layout (5 columns)
num_cols = 5
cols = st.columns(num_cols)
for idx, template in enumerate(all_templates):
col_idx = idx % num_cols
with cols[col_idx]:
# Get preview image path
preview_path = get_template_preview_path(template.template_path, current_lang)
# Display preview image or placeholder
if preview_path and os.path.exists(preview_path):
st.image(preview_path, use_container_width=True)
else:
# Placeholder for templates without preview (fixed height, compact layout)
st.markdown(
f"""
<div style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 150px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
border-radius: 8px;
color: white;
margin-bottom: 15px;
padding: 10px;
">
<div style="
font-size: 14px;
opacity: 0.95;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
word-break: break-all;
">{template.display_info.name}</div>
</div>
""",
unsafe_allow_html=True
)
# Select button (unified label)
is_selected = (st.session_state['selected_template'] == template.template_path)
button_label = f"{tr('template.selected')}" if is_selected else tr('template.select_button')
button_type = "primary" if is_selected else "secondary"
if st.button(
button_label,
key=f"template_{template.template_path}",
use_container_width=True,
type=button_type,
):
st.session_state['selected_template'] = template.template_path
st.rerun()
else:
st.warning(tr("template.no_templates_with_preview"))
# Add templates in this group
for t in templates:
display_name = f" {t.display_info.name}"
display_options.append(display_name)
template_paths_ordered.append(t.template_path) # Add to ordered list
# Set default: priority is config > type-specific default > first in portrait
if t.template_path == config_default_template:
default_index = current_index
elif default_index == 0 and t.template_path == type_specific_default:
default_index = current_index
elif default_index == 0 and t.display_info.orientation == 'portrait':
default_index = current_index
current_index += 1
# Dropdown with grouped display
# Create unique display strings by appending hidden unique identifier
# This ensures Streamlit doesn't confuse templates with same name in different groups
unique_display_options = []
for i, option in enumerate(display_options):
# Add zero-width space characters as unique identifier (invisible to users)
unique_option = option + ("\u200B" * i) # \u200B is zero-width space
unique_display_options.append(unique_option)
selected_unique_option = st.selectbox(
tr("template.select"),
unique_display_options,
index=default_index,
label_visibility="collapsed",
help=tr("template.select_help")
)
# Get index from selected unique option
selected_index = unique_display_options.index(selected_unique_option)
# Check if separator is selected (shouldn't happen, but handle it)
if display_options[selected_index].startswith("───"):
st.warning(tr("template.separator_selected"))
st.stop()
# Get full template path directly by index
frame_template = template_paths_ordered[selected_index]
# Display selected template name (inside expander, below tabs)
frame_template = st.session_state['selected_template']
# Find the selected template's display name
selected_template_name = None
for size, templates in grouped_templates.items():
for template in templates:
if template.template_path == frame_template:
selected_template_name = template.display_info.name
break
if selected_template_name:
break
if selected_template_name:
st.info(f"📋 {tr('template.selected_template')}: **{selected_template_name}**")
# Display video size from template
from pixelle_video.utils.template_util import parse_template_size
video_width, video_height = parse_template_size(frame_template)