Files
replicate.maui/Replicate.Maui/Controls/ReplicateTransformerView.xaml.cs
2025-12-10 21:59:12 -05:00

2449 lines
91 KiB
C#

using CommunityToolkit.Maui.Views;
using MarketAlly.Replicate.Maui;
using MarketAlly.Replicate.Maui.Localization;
namespace MarketAlly.Replicate.Maui.Controls;
/// <summary>
/// Layout mode for the transformer view.
/// </summary>
public enum TransformerLayoutMode
{
/// <summary>
/// Image only, no buttons shown. Use for custom button implementations.
/// </summary>
ImageOnly,
/// <summary>
/// Buttons displayed in rows below the image (default).
/// </summary>
ButtonsBelow,
/// <summary>
/// Two flush buttons overlaid at the bottom of the image (50% width each).
/// </summary>
ButtonsOverlay,
/// <summary>
/// Source image on left, result image on right, with buttons below.
/// </summary>
SideBySide
}
public partial class ReplicateTransformerView : ContentView, IDisposable
{
private byte[]? _currentImageBytes;
private IReplicateTransformer? _transformer;
private IReplicateTransformerFactory? _transformerFactory;
private CancellationTokenSource? _currentTransformationCts;
private string? _currentPredictionId;
private IPredictionTracker? _tracker;
private bool _disposed;
private bool _trackPredictions = true;
private ModelPreset? _imagePreset;
private ModelPreset? _videoPreset;
private Dictionary<string, object>? _customImageParameters;
private Dictionary<string, object>? _customVideoParameters;
private TransformerLayoutMode _layoutMode = TransformerLayoutMode.ButtonsBelow;
private bool _isShowingVideo;
private string? _videoUrl;
private TransformationType _lastTransformationType = TransformationType.Image;
private string? _lastSavedImagePath;
private string? _lastSavedVideoPath;
private TransformerState _currentState = TransformerState.Empty;
private Services.IFileSaveService? _fileSaveService;
#region Bindable Properties
public static readonly BindableProperty LayoutModeProperty =
BindableProperty.Create(nameof(LayoutMode), typeof(TransformerLayoutMode), typeof(ReplicateTransformerView),
TransformerLayoutMode.ButtonsBelow, propertyChanged: OnLayoutModeChanged);
public static readonly BindableProperty ShowCaptureButtonsProperty =
BindableProperty.Create(nameof(ShowCaptureButtons), typeof(bool), typeof(ReplicateTransformerView), true,
propertyChanged: OnShowCaptureButtonsChanged);
public static readonly BindableProperty ShowVideoOptionsProperty =
BindableProperty.Create(nameof(ShowVideoOptions), typeof(bool), typeof(ReplicateTransformerView), true,
propertyChanged: OnShowVideoOptionsChanged);
public static readonly BindableProperty AnimeButtonTextProperty =
BindableProperty.Create(nameof(AnimeButtonText), typeof(string), typeof(ReplicateTransformerView), "Transform to Anime",
propertyChanged: OnAnimeButtonTextChanged);
public static readonly BindableProperty VideoButtonTextProperty =
BindableProperty.Create(nameof(VideoButtonText), typeof(string), typeof(ReplicateTransformerView), "Generate Video",
propertyChanged: OnVideoButtonTextChanged);
public static readonly BindableProperty PlaceholderTextProperty =
BindableProperty.Create(nameof(PlaceholderText), typeof(string), typeof(ReplicateTransformerView), "Select or capture an image",
propertyChanged: OnPlaceholderTextChanged);
public static readonly BindableProperty OverlaySelectButtonTextProperty =
BindableProperty.Create(nameof(OverlaySelectButtonText), typeof(string), typeof(ReplicateTransformerView), "🖼 Select",
propertyChanged: OnOverlaySelectButtonTextChanged);
public static readonly BindableProperty OverlayTransformButtonTextProperty =
BindableProperty.Create(nameof(OverlayTransformButtonText), typeof(string), typeof(ReplicateTransformerView), "✨ Transform",
propertyChanged: OnOverlayTransformButtonTextChanged);
public static readonly BindableProperty CustomImagePromptProperty =
BindableProperty.Create(nameof(CustomImagePrompt), typeof(string), typeof(ReplicateTransformerView), null);
public static readonly BindableProperty CustomVideoPromptProperty =
BindableProperty.Create(nameof(CustomVideoPrompt), typeof(string), typeof(ReplicateTransformerView), null);
public static readonly BindableProperty IsProcessingProperty =
BindableProperty.Create(nameof(IsProcessing), typeof(bool), typeof(ReplicateTransformerView), false);
public static readonly BindableProperty TrackPredictionsProperty =
BindableProperty.Create(nameof(TrackPredictions), typeof(bool), typeof(ReplicateTransformerView), true,
propertyChanged: OnTrackPredictionsChanged);
public static readonly BindableProperty UseLocalizationProperty =
BindableProperty.Create(nameof(UseLocalization), typeof(bool), typeof(ReplicateTransformerView), false,
propertyChanged: OnUseLocalizationChanged);
public static readonly BindableProperty ResultUrlProperty =
BindableProperty.Create(nameof(ResultUrl), typeof(string), typeof(ReplicateTransformerView), null);
public static readonly BindableProperty SourceImageProperty =
BindableProperty.Create(nameof(SourceImageSource), typeof(ImageSource), typeof(ReplicateTransformerView), null);
public static readonly BindableProperty OverlayTransformTypeProperty =
BindableProperty.Create(nameof(OverlayTransformType), typeof(TransformationType), typeof(ReplicateTransformerView), TransformationType.Image);
public static readonly BindableProperty ShowSideBySideSourceButtonsProperty =
BindableProperty.Create(nameof(ShowSideBySideSourceButtons), typeof(bool), typeof(ReplicateTransformerView), true,
propertyChanged: OnShowSideBySideSourceButtonsChanged);
public static readonly BindableProperty ShowSideBySideResultButtonsProperty =
BindableProperty.Create(nameof(ShowSideBySideResultButtons), typeof(bool), typeof(ReplicateTransformerView), true,
propertyChanged: OnShowSideBySideResultButtonsChanged);
public static readonly BindableProperty SideBySideCaptureButtonTextProperty =
BindableProperty.Create(nameof(SideBySideCaptureButtonText), typeof(string), typeof(ReplicateTransformerView), "📸 Take",
propertyChanged: OnSideBySideCaptureButtonTextChanged);
public static readonly BindableProperty SideBySidePickButtonTextProperty =
BindableProperty.Create(nameof(SideBySidePickButtonText), typeof(string), typeof(ReplicateTransformerView), "🖼 Pick",
propertyChanged: OnSideBySidePickButtonTextChanged);
public static readonly BindableProperty SideBySideClearButtonTextProperty =
BindableProperty.Create(nameof(SideBySideClearButtonText), typeof(string), typeof(ReplicateTransformerView), "🗑 Clear",
propertyChanged: OnSideBySideClearButtonTextChanged);
public static readonly BindableProperty SideBySideRedoButtonTextProperty =
BindableProperty.Create(nameof(SideBySideRedoButtonText), typeof(string), typeof(ReplicateTransformerView), "↺ Redo",
propertyChanged: OnSideBySideRedoButtonTextChanged);
// ============================================
// CONSOLIDATED BUTTON CONFIGURATION
// ============================================
/// <summary>
/// Consolidated button configurations for all transformer buttons.
/// Provides a cleaner API for customizing button appearance.
/// </summary>
public static readonly BindableProperty ButtonConfigsProperty =
BindableProperty.Create(nameof(ButtonConfigs), typeof(TransformerButtonConfigs), typeof(ReplicateTransformerView),
null, propertyChanged: OnButtonConfigsChanged);
// ============================================
// BUTTON DISPLAY MODE AND ICON PROPERTIES (Legacy - use ButtonConfigs instead)
// ============================================
// Global display mode (affects all buttons unless overridden)
public static readonly BindableProperty DefaultButtonDisplayModeProperty =
BindableProperty.Create(nameof(DefaultButtonDisplayMode), typeof(ButtonDisplayMode), typeof(ReplicateTransformerView),
ButtonDisplayMode.Both, propertyChanged: OnButtonDisplayModeChanged);
// Overlay buttons - Select
public static readonly BindableProperty OverlaySelectIconTextProperty =
BindableProperty.Create(nameof(OverlaySelectIconText), typeof(string), typeof(ReplicateTransformerView), "🖼",
propertyChanged: OnOverlaySelectAppearanceChanged);
public static readonly BindableProperty OverlaySelectIconSourceProperty =
BindableProperty.Create(nameof(OverlaySelectIconSource), typeof(ImageSource), typeof(ReplicateTransformerView), null,
propertyChanged: OnOverlaySelectAppearanceChanged);
public static readonly BindableProperty OverlaySelectLabelProperty =
BindableProperty.Create(nameof(OverlaySelectLabel), typeof(string), typeof(ReplicateTransformerView), "Select",
propertyChanged: OnOverlaySelectAppearanceChanged);
public static readonly BindableProperty OverlaySelectDisplayModeProperty =
BindableProperty.Create(nameof(OverlaySelectDisplayMode), typeof(ButtonDisplayMode?), typeof(ReplicateTransformerView), null,
propertyChanged: OnOverlaySelectAppearanceChanged);
// Overlay buttons - Transform
public static readonly BindableProperty OverlayTransformIconTextProperty =
BindableProperty.Create(nameof(OverlayTransformIconText), typeof(string), typeof(ReplicateTransformerView), "✨",
propertyChanged: OnOverlayTransformAppearanceChanged);
public static readonly BindableProperty OverlayTransformIconSourceProperty =
BindableProperty.Create(nameof(OverlayTransformIconSource), typeof(ImageSource), typeof(ReplicateTransformerView), null,
propertyChanged: OnOverlayTransformAppearanceChanged);
public static readonly BindableProperty OverlayTransformLabelProperty =
BindableProperty.Create(nameof(OverlayTransformLabel), typeof(string), typeof(ReplicateTransformerView), "Transform",
propertyChanged: OnOverlayTransformAppearanceChanged);
public static readonly BindableProperty OverlayTransformDisplayModeProperty =
BindableProperty.Create(nameof(OverlayTransformDisplayMode), typeof(ButtonDisplayMode?), typeof(ReplicateTransformerView), null,
propertyChanged: OnOverlayTransformAppearanceChanged);
// Overlay button position
public static readonly BindableProperty OverlayButtonPositionProperty =
BindableProperty.Create(nameof(OverlayButtonPosition), typeof(OverlayButtonPosition), typeof(ReplicateTransformerView),
OverlayButtonPosition.Bottom, propertyChanged: OnOverlayButtonPositionChanged);
// SideBySide - Capture button
public static readonly BindableProperty SideBySideCaptureIconTextProperty =
BindableProperty.Create(nameof(SideBySideCaptureIconText), typeof(string), typeof(ReplicateTransformerView), "📸",
propertyChanged: OnSideBySideCaptureAppearanceChanged);
public static readonly BindableProperty SideBySideCaptureIconSourceProperty =
BindableProperty.Create(nameof(SideBySideCaptureIconSource), typeof(ImageSource), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySideCaptureAppearanceChanged);
public static readonly BindableProperty SideBySideCaptureLabelProperty =
BindableProperty.Create(nameof(SideBySideCaptureLabel), typeof(string), typeof(ReplicateTransformerView), "Take",
propertyChanged: OnSideBySideCaptureAppearanceChanged);
public static readonly BindableProperty SideBySideCaptureDisplayModeProperty =
BindableProperty.Create(nameof(SideBySideCaptureDisplayMode), typeof(ButtonDisplayMode?), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySideCaptureAppearanceChanged);
// SideBySide - Pick button
public static readonly BindableProperty SideBySidePickIconTextProperty =
BindableProperty.Create(nameof(SideBySidePickIconText), typeof(string), typeof(ReplicateTransformerView), "🖼",
propertyChanged: OnSideBySidePickAppearanceChanged);
public static readonly BindableProperty SideBySidePickIconSourceProperty =
BindableProperty.Create(nameof(SideBySidePickIconSource), typeof(ImageSource), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySidePickAppearanceChanged);
public static readonly BindableProperty SideBySidePickLabelProperty =
BindableProperty.Create(nameof(SideBySidePickLabel), typeof(string), typeof(ReplicateTransformerView), "Pick",
propertyChanged: OnSideBySidePickAppearanceChanged);
public static readonly BindableProperty SideBySidePickDisplayModeProperty =
BindableProperty.Create(nameof(SideBySidePickDisplayMode), typeof(ButtonDisplayMode?), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySidePickAppearanceChanged);
// SideBySide - Clear button
public static readonly BindableProperty SideBySideClearIconTextProperty =
BindableProperty.Create(nameof(SideBySideClearIconText), typeof(string), typeof(ReplicateTransformerView), "🗑",
propertyChanged: OnSideBySideClearAppearanceChanged);
public static readonly BindableProperty SideBySideClearIconSourceProperty =
BindableProperty.Create(nameof(SideBySideClearIconSource), typeof(ImageSource), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySideClearAppearanceChanged);
public static readonly BindableProperty SideBySideClearLabelProperty =
BindableProperty.Create(nameof(SideBySideClearLabel), typeof(string), typeof(ReplicateTransformerView), "Clear",
propertyChanged: OnSideBySideClearAppearanceChanged);
public static readonly BindableProperty SideBySideClearDisplayModeProperty =
BindableProperty.Create(nameof(SideBySideClearDisplayMode), typeof(ButtonDisplayMode?), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySideClearAppearanceChanged);
// SideBySide - Redo button
public static readonly BindableProperty SideBySideRedoIconTextProperty =
BindableProperty.Create(nameof(SideBySideRedoIconText), typeof(string), typeof(ReplicateTransformerView), "↺",
propertyChanged: OnSideBySideRedoAppearanceChanged);
public static readonly BindableProperty SideBySideRedoIconSourceProperty =
BindableProperty.Create(nameof(SideBySideRedoIconSource), typeof(ImageSource), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySideRedoAppearanceChanged);
public static readonly BindableProperty SideBySideRedoLabelProperty =
BindableProperty.Create(nameof(SideBySideRedoLabel), typeof(string), typeof(ReplicateTransformerView), "Redo",
propertyChanged: OnSideBySideRedoAppearanceChanged);
public static readonly BindableProperty SideBySideRedoDisplayModeProperty =
BindableProperty.Create(nameof(SideBySideRedoDisplayMode), typeof(ButtonDisplayMode?), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySideRedoAppearanceChanged);
// SideBySide - Transform button
public static readonly BindableProperty SideBySideTransformIconTextProperty =
BindableProperty.Create(nameof(SideBySideTransformIconText), typeof(string), typeof(ReplicateTransformerView), "✨",
propertyChanged: OnSideBySideTransformAppearanceChanged);
public static readonly BindableProperty SideBySideTransformIconSourceProperty =
BindableProperty.Create(nameof(SideBySideTransformIconSource), typeof(ImageSource), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySideTransformAppearanceChanged);
public static readonly BindableProperty SideBySideTransformLabelProperty =
BindableProperty.Create(nameof(SideBySideTransformLabel), typeof(string), typeof(ReplicateTransformerView), "Transform",
propertyChanged: OnSideBySideTransformAppearanceChanged);
public static readonly BindableProperty SideBySideTransformDisplayModeProperty =
BindableProperty.Create(nameof(SideBySideTransformDisplayMode), typeof(ButtonDisplayMode?), typeof(ReplicateTransformerView), null,
propertyChanged: OnSideBySideTransformAppearanceChanged);
public static readonly BindableProperty ShowSideBySideTransformButtonProperty =
BindableProperty.Create(nameof(ShowSideBySideTransformButton), typeof(bool), typeof(ReplicateTransformerView), true,
propertyChanged: OnShowSideBySideTransformButtonChanged);
public static readonly BindableProperty SideBySideTransformTypeProperty =
BindableProperty.Create(nameof(SideBySideTransformType), typeof(TransformationType), typeof(ReplicateTransformerView), TransformationType.Image);
public static readonly BindableProperty AutoTransformOnSelectProperty =
BindableProperty.Create(nameof(AutoTransformOnSelect), typeof(bool), typeof(ReplicateTransformerView), false);
public static readonly BindableProperty AutoTransformTypeProperty =
BindableProperty.Create(nameof(AutoTransformType), typeof(TransformationType), typeof(ReplicateTransformerView), TransformationType.Image);
// ============================================
// AUTO-SAVE PROPERTIES
// ============================================
public static readonly BindableProperty AutoSaveImageProperty =
BindableProperty.Create(nameof(AutoSaveImage), typeof(bool), typeof(ReplicateTransformerView), false);
public static readonly BindableProperty AutoSaveVideoProperty =
BindableProperty.Create(nameof(AutoSaveVideo), typeof(bool), typeof(ReplicateTransformerView), false);
public static readonly BindableProperty ImageSavePathProperty =
BindableProperty.Create(nameof(ImageSavePath), typeof(string), typeof(ReplicateTransformerView), null);
public static readonly BindableProperty VideoSavePathProperty =
BindableProperty.Create(nameof(VideoSavePath), typeof(string), typeof(ReplicateTransformerView), null);
public static readonly BindableProperty ImageFilenamePatternProperty =
BindableProperty.Create(nameof(ImageFilenamePattern), typeof(string), typeof(ReplicateTransformerView), "image_{timestamp}");
public static readonly BindableProperty VideoFilenamePatternProperty =
BindableProperty.Create(nameof(VideoFilenamePattern), typeof(string), typeof(ReplicateTransformerView), "video_{timestamp}");
/// <summary>
/// The layout mode for the control.
/// </summary>
public TransformerLayoutMode LayoutMode
{
get => (TransformerLayoutMode)GetValue(LayoutModeProperty);
set => SetValue(LayoutModeProperty, value);
}
public bool ShowCaptureButtons
{
get => (bool)GetValue(ShowCaptureButtonsProperty);
set => SetValue(ShowCaptureButtonsProperty, value);
}
public bool ShowVideoOptions
{
get => (bool)GetValue(ShowVideoOptionsProperty);
set => SetValue(ShowVideoOptionsProperty, value);
}
public string AnimeButtonText
{
get => (string)GetValue(AnimeButtonTextProperty);
set => SetValue(AnimeButtonTextProperty, value);
}
public string VideoButtonText
{
get => (string)GetValue(VideoButtonTextProperty);
set => SetValue(VideoButtonTextProperty, value);
}
public string PlaceholderText
{
get => (string)GetValue(PlaceholderTextProperty);
set => SetValue(PlaceholderTextProperty, value);
}
/// <summary>
/// Text for the select button in overlay mode.
/// </summary>
public string OverlaySelectButtonText
{
get => (string)GetValue(OverlaySelectButtonTextProperty);
set => SetValue(OverlaySelectButtonTextProperty, value);
}
/// <summary>
/// Text for the transform button in overlay mode.
/// </summary>
public string OverlayTransformButtonText
{
get => (string)GetValue(OverlayTransformButtonTextProperty);
set => SetValue(OverlayTransformButtonTextProperty, value);
}
public string? CustomImagePrompt
{
get => (string?)GetValue(CustomImagePromptProperty);
set => SetValue(CustomImagePromptProperty, value);
}
public string? CustomVideoPrompt
{
get => (string?)GetValue(CustomVideoPromptProperty);
set => SetValue(CustomVideoPromptProperty, value);
}
public bool IsProcessing
{
get => (bool)GetValue(IsProcessingProperty);
private set => SetValue(IsProcessingProperty, value);
}
/// <summary>
/// Enable or disable prediction tracking with history.
/// </summary>
public bool TrackPredictions
{
get => (bool)GetValue(TrackPredictionsProperty);
set => SetValue(TrackPredictionsProperty, value);
}
/// <summary>
/// Enable localization for button labels and status messages.
/// When enabled, uses ReplicateStrings for all UI text based on CurrentCulture.
/// </summary>
public bool UseLocalization
{
get => (bool)GetValue(UseLocalizationProperty);
set => SetValue(UseLocalizationProperty, value);
}
/// <summary>
/// Gets the prediction tracker for accessing history and events.
/// Returns null if transformer hasn't been initialized or tracking is disabled.
/// </summary>
public IPredictionTracker? Tracker => _tracker;
public string? ResultUrl
{
get => (string?)GetValue(ResultUrlProperty);
private set => SetValue(ResultUrlProperty, value);
}
public ImageSource? SourceImageSource
{
get => (ImageSource?)GetValue(SourceImageProperty);
private set => SetValue(SourceImageProperty, value);
}
/// <summary>
/// The type of transformation to perform when the overlay transform button is clicked.
/// Default is Image. Set to Video for video generation pages.
/// </summary>
public TransformationType OverlayTransformType
{
get => (TransformationType)GetValue(OverlayTransformTypeProperty);
set => SetValue(OverlayTransformTypeProperty, value);
}
/// <summary>
/// Show or hide the source side buttons (Take/Pick) in SideBySide layout.
/// </summary>
public bool ShowSideBySideSourceButtons
{
get => (bool)GetValue(ShowSideBySideSourceButtonsProperty);
set => SetValue(ShowSideBySideSourceButtonsProperty, value);
}
/// <summary>
/// Show or hide the result side buttons (Clear/Redo) in SideBySide layout.
/// </summary>
public bool ShowSideBySideResultButtons
{
get => (bool)GetValue(ShowSideBySideResultButtonsProperty);
set => SetValue(ShowSideBySideResultButtonsProperty, value);
}
/// <summary>
/// Text for the capture button in SideBySide layout.
/// </summary>
public string SideBySideCaptureButtonText
{
get => (string)GetValue(SideBySideCaptureButtonTextProperty);
set => SetValue(SideBySideCaptureButtonTextProperty, value);
}
/// <summary>
/// Text for the pick button in SideBySide layout.
/// </summary>
public string SideBySidePickButtonText
{
get => (string)GetValue(SideBySidePickButtonTextProperty);
set => SetValue(SideBySidePickButtonTextProperty, value);
}
/// <summary>
/// Text for the clear button in SideBySide layout.
/// </summary>
public string SideBySideClearButtonText
{
get => (string)GetValue(SideBySideClearButtonTextProperty);
set => SetValue(SideBySideClearButtonTextProperty, value);
}
/// <summary>
/// Text for the redo button in SideBySide layout.
/// </summary>
public string SideBySideRedoButtonText
{
get => (string)GetValue(SideBySideRedoButtonTextProperty);
set => SetValue(SideBySideRedoButtonTextProperty, value);
}
// ============================================
// CONSOLIDATED BUTTON CONFIGURATION ACCESSOR
// ============================================
/// <summary>
/// Consolidated button configurations for all transformer buttons.
/// Provides a cleaner API for customizing button appearance.
/// Use this instead of individual button properties for a cleaner API.
/// </summary>
/// <example>
/// <code>
/// // XAML usage:
/// &lt;replicate:ReplicateTransformerView&gt;
/// &lt;replicate:ReplicateTransformerView.ButtonConfigs&gt;
/// &lt;replicate:TransformerButtonConfigs DefaultDisplayMode="Icon"&gt;
/// &lt;replicate:TransformerButtonConfigs.OverlaySelect&gt;
/// &lt;replicate:ButtonConfig Text="Select" IconText="🖼"/&gt;
/// &lt;/replicate:TransformerButtonConfigs.OverlaySelect&gt;
/// &lt;/replicate:TransformerButtonConfigs&gt;
/// &lt;/replicate:ReplicateTransformerView.ButtonConfigs&gt;
/// &lt;/replicate:ReplicateTransformerView&gt;
///
/// // Code-behind usage:
/// transformerView.ButtonConfigs = TransformerButtonConfigs.CreateDefault();
/// transformerView.ButtonConfigs.DefaultDisplayMode = ButtonDisplayMode.Icon;
/// </code>
/// </example>
public TransformerButtonConfigs? ButtonConfigs
{
get => (TransformerButtonConfigs?)GetValue(ButtonConfigsProperty);
set => SetValue(ButtonConfigsProperty, value);
}
// ============================================
// BUTTON DISPLAY MODE AND ICON PROPERTY ACCESSORS (Legacy - use ButtonConfigs instead)
// ============================================
/// <summary>
/// Global display mode for buttons (Label, Icon, or Both). Can be overridden per button.
/// Consider using ButtonConfigs property instead for a cleaner API.
/// </summary>
public ButtonDisplayMode DefaultButtonDisplayMode
{
get => (ButtonDisplayMode)GetValue(DefaultButtonDisplayModeProperty);
set => SetValue(DefaultButtonDisplayModeProperty, value);
}
// Overlay Select Button
public string OverlaySelectIconText
{
get => (string)GetValue(OverlaySelectIconTextProperty);
set => SetValue(OverlaySelectIconTextProperty, value);
}
public ImageSource? OverlaySelectIconSource
{
get => (ImageSource?)GetValue(OverlaySelectIconSourceProperty);
set => SetValue(OverlaySelectIconSourceProperty, value);
}
public string OverlaySelectLabel
{
get => (string)GetValue(OverlaySelectLabelProperty);
set => SetValue(OverlaySelectLabelProperty, value);
}
public ButtonDisplayMode? OverlaySelectDisplayMode
{
get => (ButtonDisplayMode?)GetValue(OverlaySelectDisplayModeProperty);
set => SetValue(OverlaySelectDisplayModeProperty, value);
}
// Overlay Transform Button
public string OverlayTransformIconText
{
get => (string)GetValue(OverlayTransformIconTextProperty);
set => SetValue(OverlayTransformIconTextProperty, value);
}
public ImageSource? OverlayTransformIconSource
{
get => (ImageSource?)GetValue(OverlayTransformIconSourceProperty);
set => SetValue(OverlayTransformIconSourceProperty, value);
}
public string OverlayTransformLabel
{
get => (string)GetValue(OverlayTransformLabelProperty);
set => SetValue(OverlayTransformLabelProperty, value);
}
public ButtonDisplayMode? OverlayTransformDisplayMode
{
get => (ButtonDisplayMode?)GetValue(OverlayTransformDisplayModeProperty);
set => SetValue(OverlayTransformDisplayModeProperty, value);
}
/// <summary>
/// Position of the overlay buttons (Top or Bottom). Default is Bottom.
/// </summary>
public OverlayButtonPosition OverlayButtonPosition
{
get => (OverlayButtonPosition)GetValue(OverlayButtonPositionProperty);
set => SetValue(OverlayButtonPositionProperty, value);
}
// SideBySide Capture Button
public string SideBySideCaptureIconText
{
get => (string)GetValue(SideBySideCaptureIconTextProperty);
set => SetValue(SideBySideCaptureIconTextProperty, value);
}
public ImageSource? SideBySideCaptureIconSource
{
get => (ImageSource?)GetValue(SideBySideCaptureIconSourceProperty);
set => SetValue(SideBySideCaptureIconSourceProperty, value);
}
public string SideBySideCaptureLabel
{
get => (string)GetValue(SideBySideCaptureLabelProperty);
set => SetValue(SideBySideCaptureLabelProperty, value);
}
public ButtonDisplayMode? SideBySideCaptureDisplayMode
{
get => (ButtonDisplayMode?)GetValue(SideBySideCaptureDisplayModeProperty);
set => SetValue(SideBySideCaptureDisplayModeProperty, value);
}
// SideBySide Pick Button
public string SideBySidePickIconText
{
get => (string)GetValue(SideBySidePickIconTextProperty);
set => SetValue(SideBySidePickIconTextProperty, value);
}
public ImageSource? SideBySidePickIconSource
{
get => (ImageSource?)GetValue(SideBySidePickIconSourceProperty);
set => SetValue(SideBySidePickIconSourceProperty, value);
}
public string SideBySidePickLabel
{
get => (string)GetValue(SideBySidePickLabelProperty);
set => SetValue(SideBySidePickLabelProperty, value);
}
public ButtonDisplayMode? SideBySidePickDisplayMode
{
get => (ButtonDisplayMode?)GetValue(SideBySidePickDisplayModeProperty);
set => SetValue(SideBySidePickDisplayModeProperty, value);
}
// SideBySide Clear Button
public string SideBySideClearIconText
{
get => (string)GetValue(SideBySideClearIconTextProperty);
set => SetValue(SideBySideClearIconTextProperty, value);
}
public ImageSource? SideBySideClearIconSource
{
get => (ImageSource?)GetValue(SideBySideClearIconSourceProperty);
set => SetValue(SideBySideClearIconSourceProperty, value);
}
public string SideBySideClearLabel
{
get => (string)GetValue(SideBySideClearLabelProperty);
set => SetValue(SideBySideClearLabelProperty, value);
}
public ButtonDisplayMode? SideBySideClearDisplayMode
{
get => (ButtonDisplayMode?)GetValue(SideBySideClearDisplayModeProperty);
set => SetValue(SideBySideClearDisplayModeProperty, value);
}
// SideBySide Redo Button
public string SideBySideRedoIconText
{
get => (string)GetValue(SideBySideRedoIconTextProperty);
set => SetValue(SideBySideRedoIconTextProperty, value);
}
public ImageSource? SideBySideRedoIconSource
{
get => (ImageSource?)GetValue(SideBySideRedoIconSourceProperty);
set => SetValue(SideBySideRedoIconSourceProperty, value);
}
public string SideBySideRedoLabel
{
get => (string)GetValue(SideBySideRedoLabelProperty);
set => SetValue(SideBySideRedoLabelProperty, value);
}
public ButtonDisplayMode? SideBySideRedoDisplayMode
{
get => (ButtonDisplayMode?)GetValue(SideBySideRedoDisplayModeProperty);
set => SetValue(SideBySideRedoDisplayModeProperty, value);
}
// SideBySide Transform Button
public string SideBySideTransformIconText
{
get => (string)GetValue(SideBySideTransformIconTextProperty);
set => SetValue(SideBySideTransformIconTextProperty, value);
}
public ImageSource? SideBySideTransformIconSource
{
get => (ImageSource?)GetValue(SideBySideTransformIconSourceProperty);
set => SetValue(SideBySideTransformIconSourceProperty, value);
}
public string SideBySideTransformLabel
{
get => (string)GetValue(SideBySideTransformLabelProperty);
set => SetValue(SideBySideTransformLabelProperty, value);
}
public ButtonDisplayMode? SideBySideTransformDisplayMode
{
get => (ButtonDisplayMode?)GetValue(SideBySideTransformDisplayModeProperty);
set => SetValue(SideBySideTransformDisplayModeProperty, value);
}
/// <summary>
/// Show or hide the transform button in SideBySide layout.
/// </summary>
public bool ShowSideBySideTransformButton
{
get => (bool)GetValue(ShowSideBySideTransformButtonProperty);
set => SetValue(ShowSideBySideTransformButtonProperty, value);
}
/// <summary>
/// The type of transformation to perform when the SideBySide transform button is clicked.
/// Default is Image. Set to Video for video generation.
/// </summary>
public TransformationType SideBySideTransformType
{
get => (TransformationType)GetValue(SideBySideTransformTypeProperty);
set => SetValue(SideBySideTransformTypeProperty, value);
}
/// <summary>
/// Automatically start transformation when an image is selected.
/// </summary>
public bool AutoTransformOnSelect
{
get => (bool)GetValue(AutoTransformOnSelectProperty);
set => SetValue(AutoTransformOnSelectProperty, value);
}
/// <summary>
/// The type of transformation to auto-start (Image or Video).
/// Only used when AutoTransformOnSelect is true.
/// </summary>
public TransformationType AutoTransformType
{
get => (TransformationType)GetValue(AutoTransformTypeProperty);
set => SetValue(AutoTransformTypeProperty, value);
}
// ============================================
// AUTO-SAVE PROPERTY ACCESSORS
// ============================================
/// <summary>
/// Automatically save image results to local storage when transformation completes.
/// </summary>
public bool AutoSaveImage
{
get => (bool)GetValue(AutoSaveImageProperty);
set => SetValue(AutoSaveImageProperty, value);
}
/// <summary>
/// Automatically save video results to local storage when transformation completes.
/// </summary>
public bool AutoSaveVideo
{
get => (bool)GetValue(AutoSaveVideoProperty);
set => SetValue(AutoSaveVideoProperty, value);
}
/// <summary>
/// Local folder path where images will be saved. If null, uses app's cache directory.
/// </summary>
public string? ImageSavePath
{
get => (string?)GetValue(ImageSavePathProperty);
set => SetValue(ImageSavePathProperty, value);
}
/// <summary>
/// Local folder path where videos will be saved. If null, uses app's cache directory.
/// </summary>
public string? VideoSavePath
{
get => (string?)GetValue(VideoSavePathProperty);
set => SetValue(VideoSavePathProperty, value);
}
/// <summary>
/// Filename pattern for saved images. Supports placeholders:
/// {timestamp} - Unix timestamp, {datetime} - formatted date/time,
/// {id} - prediction ID, {guid} - new GUID.
/// Extension is added automatically based on content type.
/// </summary>
public string ImageFilenamePattern
{
get => (string)GetValue(ImageFilenamePatternProperty);
set => SetValue(ImageFilenamePatternProperty, value);
}
/// <summary>
/// Filename pattern for saved videos. Supports placeholders:
/// {timestamp} - Unix timestamp, {datetime} - formatted date/time,
/// {id} - prediction ID, {guid} - new GUID.
/// Extension is added automatically based on content type.
/// </summary>
public string VideoFilenamePattern
{
get => (string)GetValue(VideoFilenamePatternProperty);
set => SetValue(VideoFilenamePatternProperty, value);
}
#endregion
#region Events
public event EventHandler<TransformationStartedEventArgs>? TransformationStarted;
public event EventHandler<TransformationCompletedEventArgs>? TransformationCompleted;
public event EventHandler<TransformationErrorEventArgs>? TransformationError;
public event EventHandler<ImageSelectedEventArgs>? ImageSelected;
/// <summary>
/// Raised when the source image is tapped.
/// </summary>
public event EventHandler<ImageTappedEventArgs>? SourceImageTapped;
/// <summary>
/// Raised when the result image is tapped.
/// </summary>
public event EventHandler<ImageTappedEventArgs>? ResultImageTapped;
/// <summary>
/// Raised when the source image is double-tapped.
/// </summary>
public event EventHandler<ImageTappedEventArgs>? SourceImageDoubleTapped;
/// <summary>
/// Raised when the result image is double-tapped.
/// </summary>
public event EventHandler<ImageTappedEventArgs>? ResultImageDoubleTapped;
/// <summary>
/// Raised when a result is auto-saved to local storage.
/// </summary>
public event EventHandler<FileSavedEventArgs>? FileSaved;
/// <summary>
/// Raised when auto-save fails.
/// </summary>
public event EventHandler<FileSaveErrorEventArgs>? FileSaveError;
/// <summary>
/// Raised when a prediction is tracked (started or updated).
/// This is forwarded from the internal PredictionTracker.
/// </summary>
public event EventHandler<PredictionStatusChangedEventArgs>? PredictionTracked;
/// <summary>
/// Raised when a tracked prediction completes.
/// This is forwarded from the internal PredictionTracker.
/// </summary>
public event EventHandler<PredictionCompletedEventArgs>? PredictionCompleted;
/// <summary>
/// Raised when the control's state changes.
/// </summary>
public event EventHandler<StateChangedEventArgs>? StateChanged;
#endregion
/// <summary>
/// Gets the current state of the control.
/// </summary>
public TransformerState CurrentState => _currentState;
private void SetState(TransformerState newState)
{
if (_currentState == newState)
return;
var previousState = _currentState;
_currentState = newState;
StateChanged?.Invoke(this, new StateChangedEventArgs(previousState, newState));
OnPropertyChanged(nameof(CurrentState));
}
public ReplicateTransformerView()
{
InitializeComponent();
UpdateVideoButtonsVisibility();
ApplyLayoutMode();
UpdateAllButtonAppearances();
SetupGestureRecognizers();
}
private void SetupGestureRecognizers()
{
// Single image layout - source image
var sourceImageTap = new TapGestureRecognizer();
sourceImageTap.Tapped += OnSourceImageTapped;
SourceImage.GestureRecognizers.Add(sourceImageTap);
var sourceImageDoubleTap = new TapGestureRecognizer { NumberOfTapsRequired = 2 };
sourceImageDoubleTap.Tapped += OnSourceImageDoubleTapped;
SourceImage.GestureRecognizers.Add(sourceImageDoubleTap);
// Single image layout - result image
var resultImageTap = new TapGestureRecognizer();
resultImageTap.Tapped += OnResultImageTapped;
ResultImage.GestureRecognizers.Add(resultImageTap);
var resultImageDoubleTap = new TapGestureRecognizer { NumberOfTapsRequired = 2 };
resultImageDoubleTap.Tapped += OnResultImageDoubleTapped;
ResultImage.GestureRecognizers.Add(resultImageDoubleTap);
// Side by side layout - source image
var sbsSourceTap = new TapGestureRecognizer();
sbsSourceTap.Tapped += OnSourceImageTapped;
SideBySideSourceImage.GestureRecognizers.Add(sbsSourceTap);
var sbsSourceDoubleTap = new TapGestureRecognizer { NumberOfTapsRequired = 2 };
sbsSourceDoubleTap.Tapped += OnSourceImageDoubleTapped;
SideBySideSourceImage.GestureRecognizers.Add(sbsSourceDoubleTap);
// Side by side layout - result image
var sbsResultTap = new TapGestureRecognizer();
sbsResultTap.Tapped += OnResultImageTapped;
SideBySideResultImage.GestureRecognizers.Add(sbsResultTap);
var sbsResultDoubleTap = new TapGestureRecognizer { NumberOfTapsRequired = 2 };
sbsResultDoubleTap.Tapped += OnResultImageDoubleTapped;
SideBySideResultImage.GestureRecognizers.Add(sbsResultDoubleTap);
}
private void OnSourceImageTapped(object? sender, TappedEventArgs e)
{
SourceImageTapped?.Invoke(this, new ImageTappedEventArgs(true, null, _currentImageBytes));
}
private void OnSourceImageDoubleTapped(object? sender, TappedEventArgs e)
{
SourceImageDoubleTapped?.Invoke(this, new ImageTappedEventArgs(true, null, _currentImageBytes));
}
private void OnResultImageTapped(object? sender, TappedEventArgs e)
{
ResultImageTapped?.Invoke(this, new ImageTappedEventArgs(false, ResultUrl, null));
}
private void OnResultImageDoubleTapped(object? sender, TappedEventArgs e)
{
ResultImageDoubleTapped?.Invoke(this, new ImageTappedEventArgs(false, ResultUrl, null));
}
/// <summary>
/// Initialize with a pre-configured transformer instance.
/// </summary>
public void Initialize(IReplicateTransformer transformer)
{
_transformer = transformer;
InitializeTracker();
}
/// <summary>
/// Initialize with a factory for BYOK (Bring Your Own Key) scenarios.
/// Call SetApiToken() to configure the API token before transformations.
/// </summary>
public void Initialize(IReplicateTransformerFactory factory)
{
_transformerFactory = factory;
_transformer = factory.Create();
InitializeTracker();
}
/// <summary>
/// Set or update the API token at runtime (BYOK scenario).
/// Requires Initialize(IReplicateTransformerFactory) to be called first.
/// </summary>
/// <param name="apiToken">The Replicate API token.</param>
public void SetApiToken(string apiToken)
{
if (_transformerFactory == null)
{
throw new InvalidOperationException(
"SetApiToken requires initialization with IReplicateTransformerFactory. " +
"Call Initialize(IReplicateTransformerFactory) first.");
}
_transformer = _transformerFactory.CreateWithToken(apiToken);
InitializeTracker();
}
/// <summary>
/// Configure custom settings at runtime (BYOK scenario with full control).
/// Requires Initialize(IReplicateTransformerFactory) to be called first.
/// </summary>
/// <param name="configure">Action to configure settings (token, prompts, timeouts, etc.).</param>
public void ConfigureSettings(Action<ReplicateSettings> configure)
{
if (_transformerFactory == null)
{
throw new InvalidOperationException(
"ConfigureSettings requires initialization with IReplicateTransformerFactory. " +
"Call Initialize(IReplicateTransformerFactory) first.");
}
_transformer = _transformerFactory.CreateWithSettings(configure);
InitializeTracker();
}
/// <summary>
/// Set a model preset for image transformations.
/// When set, image transformations will use the preset instead of ReplicateSettings.
/// </summary>
/// <param name="preset">The model preset to use for images.</param>
/// <param name="customParameters">Optional custom parameters to override preset defaults.</param>
public void SetImagePreset(ModelPreset preset, Dictionary<string, object>? customParameters = null)
{
if (preset.Type != ModelType.Image)
{
throw new ArgumentException($"Preset '{preset.Name}' is not an image model.", nameof(preset));
}
_imagePreset = preset;
_customImageParameters = customParameters;
}
/// <summary>
/// Set a model preset for video generation.
/// When set, video transformations will use the preset instead of ReplicateSettings.
/// </summary>
/// <param name="preset">The model preset to use for video.</param>
/// <param name="customParameters">Optional custom parameters to override preset defaults.</param>
public void SetVideoPreset(ModelPreset preset, Dictionary<string, object>? customParameters = null)
{
if (preset.Type != ModelType.Video)
{
throw new ArgumentException($"Preset '{preset.Name}' is not a video model.", nameof(preset));
}
_videoPreset = preset;
_customVideoParameters = customParameters;
}
/// <summary>
/// Update custom parameters for the current image preset.
/// </summary>
public void SetImageParameters(Dictionary<string, object>? customParameters)
{
_customImageParameters = customParameters;
}
/// <summary>
/// Update custom parameters for the current video preset.
/// </summary>
public void SetVideoParameters(Dictionary<string, object>? customParameters)
{
_customVideoParameters = customParameters;
}
/// <summary>
/// Clear presets and use ReplicateSettings instead.
/// </summary>
public void ClearPresets()
{
_imagePreset = null;
_videoPreset = null;
_customImageParameters = null;
_customVideoParameters = null;
}
/// <summary>
/// Gets the currently configured image preset, if any.
/// </summary>
public ModelPreset? ImagePreset => _imagePreset;
/// <summary>
/// Gets the currently configured video preset, if any.
/// </summary>
public ModelPreset? VideoPreset => _videoPreset;
private void InitializeTracker()
{
if (_trackPredictions && _transformer != null)
{
// Unsubscribe from old tracker
if (_tracker != null)
{
_tracker.PredictionStatusChanged -= OnTrackerPredictionStatusChanged;
_tracker.PredictionCompleted -= OnTrackerPredictionCompleted;
_tracker.Dispose();
}
_tracker = new PredictionTracker(_transformer);
// Subscribe to forward events
_tracker.PredictionStatusChanged += OnTrackerPredictionStatusChanged;
_tracker.PredictionCompleted += OnTrackerPredictionCompleted;
}
}
private void OnTrackerPredictionStatusChanged(object? sender, PredictionStatusChangedEventArgs e)
{
PredictionTracked?.Invoke(this, e);
}
private void OnTrackerPredictionCompleted(object? sender, PredictionCompletedEventArgs e)
{
PredictionCompleted?.Invoke(this, e);
}
#region Property Changed Handlers
private static void OnLayoutModeChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
{
view._layoutMode = (TransformerLayoutMode)newValue;
view.ApplyLayoutMode();
}
}
private static void OnShowCaptureButtonsChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
{
view.CapturePhotoButton.IsVisible = (bool)newValue && view._layoutMode == TransformerLayoutMode.ButtonsBelow;
view.CaptureVideoButton.IsVisible = (bool)newValue && view.ShowVideoOptions && view._layoutMode == TransformerLayoutMode.ButtonsBelow;
}
}
private static void OnShowVideoOptionsChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
{
view.UpdateVideoButtonsVisibility();
}
}
private static void OnAnimeButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.TransformAnimeButton.Text = (string)newValue;
}
private static void OnVideoButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.TransformVideoButton.Text = (string)newValue;
}
private static void OnPlaceholderTextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
{
view.PlaceholderLabel.Text = (string)newValue;
view.SideBySidePlaceholderLabel.Text = (string)newValue;
}
}
private static void OnOverlaySelectButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.OverlayPickButton.Text = (string)newValue;
}
private static void OnOverlayTransformButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.OverlayTransformButton.Text = (string)newValue;
}
private static void OnTrackPredictionsChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
{
view._trackPredictions = (bool)newValue;
if ((bool)newValue && view._transformer != null && view._tracker == null)
{
view._tracker = new PredictionTracker(view._transformer);
}
else if (!(bool)newValue && view._tracker != null)
{
view._tracker.Dispose();
view._tracker = null;
}
}
}
private static void OnUseLocalizationChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
{
view.ApplyLocalization();
}
}
private static void OnShowSideBySideSourceButtonsChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.SideBySideSourceButtons.IsVisible = (bool)newValue && view._layoutMode == TransformerLayoutMode.SideBySide;
}
private static void OnShowSideBySideResultButtonsChanged(BindableObject bindable, object oldValue, object newValue)
{
// Result buttons visibility is controlled by whether there's a result, combined with this setting
// The actual visibility logic is in UpdateSideBySideResultButtonsVisibility
if (bindable is ReplicateTransformerView view)
view.UpdateSideBySideResultButtonsVisibility();
}
private static void OnSideBySideCaptureButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.SideBySideCaptureButton.Text = (string)newValue;
}
private static void OnSideBySidePickButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.SideBySidePickButton.Text = (string)newValue;
}
private static void OnSideBySideClearButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.SideBySideClearButton.Text = (string)newValue;
}
private static void OnSideBySideRedoButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.SideBySideRedoButton.Text = (string)newValue;
}
// Consolidated button configs property changed handler
private static void OnButtonConfigsChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
{
// Unsubscribe from old config
if (oldValue is TransformerButtonConfigs oldConfigs)
{
oldConfigs.ConfigurationChanged -= view.OnButtonConfigsConfigurationChanged;
}
// Subscribe to new config
if (newValue is TransformerButtonConfigs newConfigs)
{
newConfigs.ConfigurationChanged += view.OnButtonConfigsConfigurationChanged;
}
view.UpdateAllButtonAppearances();
}
}
private void OnButtonConfigsConfigurationChanged(object? sender, EventArgs e)
{
UpdateAllButtonAppearances();
}
// Button display mode property changed handlers
private static void OnButtonDisplayModeChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateAllButtonAppearances();
}
private static void OnOverlaySelectAppearanceChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateOverlaySelectButtonAppearance();
}
private static void OnOverlayTransformAppearanceChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateOverlayTransformButtonAppearance();
}
private static void OnOverlayButtonPositionChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateOverlayButtonPosition();
}
private static void OnSideBySideCaptureAppearanceChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateSideBySideCaptureButtonAppearance();
}
private static void OnSideBySidePickAppearanceChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateSideBySidePickButtonAppearance();
}
private static void OnSideBySideClearAppearanceChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateSideBySideClearButtonAppearance();
}
private static void OnSideBySideRedoAppearanceChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateSideBySideRedoButtonAppearance();
}
private static void OnSideBySideTransformAppearanceChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateSideBySideTransformButtonAppearance();
}
private static void OnShowSideBySideTransformButtonChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is ReplicateTransformerView view)
view.UpdateSideBySideTransformButtonVisibility();
}
// Helper method to format button text based on display mode
private string FormatButtonText(string iconText, string label, ImageSource? iconSource, ButtonDisplayMode? specificMode)
{
var mode = specificMode ?? DefaultButtonDisplayMode;
return mode switch
{
ButtonDisplayMode.Label => label,
ButtonDisplayMode.Icon => iconSource == null ? iconText : string.Empty,
ButtonDisplayMode.Both => iconSource == null ? $"{iconText} {label}".Trim() : label,
_ => label
};
}
// Helper to update button appearance (text and image)
private void UpdateButtonAppearance(Button button, string iconText, string label, ImageSource? iconSource, ButtonDisplayMode? specificMode)
{
var mode = specificMode ?? DefaultButtonDisplayMode;
button.Text = FormatButtonText(iconText, label, iconSource, specificMode);
// Set image source if using Icon or Both mode with an ImageSource
if (iconSource != null && mode != ButtonDisplayMode.Label)
{
button.ImageSource = iconSource;
}
else
{
button.ImageSource = null;
}
}
private void UpdateAllButtonAppearances()
{
UpdateOverlaySelectButtonAppearance();
UpdateOverlayTransformButtonAppearance();
UpdateSideBySideCaptureButtonAppearance();
UpdateSideBySidePickButtonAppearance();
UpdateSideBySideTransformButtonAppearance();
UpdateSideBySideClearButtonAppearance();
UpdateSideBySideRedoButtonAppearance();
}
/// <summary>
/// Apply localized strings to all UI elements when UseLocalization is enabled.
/// </summary>
private void ApplyLocalization()
{
if (!UseLocalization)
return;
// Update button labels with localized strings
OverlaySelectLabel = ReplicateStrings.Get(ReplicateStrings.Keys.Select);
OverlayTransformLabel = ReplicateStrings.Get(ReplicateStrings.Keys.Transform);
SideBySideCaptureLabel = ReplicateStrings.Get(ReplicateStrings.Keys.Take);
SideBySidePickLabel = ReplicateStrings.Get(ReplicateStrings.Keys.Pick);
SideBySideTransformLabel = ReplicateStrings.Get(ReplicateStrings.Keys.Transform);
SideBySideClearLabel = ReplicateStrings.Get(ReplicateStrings.Keys.Clear);
SideBySideRedoLabel = ReplicateStrings.Get(ReplicateStrings.Keys.Redo);
// Update placeholder labels
PlaceholderLabel.Text = ReplicateStrings.Get(ReplicateStrings.Keys.TapToSelectImage);
SideBySidePlaceholderLabel.Text = ReplicateStrings.Get(ReplicateStrings.Keys.TapToSelectImage);
// Update all button appearances to apply the new labels
UpdateAllButtonAppearances();
}
/// <summary>
/// Get a localized string, or the fallback if localization is disabled.
/// </summary>
private string L(string key, string fallback)
{
return UseLocalization ? ReplicateStrings.Get(key) : fallback;
}
/// <summary>
/// Gets the effective display mode, preferring ButtonConfigs if set.
/// </summary>
private ButtonDisplayMode GetEffectiveDisplayMode()
{
return ButtonConfigs?.DefaultDisplayMode ?? DefaultButtonDisplayMode;
}
private void UpdateOverlaySelectButtonAppearance()
{
// Prefer ButtonConfigs if set
if (ButtonConfigs != null)
{
ButtonConfigs.OverlaySelect.ApplyTo(OverlayPickButton, GetEffectiveDisplayMode());
}
else
{
UpdateButtonAppearance(OverlayPickButton, OverlaySelectIconText, OverlaySelectLabel, OverlaySelectIconSource, OverlaySelectDisplayMode);
}
}
private void UpdateOverlayTransformButtonAppearance()
{
if (ButtonConfigs != null)
{
ButtonConfigs.OverlayTransform.ApplyTo(OverlayTransformButton, GetEffectiveDisplayMode());
}
else
{
UpdateButtonAppearance(OverlayTransformButton, OverlayTransformIconText, OverlayTransformLabel, OverlayTransformIconSource, OverlayTransformDisplayMode);
}
}
private void UpdateOverlayButtonPosition()
{
OverlayButtonsContainer.VerticalOptions = OverlayButtonPosition == OverlayButtonPosition.Top
? LayoutOptions.Start
: LayoutOptions.End;
}
private void UpdateSideBySideCaptureButtonAppearance()
{
if (ButtonConfigs != null)
{
ButtonConfigs.SideBySideCapture.ApplyTo(SideBySideCaptureButton, GetEffectiveDisplayMode());
}
else
{
UpdateButtonAppearance(SideBySideCaptureButton, SideBySideCaptureIconText, SideBySideCaptureLabel, SideBySideCaptureIconSource, SideBySideCaptureDisplayMode);
}
}
private void UpdateSideBySidePickButtonAppearance()
{
if (ButtonConfigs != null)
{
ButtonConfigs.SideBySidePick.ApplyTo(SideBySidePickButton, GetEffectiveDisplayMode());
}
else
{
UpdateButtonAppearance(SideBySidePickButton, SideBySidePickIconText, SideBySidePickLabel, SideBySidePickIconSource, SideBySidePickDisplayMode);
}
}
private void UpdateSideBySideClearButtonAppearance()
{
if (ButtonConfigs != null)
{
ButtonConfigs.SideBySideClear.ApplyTo(SideBySideClearButton, GetEffectiveDisplayMode());
}
else
{
UpdateButtonAppearance(SideBySideClearButton, SideBySideClearIconText, SideBySideClearLabel, SideBySideClearIconSource, SideBySideClearDisplayMode);
}
}
private void UpdateSideBySideRedoButtonAppearance()
{
if (ButtonConfigs != null)
{
ButtonConfigs.SideBySideRedo.ApplyTo(SideBySideRedoButton, GetEffectiveDisplayMode());
}
else
{
UpdateButtonAppearance(SideBySideRedoButton, SideBySideRedoIconText, SideBySideRedoLabel, SideBySideRedoIconSource, SideBySideRedoDisplayMode);
}
}
private void UpdateSideBySideTransformButtonAppearance()
{
if (ButtonConfigs != null)
{
ButtonConfigs.SideBySideTransform.ApplyTo(SideBySideTransformButton, GetEffectiveDisplayMode());
}
else
{
UpdateButtonAppearance(SideBySideTransformButton, SideBySideTransformIconText, SideBySideTransformLabel, SideBySideTransformIconSource, SideBySideTransformDisplayMode);
}
}
private void UpdateSideBySideTransformButtonVisibility()
{
// Show transform button only when there's an image AND the setting allows it AND we're in SideBySide mode
SideBySideTransformButton.IsVisible = ShowSideBySideTransformButton &&
_layoutMode == TransformerLayoutMode.SideBySide &&
_currentImageBytes != null;
}
private void UpdateSideBySideResultButtonsVisibility()
{
// Show result buttons only when there's a result AND the setting allows it
SideBySideResultButtons.IsVisible = ShowSideBySideResultButtons &&
_layoutMode == TransformerLayoutMode.SideBySide &&
SideBySideResultImage.IsVisible;
}
private void UpdateVideoButtonsVisibility()
{
if (_layoutMode == TransformerLayoutMode.ButtonsBelow)
{
CaptureVideoButton.IsVisible = ShowVideoOptions && ShowCaptureButtons;
PickVideoButton.IsVisible = ShowVideoOptions;
TransformVideoButton.IsVisible = ShowVideoOptions;
}
}
private void ApplyLayoutMode()
{
// Reset all containers
SingleImageContainer.IsVisible = false;
SideBySideContainer.IsVisible = false;
SourceButtonsGrid.IsVisible = false;
TransformButtonsGrid.IsVisible = false;
OverlayButtonsContainer.IsVisible = false;
switch (_layoutMode)
{
case TransformerLayoutMode.ImageOnly:
SingleImageContainer.IsVisible = true;
// No buttons shown
break;
case TransformerLayoutMode.ButtonsBelow:
SingleImageContainer.IsVisible = true;
SourceButtonsGrid.IsVisible = true;
// TransformButtonsGrid shown when image is selected
UpdateVideoButtonsVisibility();
break;
case TransformerLayoutMode.ButtonsOverlay:
SingleImageContainer.IsVisible = true;
OverlayButtonsContainer.IsVisible = true;
break;
case TransformerLayoutMode.SideBySide:
SideBySideContainer.IsVisible = true;
SideBySideSourceButtons.IsVisible = ShowSideBySideSourceButtons;
// Result buttons shown only when there's a result
UpdateSideBySideResultButtonsVisibility();
break;
}
}
#endregion
#region Button Handlers
private async void OnCapturePhotoClicked(object? sender, EventArgs e)
{
try
{
if (!MediaPicker.Default.IsCaptureSupported)
{
await ShowError("Camera capture is not supported on this device.");
return;
}
var photo = await MediaPicker.Default.CapturePhotoAsync();
if (photo != null)
{
await LoadImageFromFileResult(photo);
}
}
catch (PermissionException)
{
await ShowError("Camera permission is required to capture photos.");
}
catch (Exception ex)
{
await ShowError($"Failed to capture photo: {ex.Message}");
}
}
private async void OnPickImageClicked(object? sender, EventArgs e)
{
try
{
var photo = await MediaPicker.Default.PickPhotoAsync();
if (photo != null)
{
await LoadImageFromFileResult(photo);
}
}
catch (Exception ex)
{
await ShowError($"Failed to pick image: {ex.Message}");
}
}
private async void OnCaptureVideoClicked(object? sender, EventArgs e)
{
try
{
if (!MediaPicker.Default.IsCaptureSupported)
{
await ShowError("Video capture is not supported on this device.");
return;
}
var video = await MediaPicker.Default.CaptureVideoAsync();
if (video != null)
{
// For video, we'll extract first frame or use thumbnail
await LoadImageFromFileResult(video);
}
}
catch (PermissionException)
{
await ShowError("Camera permission is required to capture video.");
}
catch (Exception ex)
{
await ShowError($"Failed to capture video: {ex.Message}");
}
}
private async void OnPickVideoClicked(object? sender, EventArgs e)
{
try
{
var video = await MediaPicker.Default.PickVideoAsync();
if (video != null)
{
await LoadImageFromFileResult(video);
}
}
catch (Exception ex)
{
await ShowError($"Failed to pick video: {ex.Message}");
}
}
private async void OnTransformAnimeClicked(object? sender, EventArgs e)
{
if (_currentImageBytes == null || _transformer == null)
return;
await PerformTransformation(TransformationType.Image);
}
private async void OnTransformVideoClicked(object? sender, EventArgs e)
{
if (_currentImageBytes == null || _transformer == null)
return;
await PerformTransformation(TransformationType.Video);
}
private async void OnOverlayTransformClicked(object? sender, EventArgs e)
{
if (_currentImageBytes == null || _transformer == null)
return;
await PerformTransformation(OverlayTransformType);
}
private void OnResetClicked(object? sender, EventArgs e)
{
Reset();
}
private void OnSideBySideClearClicked(object? sender, EventArgs e)
{
// Clear only the result side
SideBySideResultImage.Source = null;
SideBySideResultImage.IsVisible = false;
SideBySideResultPlaceholder.IsVisible = true;
SideBySideResultButtons.IsVisible = false;
ResultUrl = null;
}
private async void OnSideBySideRedoClicked(object? sender, EventArgs e)
{
// Redo transformation with current source image
if (_currentImageBytes == null || _transformer == null)
return;
await PerformTransformation(TransformationType.Image);
}
private async void OnSideBySideTransformClicked(object? sender, EventArgs e)
{
if (_currentImageBytes == null || _transformer == null)
return;
await PerformTransformation(SideBySideTransformType);
}
private void OnDismissErrorClicked(object? sender, EventArgs e)
{
ErrorOverlay.IsVisible = false;
}
private void OnPlayVideoClicked(object? sender, EventArgs e)
{
if (string.IsNullOrEmpty(_videoUrl))
return;
SwitchToVideoPlayer();
}
private void OnSideBySidePlayVideoClicked(object? sender, EventArgs e)
{
if (string.IsNullOrEmpty(_videoUrl))
return;
SwitchToVideoPlayer();
}
private void SwitchToVideoPlayer()
{
if (string.IsNullOrEmpty(_videoUrl))
return;
_isShowingVideo = true;
if (_layoutMode == TransformerLayoutMode.SideBySide)
{
// Hide result image and play button, show video player
SideBySideResultImage.IsVisible = false;
SideBySidePlayButtonOverlay.IsVisible = false;
SideBySideVideoPlayer.Source = MediaSource.FromUri(_videoUrl);
SideBySideVideoPlayer.IsVisible = true;
// Flip buttons to show Back to Image / Clear
UpdateSideBySideVideoButtons(true);
}
else
{
// Single layout - hide result image, show video
ResultImage.IsVisible = false;
PlayButtonOverlay.IsVisible = false;
VideoPlayer.Source = MediaSource.FromUri(_videoUrl);
VideoPlayer.IsVisible = true;
}
}
/// <summary>
/// Switch from video player back to showing the source image (single layout)
/// or the result thumbnail (side-by-side layout).
/// </summary>
public void SwitchToImageView()
{
_isShowingVideo = false;
if (_layoutMode == TransformerLayoutMode.SideBySide)
{
// Stop and hide video player
SideBySideVideoPlayer.Stop();
SideBySideVideoPlayer.IsVisible = false;
// Show result image with play button overlay
SideBySideResultImage.IsVisible = true;
if (!string.IsNullOrEmpty(_videoUrl))
{
SideBySidePlayButtonOverlay.IsVisible = true;
}
// Flip buttons back to Clear / Redo
UpdateSideBySideVideoButtons(false);
}
else
{
// Single layout
VideoPlayer.Stop();
VideoPlayer.IsVisible = false;
// Show source image (or result image if we have one)
if (_currentImageBytes != null)
{
SourceImage.IsVisible = true;
}
if (!string.IsNullOrEmpty(_videoUrl))
{
PlayButtonOverlay.IsVisible = true;
}
}
}
private void UpdateSideBySideVideoButtons(bool showVideoButtons)
{
if (showVideoButtons)
{
// Change buttons to: Back to Image / Clear
var backToImageConfig = DefaultButtonConfigs.BackToImage;
SideBySideClearButton.Text = backToImageConfig.GetDisplayText(GetEffectiveDisplayMode());
SideBySideClearButton.Clicked -= OnSideBySideClearClicked;
SideBySideClearButton.Clicked += OnSideBySideBackToImageClicked;
var clearConfig = DefaultButtonConfigs.Clear;
SideBySideRedoButton.Text = clearConfig.GetDisplayText(GetEffectiveDisplayMode());
SideBySideRedoButton.Clicked -= OnSideBySideRedoClicked;
SideBySideRedoButton.Clicked += OnSideBySideClearVideoClicked;
SideBySideResultButtons.IsVisible = true;
}
else
{
// Restore original buttons: Clear / Redo
UpdateSideBySideClearButtonAppearance();
SideBySideClearButton.Clicked -= OnSideBySideBackToImageClicked;
SideBySideClearButton.Clicked += OnSideBySideClearClicked;
UpdateSideBySideRedoButtonAppearance();
SideBySideRedoButton.Clicked -= OnSideBySideClearVideoClicked;
SideBySideRedoButton.Clicked += OnSideBySideRedoClicked;
UpdateSideBySideResultButtonsVisibility();
}
}
private void OnSideBySideBackToImageClicked(object? sender, EventArgs e)
{
SwitchToImageView();
}
private void OnSideBySideClearVideoClicked(object? sender, EventArgs e)
{
// Stop video and clear everything
SideBySideVideoPlayer.Stop();
SideBySideVideoPlayer.IsVisible = false;
SideBySideResultImage.Source = null;
SideBySideResultImage.IsVisible = false;
SideBySidePlayButtonOverlay.IsVisible = false;
SideBySideResultPlaceholder.IsVisible = true;
_videoUrl = null;
_isShowingVideo = false;
// Restore original buttons
UpdateSideBySideVideoButtons(false);
SideBySideResultButtons.IsVisible = false;
ResultUrl = null;
}
#endregion
#region Public Methods
public async Task SetImageAsync(byte[] imageBytes)
{
_currentImageBytes = imageBytes;
SourceImageSource = ImageSource.FromStream(() => new MemoryStream(imageBytes));
if (_layoutMode == TransformerLayoutMode.SideBySide)
{
SideBySideSourceImage.Source = SourceImageSource;
SideBySidePlaceholder.IsVisible = false;
SideBySideSourceImage.IsVisible = true;
// Reset result side
SideBySideResultImage.IsVisible = false;
SideBySideResultPlaceholder.IsVisible = true;
SideBySideResultButtons.IsVisible = false;
// Show transform button
UpdateSideBySideTransformButtonVisibility();
}
else
{
SourceImage.Source = SourceImageSource;
ShowImageState();
}
ImageSelected?.Invoke(this, new ImageSelectedEventArgs(imageBytes));
// Auto-transform if enabled
if (AutoTransformOnSelect && _transformer != null)
{
await PerformTransformation(AutoTransformType);
}
}
public async Task SetImageAsync(Stream imageStream)
{
using var ms = new MemoryStream();
await imageStream.CopyToAsync(ms);
await SetImageAsync(ms.ToArray());
}
public async Task SetImageAsync(string filePath)
{
var bytes = await File.ReadAllBytesAsync(filePath);
await SetImageAsync(bytes);
}
/// <summary>
/// Programmatically trigger an image transformation.
/// </summary>
public async Task TransformImageAsync()
{
if (_currentImageBytes == null || _transformer == null)
return;
await PerformTransformation(TransformationType.Image);
}
/// <summary>
/// Programmatically trigger a video transformation.
/// </summary>
public async Task TransformVideoAsync()
{
if (_currentImageBytes == null || _transformer == null)
return;
await PerformTransformation(TransformationType.Video);
}
/// <summary>
/// Gets the path to the last auto-saved image file, if any.
/// </summary>
public string? LastSavedImagePath => _lastSavedImagePath;
/// <summary>
/// Gets the path to the last auto-saved video file, if any.
/// </summary>
public string? LastSavedVideoPath => _lastSavedVideoPath;
/// <summary>
/// Manually save the current result to a file.
/// </summary>
/// <param name="url">The URL of the result to save.</param>
/// <param name="type">The type of result (Image or Video).</param>
/// <param name="predictionId">Optional prediction ID for filename pattern.</param>
/// <returns>The path to the saved file, or null if save failed.</returns>
public async Task<string?> SaveResultAsync(string url, TransformationType type, string? predictionId = null)
{
return await SaveFileFromUrlAsync(url, type, predictionId);
}
/// <summary>
/// Refresh all localized strings. Call this after changing ReplicateStrings.CurrentCulture
/// to update all UI text to the new language.
/// </summary>
public void RefreshLocalization()
{
if (UseLocalization)
{
ApplyLocalization();
}
}
public void Reset()
{
_currentImageBytes = null;
ResultUrl = null;
SourceImageSource = null;
_videoUrl = null;
_isShowingVideo = false;
// Reset single image layout
SourceImage.Source = null;
SourceImage.IsVisible = false;
ResultImage.Source = null;
ResultImage.IsVisible = false;
PlaceholderView.IsVisible = true;
ProcessingOverlay.IsVisible = false;
ErrorOverlay.IsVisible = false;
// Reset video player
VideoPlayer.Stop();
VideoPlayer.IsVisible = false;
PlayButtonOverlay.IsVisible = false;
// Reset side-by-side layout
SideBySideSourceImage.Source = null;
SideBySideSourceImage.IsVisible = false;
SideBySidePlaceholder.IsVisible = true;
SideBySideResultImage.Source = null;
SideBySideResultImage.IsVisible = false;
SideBySideResultPlaceholder.IsVisible = true;
SideBySideProcessingOverlay.IsVisible = false;
// Reset side-by-side video player
SideBySideVideoPlayer.Stop();
SideBySideVideoPlayer.IsVisible = false;
SideBySidePlayButtonOverlay.IsVisible = false;
// Restore original button handlers if they were swapped
UpdateSideBySideVideoButtons(false);
// Reset buttons based on layout
if (_layoutMode == TransformerLayoutMode.ButtonsBelow || _layoutMode == TransformerLayoutMode.SideBySide)
{
SourceButtonsGrid.IsVisible = true;
TransformButtonsGrid.IsVisible = false;
}
}
#endregion
#region Private Methods
private async Task LoadImageFromFileResult(FileResult file)
{
using var stream = await file.OpenReadAsync();
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
await SetImageAsync(ms.ToArray());
}
private async Task<string?> SaveFileFromUrlAsync(string url, TransformationType type, string? predictionId)
{
// Try to get the service from DI if not already set
_fileSaveService ??= TryGetFileSaveService();
if (_fileSaveService == null)
{
// Fallback: use inline implementation if service not available
return await SaveFileFromUrlInlineAsync(url, type, predictionId);
}
var savePath = type == TransformationType.Image ? ImageSavePath : VideoSavePath;
var pattern = type == TransformationType.Image ? ImageFilenamePattern : VideoFilenamePattern;
var result = await _fileSaveService.SaveFromUrlAsync(url, type, savePath, pattern, predictionId);
if (result.Success)
{
// Store last saved path
if (type == TransformationType.Image)
_lastSavedImagePath = result.FilePath;
else
_lastSavedVideoPath = result.FilePath;
// Raise event
FileSaved?.Invoke(this, new FileSavedEventArgs(result.FilePath!, type, result.FileSizeBytes));
return result.FilePath;
}
else
{
FileSaveError?.Invoke(this, new FileSaveErrorEventArgs(type, result.Error!));
return null;
}
}
private Services.IFileSaveService? TryGetFileSaveService()
{
try
{
return Handler?.MauiContext?.Services?.GetService<Services.IFileSaveService>();
}
catch
{
return null;
}
}
// Fallback implementation when IFileSaveService is not available via DI
private async Task<string?> SaveFileFromUrlInlineAsync(string url, TransformationType type, string? predictionId)
{
try
{
using var httpClient = new HttpClient();
// Determine save path
var basePath = type == TransformationType.Image ? ImageSavePath : VideoSavePath;
if (string.IsNullOrEmpty(basePath))
{
basePath = FileSystem.CacheDirectory;
}
// Ensure directory exists
if (!Directory.Exists(basePath))
{
Directory.CreateDirectory(basePath);
}
// Download the file
var response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
// Determine file extension from content type or URL
var extension = GetFileExtension(response.Content.Headers.ContentType?.MediaType, url, type);
// Generate filename from pattern
var pattern = type == TransformationType.Image ? ImageFilenamePattern : VideoFilenamePattern;
var filename = GenerateFilename(pattern, predictionId) + extension;
// Full path
var filePath = Path.Combine(basePath, filename);
// Save the file
var bytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync(filePath, bytes);
// Store last saved path
if (type == TransformationType.Image)
_lastSavedImagePath = filePath;
else
_lastSavedVideoPath = filePath;
// Raise event
FileSaved?.Invoke(this, new FileSavedEventArgs(filePath, type, bytes.Length));
return filePath;
}
catch (Exception ex)
{
FileSaveError?.Invoke(this, new FileSaveErrorEventArgs(type, ex));
return null;
}
}
private static string GetFileExtension(string? contentType, string url, TransformationType type)
{
// Try to get from content type
if (!string.IsNullOrEmpty(contentType))
{
return contentType switch
{
"image/png" => ".png",
"image/jpeg" or "image/jpg" => ".jpg",
"image/webp" => ".webp",
"image/gif" => ".gif",
"video/mp4" => ".mp4",
"video/webm" => ".webm",
"video/quicktime" => ".mov",
_ => null
} ?? GetExtensionFromUrl(url, type);
}
return GetExtensionFromUrl(url, type);
}
private static string GetExtensionFromUrl(string url, TransformationType type)
{
// Try to extract from URL
try
{
var uri = new Uri(url);
var path = uri.AbsolutePath;
var ext = Path.GetExtension(path);
if (!string.IsNullOrEmpty(ext))
return ext;
}
catch { }
// Default extensions
return type == TransformationType.Image ? ".png" : ".mp4";
}
private static string GenerateFilename(string pattern, string? predictionId)
{
var now = DateTimeOffset.Now;
var result = pattern
.Replace("{timestamp}", now.ToUnixTimeSeconds().ToString())
.Replace("{datetime}", now.ToString("yyyyMMdd_HHmmss"))
.Replace("{id}", predictionId ?? Guid.NewGuid().ToString("N")[..8])
.Replace("{guid}", Guid.NewGuid().ToString("N"));
// Sanitize filename
foreach (var c in Path.GetInvalidFileNameChars())
{
result = result.Replace(c, '_');
}
return result;
}
private async Task AutoSaveIfEnabledAsync(string? url, TransformationType type, string? predictionId)
{
if (string.IsNullOrEmpty(url))
return;
var shouldSave = type == TransformationType.Image ? AutoSaveImage : AutoSaveVideo;
if (!shouldSave)
return;
await SaveFileFromUrlAsync(url, type, predictionId);
}
private void ShowImageState()
{
PlaceholderView.IsVisible = false;
SourceImage.IsVisible = true;
ResultImage.IsVisible = false;
if (_layoutMode == TransformerLayoutMode.ButtonsBelow)
{
SourceButtonsGrid.IsVisible = true;
TransformButtonsGrid.IsVisible = true;
}
}
private async Task PerformTransformation(TransformationType type)
{
if (_currentImageBytes == null || _transformer == null)
{
await ShowError(L(ReplicateStrings.Keys.NoTransformerInitialized,
"No image selected or transformer not initialized. Call Initialize() with IReplicateTransformer."));
return;
}
IsProcessing = true;
// Show processing based on layout
if (_layoutMode == TransformerLayoutMode.SideBySide)
{
SideBySideProcessingOverlay.IsVisible = true;
SideBySideProcessingLabel.Text = type == TransformationType.Image
? L(ReplicateStrings.Keys.Transforming, "Transforming...")
: L(ReplicateStrings.Keys.GeneratingVideo, "Generating video...");
}
else
{
ProcessingOverlay.IsVisible = true;
ProcessingLabel.Text = type == TransformationType.Image
? L(ReplicateStrings.Keys.TransformingImage, "Transforming image...")
: L(ReplicateStrings.Keys.GeneratingVideo, "Generating video...");
ProcessingSubLabel.Text = type == TransformationType.Video
? L(ReplicateStrings.Keys.MayTakeSeveralMinutes, "This may take several minutes")
: L(ReplicateStrings.Keys.MayTakeAMoment, "This may take a moment");
}
SetButtonsEnabled(false);
_currentTransformationCts = new CancellationTokenSource();
TransformationStarted?.Invoke(this, new TransformationStartedEventArgs(type));
try
{
PredictionResult result;
if (type == TransformationType.Image)
{
// Use preset if configured, otherwise use legacy settings-based approach
if (_imagePreset != null)
{
result = await _transformer.RunPresetAsync(
_imagePreset,
_currentImageBytes,
CustomImagePrompt,
_customImageParameters,
cancellationToken: _currentTransformationCts.Token);
}
else
{
result = await _transformer.TransformToAnimeAsync(
_currentImageBytes,
CustomImagePrompt,
cancellationToken: _currentTransformationCts.Token);
}
}
else
{
// Use preset if configured, otherwise use legacy settings-based approach
if (_videoPreset != null)
{
result = await _transformer.RunPresetAsync(
_videoPreset,
_currentImageBytes,
CustomVideoPrompt,
_customVideoParameters,
cancellationToken: _currentTransformationCts.Token);
}
else
{
result = await _transformer.TransformToVideoAsync(
_currentImageBytes,
CustomVideoPrompt,
cancellationToken: _currentTransformationCts.Token);
}
}
_currentPredictionId = result.Id;
ResultUrl = result.Output;
// Track prediction in history
_tracker?.Track(result, type, _currentImageBytes);
// Store transformation type
_lastTransformationType = type;
// Display result based on layout and type
if (type == TransformationType.Image && result.Output != null)
{
_videoUrl = null; // Clear video URL for image results
var resultImageSource = ImageSource.FromUri(new Uri(result.Output));
if (_layoutMode == TransformerLayoutMode.SideBySide)
{
SideBySideResultImage.Source = resultImageSource;
SideBySideResultPlaceholder.IsVisible = false;
SideBySideResultImage.IsVisible = true;
SideBySidePlayButtonOverlay.IsVisible = false;
UpdateSideBySideResultButtonsVisibility();
}
else
{
ResultImage.Source = resultImageSource;
SourceImage.IsVisible = false;
ResultImage.IsVisible = true;
PlayButtonOverlay.IsVisible = false;
}
}
else if (type == TransformationType.Video && result.Output != null)
{
// Store video URL for playback
_videoUrl = result.Output;
if (_layoutMode == TransformerLayoutMode.SideBySide)
{
// Show source image as thumbnail on result side with play button overlay
SideBySideResultImage.Source = SourceImageSource;
SideBySideResultPlaceholder.IsVisible = false;
SideBySideResultImage.IsVisible = true;
SideBySidePlayButtonOverlay.IsVisible = true;
UpdateSideBySideResultButtonsVisibility();
}
else
{
// Single layout - show source image with play button overlay
SourceImage.IsVisible = true;
ResultImage.IsVisible = false;
PlayButtonOverlay.IsVisible = true;
}
}
TransformationCompleted?.Invoke(this, new TransformationCompletedEventArgs(type, result));
// Auto-save if enabled
_ = AutoSaveIfEnabledAsync(result.Output, type, result.Id);
}
catch (OperationCanceledException)
{
// User canceled - don't show error
}
catch (Exception ex)
{
await ShowError(ex.Message);
TransformationError?.Invoke(this, new TransformationErrorEventArgs(type, ex));
}
finally
{
IsProcessing = false;
ProcessingOverlay.IsVisible = false;
SideBySideProcessingOverlay.IsVisible = false;
SetButtonsEnabled(true);
_currentTransformationCts?.Dispose();
_currentTransformationCts = null;
}
}
/// <summary>
/// Cancel the current transformation in progress.
/// </summary>
public async Task CancelTransformationAsync()
{
_currentTransformationCts?.Cancel();
// Also cancel on the server if we have a prediction ID
if (_currentPredictionId != null && _transformer != null)
{
try
{
await _transformer.CancelPredictionAsync(_currentPredictionId);
}
catch
{
// Ignore errors during cancellation
}
}
}
private void SetButtonsEnabled(bool enabled)
{
// Standard buttons
CapturePhotoButton.IsEnabled = enabled;
PickImageButton.IsEnabled = enabled;
CaptureVideoButton.IsEnabled = enabled;
PickVideoButton.IsEnabled = enabled;
TransformAnimeButton.IsEnabled = enabled;
TransformVideoButton.IsEnabled = enabled;
ResetButton.IsEnabled = enabled;
// Overlay buttons
OverlayPickButton.IsEnabled = enabled;
OverlayTransformButton.IsEnabled = enabled;
// SideBySide buttons
SideBySideCaptureButton.IsEnabled = enabled;
SideBySidePickButton.IsEnabled = enabled;
SideBySideClearButton.IsEnabled = enabled;
SideBySideRedoButton.IsEnabled = enabled;
SideBySideTransformButton.IsEnabled = enabled;
}
private Task ShowError(string message)
{
ErrorLabel.Text = message;
ErrorOverlay.IsVisible = true;
return Task.CompletedTask;
}
#endregion
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
_currentTransformationCts?.Cancel();
_currentTransformationCts?.Dispose();
_tracker?.Dispose();
}
_disposed = true;
}
#endregion
}
// Event args classes are defined in Replicate.Maui/EventArgs.cs