Initial
This commit is contained in:
parent
9a3780ab40
commit
9c075174bd
103
.gitignore
vendored
Normal file
103
.gitignore
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio
|
||||
.vs/
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.rsuser
|
||||
*.userprefs
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/
|
||||
|
||||
# Rider
|
||||
.idea/
|
||||
|
||||
# NuGet
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# MSBuild
|
||||
*.csproj.user
|
||||
*.vbproj.user
|
||||
|
||||
# Windows
|
||||
[Tt]humbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
|
||||
# MAUI / Xamarin
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
*.apk
|
||||
*.aab
|
||||
|
||||
# Test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
*.trx
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# Resharper
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
# TeamCity
|
||||
.TeamCity*
|
||||
|
||||
# DotCover
|
||||
*.dotCover
|
||||
|
||||
# Publish profiles
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Backup files
|
||||
*~
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# Local app settings
|
||||
appsettings.*.json
|
||||
!appsettings.json
|
||||
!appsettings.Development.json
|
||||
|
||||
# Secrets
|
||||
*.pfx
|
||||
*.snk
|
||||
secrets.json
|
||||
1107
API_Reference.md
Normal file
1107
API_Reference.md
Normal file
File diff suppressed because it is too large
Load Diff
445
README.md
Normal file
445
README.md
Normal file
@ -0,0 +1,445 @@
|
||||
# MarketAlly.Replicate.Maui
|
||||
|
||||
[](https://www.nuget.org/packages/MarketAlly.Replicate.Maui/)
|
||||
[](https://www.nuget.org/packages/MarketAlly.Replicate.Maui/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://dotnet.microsoft.com/download)
|
||||
[](https://dotnet.microsoft.com/apps/maui)
|
||||
|
||||
A production-ready .NET library for [Replicate](https://replicate.com) AI API integration. Generate AI images and videos using popular models like Stable Diffusion XL, HiDream, Google Veo 3, Kling, Luma Ray, and more.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dual-target support**: Use with .NET MAUI apps (iOS, Android, Windows, macOS) or plain .NET 9 (console, ASP.NET Core, Blazor)
|
||||
- **Pre-configured model presets**: Ready-to-use configurations for popular AI models
|
||||
- **Ready-to-use MAUI controls**: Drop-in UI components for image/video transformation
|
||||
- **Prediction tracking**: Real-time status updates and history management
|
||||
- **BYOK support**: Bring Your Own Key for multi-tenant applications
|
||||
- **Localization**: Built-in support for 8 languages with custom translation capability
|
||||
- **Webhook integration**: Support for async processing with webhooks
|
||||
|
||||
## Supported Models
|
||||
|
||||
### Image Models
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| **HiDream E1.1** | Fast high-quality image generation/editing |
|
||||
| **Stable Diffusion XL** | Popular open-source image generation |
|
||||
| **Ideogram V3 Turbo** | Best for images with realistic, legible text |
|
||||
| **Recraft V3 SVG** | High-quality SVG images, logos, and icons |
|
||||
|
||||
### Video Models
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| **Google Veo 3 Fast** | State-of-the-art video generation from Google |
|
||||
| **Kling 2.5 Turbo Pro** | Latest high-quality video generation |
|
||||
| **Kling 1.6 Pro** | Professional video generation |
|
||||
| **Seedance 1 Pro** | High-quality video with image-to-video support |
|
||||
| **Luma Ray Flash 2** | Fast video generation with camera control |
|
||||
| **Wan 2.1** | Image-to-video with 480p/720p support |
|
||||
| **MiniMax Video 01** | Text and image to video generation |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
dotnet add package MarketAlly.Replicate.Maui
|
||||
```
|
||||
|
||||
Or via Package Manager:
|
||||
```powershell
|
||||
Install-Package MarketAlly.Replicate.Maui
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: .NET MAUI Application (Full Experience)
|
||||
|
||||
**1. Configure in MauiProgram.cs:**
|
||||
|
||||
```csharp
|
||||
using MarketAlly.Replicate.Maui;
|
||||
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder
|
||||
.UseMauiApp<App>()
|
||||
.UseReplicateMaui(settings =>
|
||||
{
|
||||
settings.ApiToken = "your-replicate-api-token";
|
||||
});
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
```
|
||||
|
||||
**2. Add the control to your XAML:**
|
||||
|
||||
```xml
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:replicate="clr-namespace:MarketAlly.Replicate.Maui.Controls;assembly=MarketAlly.Replicate.Maui">
|
||||
|
||||
<replicate:ReplicateTransformerView
|
||||
x:Name="TransformerView"
|
||||
LayoutMode="SideBySide"
|
||||
TransformationCompleted="OnTransformationCompleted" />
|
||||
</ContentPage>
|
||||
```
|
||||
|
||||
**3. Initialize and handle events:**
|
||||
|
||||
```csharp
|
||||
using MarketAlly.Replicate.Maui;
|
||||
using MarketAlly.Replicate.Maui.Controls;
|
||||
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
public MainPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Use a preset model
|
||||
TransformerView.SetImagePreset(ModelPresets.HiDreamE1);
|
||||
}
|
||||
|
||||
private void OnTransformationCompleted(object sender, TransformationCompletedEventArgs e)
|
||||
{
|
||||
// e.Result.Output contains the URL to the generated image/video
|
||||
Console.WriteLine($"Result: {e.Result.Output}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: API-Only Usage (Console, ASP.NET Core, Blazor)
|
||||
|
||||
```csharp
|
||||
using MarketAlly.Replicate.Maui;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
// Create transformer directly
|
||||
var settings = Options.Create(new ReplicateSettings
|
||||
{
|
||||
ApiToken = "your-replicate-api-token"
|
||||
});
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
var transformer = new ReplicateTransformer(httpClient, settings);
|
||||
|
||||
// Use a model preset
|
||||
var preset = ModelPresets.HiDreamE1;
|
||||
var imageBytes = File.ReadAllBytes("input.jpg");
|
||||
|
||||
var result = await transformer.RunPresetAsync(
|
||||
preset,
|
||||
imageBytes,
|
||||
customPrompt: "anime style portrait, studio ghibli inspired"
|
||||
);
|
||||
|
||||
Console.WriteLine($"Status: {result.Status}");
|
||||
Console.WriteLine($"Output: {result.Output}");
|
||||
```
|
||||
|
||||
### Option 3: Dependency Injection (ASP.NET Core)
|
||||
|
||||
```csharp
|
||||
// In Program.cs or Startup.cs
|
||||
services.Configure<ReplicateSettings>(options =>
|
||||
{
|
||||
options.ApiToken = Configuration["Replicate:ApiToken"];
|
||||
});
|
||||
services.AddHttpClient<IReplicateTransformer, ReplicateTransformer>();
|
||||
|
||||
// In your controller or service
|
||||
public class ImageController : ControllerBase
|
||||
{
|
||||
private readonly IReplicateTransformer _transformer;
|
||||
|
||||
public ImageController(IReplicateTransformer transformer)
|
||||
{
|
||||
_transformer = transformer;
|
||||
}
|
||||
|
||||
[HttpPost("generate")]
|
||||
public async Task<IActionResult> Generate([FromBody] GenerateRequest request)
|
||||
{
|
||||
var result = await _transformer.RunPresetAsync(
|
||||
ModelPresets.StableDiffusionXL,
|
||||
Convert.FromBase64String(request.ImageBase64),
|
||||
request.Prompt
|
||||
);
|
||||
|
||||
return Ok(new { result.Output, result.Status });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using Model Presets
|
||||
|
||||
Model presets provide pre-configured settings for popular models:
|
||||
|
||||
```csharp
|
||||
// Image Models
|
||||
var hidream = ModelPresets.HiDreamE1;
|
||||
var sdxl = ModelPresets.StableDiffusionXL;
|
||||
var ideogram = ModelPresets.IdeogramV3Turbo;
|
||||
var recraft = ModelPresets.RecraftV3Svg;
|
||||
|
||||
// Video Models
|
||||
var veo = ModelPresets.GoogleVeo3Fast;
|
||||
var kling = ModelPresets.Kling25TurboPro;
|
||||
var seedance = ModelPresets.SeedancePro;
|
||||
var luma = ModelPresets.LumaRayFlash;
|
||||
|
||||
// Get all presets
|
||||
var allPresets = ModelPresets.All;
|
||||
var imageModels = ModelPresets.ImageModels;
|
||||
var videoModels = ModelPresets.VideoModels;
|
||||
|
||||
// Find by name
|
||||
var preset = ModelPresets.FindByName("HiDream E1.1");
|
||||
```
|
||||
|
||||
### Custom Parameters
|
||||
|
||||
Override default preset parameters:
|
||||
|
||||
```csharp
|
||||
var customParams = new Dictionary<string, object>
|
||||
{
|
||||
{ "guidance_scale", 7.5 },
|
||||
{ "num_inference_steps", 30 },
|
||||
{ "seed", 42 }
|
||||
};
|
||||
|
||||
var result = await transformer.RunPresetAsync(
|
||||
ModelPresets.StableDiffusionXL,
|
||||
imageBytes,
|
||||
customPrompt: "cyberpunk cityscape",
|
||||
customParameters: customParams
|
||||
);
|
||||
```
|
||||
|
||||
## MAUI Control Features
|
||||
|
||||
### Layout Modes
|
||||
|
||||
```csharp
|
||||
// Image only - no buttons (for custom UI)
|
||||
TransformerView.LayoutMode = TransformerLayoutMode.ImageOnly;
|
||||
|
||||
// Buttons below the image (default)
|
||||
TransformerView.LayoutMode = TransformerLayoutMode.ButtonsBelow;
|
||||
|
||||
// Overlay buttons on the image
|
||||
TransformerView.LayoutMode = TransformerLayoutMode.ButtonsOverlay;
|
||||
|
||||
// Side-by-side source and result
|
||||
TransformerView.LayoutMode = TransformerLayoutMode.SideBySide;
|
||||
```
|
||||
|
||||
### Overlay Button Position
|
||||
|
||||
```csharp
|
||||
// Position overlay buttons at top or bottom
|
||||
TransformerView.OverlayButtonPosition = OverlayButtonPosition.Top;
|
||||
TransformerView.OverlayButtonPosition = OverlayButtonPosition.Bottom; // default
|
||||
```
|
||||
|
||||
### Localization
|
||||
|
||||
Built-in support for 8 languages:
|
||||
|
||||
```csharp
|
||||
// Enable localization
|
||||
TransformerView.UseLocalization = true;
|
||||
|
||||
// Set language
|
||||
ReplicateStrings.CurrentCulture = "es"; // Spanish
|
||||
|
||||
// Custom translations
|
||||
ReplicateStrings.RegisterTranslations("es", new Dictionary<string, string>
|
||||
{
|
||||
[ReplicateStrings.Keys.Transform] = "Convertir",
|
||||
[ReplicateStrings.Keys.GenerateVideo] = "Crear vídeo"
|
||||
});
|
||||
```
|
||||
|
||||
Supported languages: English (en), Spanish (es), French (fr), German (de), Chinese (zh), Japanese (ja), Portuguese (pt), Italian (it)
|
||||
|
||||
### Button Configuration
|
||||
|
||||
Customize button appearance:
|
||||
|
||||
```csharp
|
||||
// Configure individual buttons
|
||||
TransformerView.ButtonConfigs.OverlaySelect.Text = "Choose Image";
|
||||
TransformerView.ButtonConfigs.OverlaySelect.IconText = "📷";
|
||||
TransformerView.ButtonConfigs.OverlayTransform.BackgroundColor = Colors.Purple;
|
||||
|
||||
// Set display mode
|
||||
TransformerView.ButtonConfigs.DefaultDisplayMode = ButtonDisplayMode.Icon; // Icon only
|
||||
TransformerView.ButtonConfigs.DefaultDisplayMode = ButtonDisplayMode.Label; // Text only
|
||||
TransformerView.ButtonConfigs.DefaultDisplayMode = ButtonDisplayMode.Both; // Both (default)
|
||||
```
|
||||
|
||||
## Prediction Tracking
|
||||
|
||||
Track prediction status in real-time:
|
||||
|
||||
```csharp
|
||||
// Enable tracking (enabled by default)
|
||||
var tracker = new PredictionTracker(transformer);
|
||||
|
||||
// Subscribe to events
|
||||
tracker.PredictionStatusChanged += (s, e) =>
|
||||
{
|
||||
Console.WriteLine($"Prediction {e.Prediction.Id}: {e.PreviousStatus} -> {e.NewStatus}");
|
||||
};
|
||||
|
||||
tracker.PredictionCompleted += (s, e) =>
|
||||
{
|
||||
if (e.Succeeded)
|
||||
Console.WriteLine($"Completed: {e.Prediction.Output}");
|
||||
else
|
||||
Console.WriteLine($"Failed: {e.Prediction.Error}");
|
||||
};
|
||||
|
||||
// Access history
|
||||
var history = tracker.History;
|
||||
var pending = tracker.PendingPredictions;
|
||||
```
|
||||
|
||||
## BYOK (Bring Your Own Key)
|
||||
|
||||
Support multi-tenant scenarios where users provide their own API keys:
|
||||
|
||||
```csharp
|
||||
// Inject the factory
|
||||
public class MyService
|
||||
{
|
||||
private readonly IReplicateTransformerFactory _factory;
|
||||
|
||||
public MyService(IReplicateTransformerFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateForUser(string userApiKey, byte[] image)
|
||||
{
|
||||
// Create transformer with user's API key
|
||||
var transformer = _factory.CreateWithToken(userApiKey);
|
||||
|
||||
var result = await transformer.RunPresetAsync(
|
||||
ModelPresets.HiDreamE1,
|
||||
image
|
||||
);
|
||||
|
||||
return result.Output;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Webhook Support
|
||||
|
||||
Use webhooks for long-running predictions:
|
||||
|
||||
```csharp
|
||||
var options = new PredictionOptions
|
||||
{
|
||||
WebhookUrl = "https://your-server.com/webhook/replicate",
|
||||
WebhookEventsFilter = new[] { "completed" },
|
||||
WebhookOnly = true // Don't poll, just return immediately
|
||||
};
|
||||
|
||||
var result = await transformer.RunPresetAsync(
|
||||
ModelPresets.GoogleVeo3Fast,
|
||||
imageBytes,
|
||||
options: options
|
||||
);
|
||||
|
||||
// result.Id can be used to track the prediction
|
||||
// Your webhook will receive the completion notification
|
||||
```
|
||||
|
||||
## Events Reference
|
||||
|
||||
### ReplicateTransformerView Events
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `ImageSelected` | Fired when user selects an image |
|
||||
| `TransformationStarted` | Fired when transformation begins |
|
||||
| `TransformationCompleted` | Fired when transformation succeeds |
|
||||
| `TransformationError` | Fired when transformation fails |
|
||||
| `PredictionTracked` | Fired when prediction status changes |
|
||||
| `PredictionCompleted` | Fired when prediction completes |
|
||||
| `FileSaved` | Fired when result is saved to file |
|
||||
| `FileSaveError` | Fired when file save fails |
|
||||
| `StateChanged` | Fired when control state changes |
|
||||
| `ImageTapped` | Fired when image is tapped |
|
||||
|
||||
### IReplicateTransformer Events
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `PredictionCreated` | Fired immediately when prediction is created (before polling) |
|
||||
|
||||
## Error Handling
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
var result = await transformer.RunPresetAsync(preset, imageBytes);
|
||||
|
||||
if (result.IsFailed)
|
||||
{
|
||||
Console.WriteLine($"Prediction failed: {result.Error}");
|
||||
}
|
||||
else if (result.IsCanceled)
|
||||
{
|
||||
Console.WriteLine("Prediction was canceled");
|
||||
}
|
||||
else if (result.IsSucceeded)
|
||||
{
|
||||
Console.WriteLine($"Success: {result.Output}");
|
||||
}
|
||||
}
|
||||
catch (ReplicateApiException ex)
|
||||
{
|
||||
Console.WriteLine($"API Error {ex.StatusCode}: {ex.Message}");
|
||||
Console.WriteLine($"Response: {ex.ResponseBody}");
|
||||
}
|
||||
catch (ReplicateTransformationException ex)
|
||||
{
|
||||
Console.WriteLine($"Transformation Error: {ex.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- .NET 9.0 or later
|
||||
- For MAUI targets: iOS 15.0+, Android 24+, Windows 10.0.17763+, macOS 15.0+
|
||||
- Replicate API token ([Get one here](https://replicate.com/account/api-tokens))
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Links
|
||||
|
||||
- [GitHub Repository](https://github.com/MarketAlly/Replicate.Maui)
|
||||
- [NuGet Package](https://www.nuget.org/packages/MarketAlly.Replicate.Maui/)
|
||||
- [API Reference](https://github.com/MarketAlly/Replicate.Maui/blob/main/API_Reference.md)
|
||||
- [Replicate Documentation](https://replicate.com/docs)
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests, please use the [GitHub Issues](https://github.com/MarketAlly/Replicate.Maui/issues) page.
|
||||
|
||||
---
|
||||
|
||||
Made with care by [MarketAlly](https://github.com/MarketAlly)
|
||||
4
Replicate.Maui.slnx
Normal file
4
Replicate.Maui.slnx
Normal file
@ -0,0 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="Replicate.Maui/Replicate.Maui.csproj" />
|
||||
<Project Path="Test.Replicate/Test.Replicate.csproj" />
|
||||
</Solution>
|
||||
517
Replicate.Maui/Controls/ButtonConfiguration.cs
Normal file
517
Replicate.Maui/Controls/ButtonConfiguration.cs
Normal file
@ -0,0 +1,517 @@
|
||||
namespace MarketAlly.Replicate.Maui.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Display mode for transformer buttons.
|
||||
/// </summary>
|
||||
public enum ButtonDisplayMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Show only the text label.
|
||||
/// </summary>
|
||||
Label,
|
||||
|
||||
/// <summary>
|
||||
/// Show only the icon (ImageSource or IconText).
|
||||
/// </summary>
|
||||
Icon,
|
||||
|
||||
/// <summary>
|
||||
/// Show both icon and label.
|
||||
/// </summary>
|
||||
Both
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position for overlay buttons in the ImageOverlay layout mode.
|
||||
/// </summary>
|
||||
public enum OverlayButtonPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// Buttons appear at the top of the image.
|
||||
/// </summary>
|
||||
Top,
|
||||
|
||||
/// <summary>
|
||||
/// Buttons appear at the bottom of the image (default).
|
||||
/// </summary>
|
||||
Bottom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for a transformer button's appearance.
|
||||
/// </summary>
|
||||
public class ButtonConfig : BindableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when any configuration property changes.
|
||||
/// </summary>
|
||||
public event EventHandler? ConfigurationChanged;
|
||||
|
||||
public static readonly BindableProperty TextProperty =
|
||||
BindableProperty.Create(nameof(Text), typeof(string), typeof(ButtonConfig), string.Empty,
|
||||
propertyChanged: OnConfigPropertyChanged);
|
||||
|
||||
public static readonly BindableProperty IconTextProperty =
|
||||
BindableProperty.Create(nameof(IconText), typeof(string), typeof(ButtonConfig), string.Empty,
|
||||
propertyChanged: OnConfigPropertyChanged);
|
||||
|
||||
public static readonly BindableProperty ImageSourceProperty =
|
||||
BindableProperty.Create(nameof(ImageSource), typeof(ImageSource), typeof(ButtonConfig), null,
|
||||
propertyChanged: OnConfigPropertyChanged);
|
||||
|
||||
public static readonly BindableProperty DisplayModeProperty =
|
||||
BindableProperty.Create(nameof(DisplayMode), typeof(ButtonDisplayMode?), typeof(ButtonConfig), null,
|
||||
propertyChanged: OnConfigPropertyChanged);
|
||||
|
||||
public static readonly BindableProperty BackgroundColorProperty =
|
||||
BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(ButtonConfig), null,
|
||||
propertyChanged: OnConfigPropertyChanged);
|
||||
|
||||
public static readonly BindableProperty TextColorProperty =
|
||||
BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(ButtonConfig), null,
|
||||
propertyChanged: OnConfigPropertyChanged);
|
||||
|
||||
public static readonly BindableProperty IsVisibleProperty =
|
||||
BindableProperty.Create(nameof(IsVisible), typeof(bool), typeof(ButtonConfig), true,
|
||||
propertyChanged: OnConfigPropertyChanged);
|
||||
|
||||
private static void OnConfigPropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||
{
|
||||
if (bindable is ButtonConfig config)
|
||||
{
|
||||
config.ConfigurationChanged?.Invoke(config, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text label for the button.
|
||||
/// </summary>
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unicode/emoji icon text (e.g., "📸", "🖼").
|
||||
/// Used when ImageSource is null and DisplayMode includes icon.
|
||||
/// </summary>
|
||||
public string IconText
|
||||
{
|
||||
get => (string)GetValue(IconTextProperty);
|
||||
set => SetValue(IconTextProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Image source for the button icon.
|
||||
/// Takes precedence over IconText when set.
|
||||
/// </summary>
|
||||
public ImageSource? ImageSource
|
||||
{
|
||||
get => (ImageSource?)GetValue(ImageSourceProperty);
|
||||
set => SetValue(ImageSourceProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How to display the button content. If null, uses the parent's default display mode.
|
||||
/// </summary>
|
||||
public ButtonDisplayMode? DisplayMode
|
||||
{
|
||||
get => (ButtonDisplayMode?)GetValue(DisplayModeProperty);
|
||||
set => SetValue(DisplayModeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Background color for the button.
|
||||
/// </summary>
|
||||
public Color? BackgroundColor
|
||||
{
|
||||
get => (Color?)GetValue(BackgroundColorProperty);
|
||||
set => SetValue(BackgroundColorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text color for the button.
|
||||
/// </summary>
|
||||
public Color TextColor
|
||||
{
|
||||
get => (Color)GetValue(TextColorProperty);
|
||||
set => SetValue(TextColorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the button is visible.
|
||||
/// </summary>
|
||||
public bool IsVisible
|
||||
{
|
||||
get => (bool)GetValue(IsVisibleProperty);
|
||||
set => SetValue(IsVisibleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display text based on DisplayMode.
|
||||
/// </summary>
|
||||
/// <param name="defaultMode">The default display mode to use if DisplayMode is null.</param>
|
||||
public string GetDisplayText(ButtonDisplayMode defaultMode = ButtonDisplayMode.Both)
|
||||
{
|
||||
var effectiveMode = DisplayMode ?? defaultMode;
|
||||
return effectiveMode switch
|
||||
{
|
||||
ButtonDisplayMode.Label => Text,
|
||||
ButtonDisplayMode.Icon => ImageSource == null ? IconText : string.Empty,
|
||||
ButtonDisplayMode.Both => ImageSource == null ? $"{IconText} {Text}".Trim() : Text,
|
||||
_ => Text
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to show the image based on effective display mode.
|
||||
/// </summary>
|
||||
/// <param name="defaultMode">The default display mode to use if DisplayMode is null.</param>
|
||||
public bool ShouldShowImage(ButtonDisplayMode defaultMode = ButtonDisplayMode.Both)
|
||||
{
|
||||
var effectiveMode = DisplayMode ?? defaultMode;
|
||||
return ImageSource != null && effectiveMode != ButtonDisplayMode.Label;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective image source based on display mode.
|
||||
/// </summary>
|
||||
/// <param name="defaultMode">The default display mode to use if DisplayMode is null.</param>
|
||||
public ImageSource? GetEffectiveImageSource(ButtonDisplayMode defaultMode = ButtonDisplayMode.Both)
|
||||
{
|
||||
return ShouldShowImage(defaultMode) ? ImageSource : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply this configuration to a MAUI Button.
|
||||
/// </summary>
|
||||
/// <param name="button">The button to configure.</param>
|
||||
/// <param name="defaultMode">The default display mode to use if DisplayMode is null.</param>
|
||||
public void ApplyTo(Button button, ButtonDisplayMode defaultMode = ButtonDisplayMode.Both)
|
||||
{
|
||||
button.Text = GetDisplayText(defaultMode);
|
||||
button.ImageSource = GetEffectiveImageSource(defaultMode);
|
||||
|
||||
if (BackgroundColor != null)
|
||||
button.BackgroundColor = BackgroundColor;
|
||||
|
||||
if (TextColor != null)
|
||||
button.TextColor = TextColor;
|
||||
|
||||
button.IsVisible = IsVisible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ButtonConfig with the specified values.
|
||||
/// </summary>
|
||||
public static ButtonConfig Create(string text, string iconText, ImageSource? imageSource = null, ButtonDisplayMode? displayMode = null)
|
||||
{
|
||||
return new ButtonConfig
|
||||
{
|
||||
Text = text,
|
||||
IconText = iconText,
|
||||
ImageSource = imageSource,
|
||||
DisplayMode = displayMode
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a clone of this configuration.
|
||||
/// </summary>
|
||||
public ButtonConfig Clone()
|
||||
{
|
||||
return new ButtonConfig
|
||||
{
|
||||
Text = Text,
|
||||
IconText = IconText,
|
||||
ImageSource = ImageSource,
|
||||
DisplayMode = DisplayMode,
|
||||
BackgroundColor = BackgroundColor,
|
||||
TextColor = TextColor,
|
||||
IsVisible = IsVisible
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predefined button configurations for common transformer buttons.
|
||||
/// </summary>
|
||||
public static class DefaultButtonConfigs
|
||||
{
|
||||
// Source buttons
|
||||
public static ButtonConfig CapturePhoto => new()
|
||||
{
|
||||
Text = "Take",
|
||||
IconText = "📸"
|
||||
};
|
||||
|
||||
public static ButtonConfig PickImage => new()
|
||||
{
|
||||
Text = "Pick",
|
||||
IconText = "🖼"
|
||||
};
|
||||
|
||||
public static ButtonConfig Select => new()
|
||||
{
|
||||
Text = "Select",
|
||||
IconText = "🖼"
|
||||
};
|
||||
|
||||
public static ButtonConfig CaptureVideo => new()
|
||||
{
|
||||
Text = "Record",
|
||||
IconText = "🎬"
|
||||
};
|
||||
|
||||
public static ButtonConfig PickVideo => new()
|
||||
{
|
||||
Text = "Pick Video",
|
||||
IconText = "🎥"
|
||||
};
|
||||
|
||||
// Transform buttons
|
||||
public static ButtonConfig TransformImage => new()
|
||||
{
|
||||
Text = "Transform",
|
||||
IconText = "✨"
|
||||
};
|
||||
|
||||
public static ButtonConfig TransformAnime => new()
|
||||
{
|
||||
Text = "Transform to Anime",
|
||||
IconText = "✨"
|
||||
};
|
||||
|
||||
public static ButtonConfig GenerateVideo => new()
|
||||
{
|
||||
Text = "Generate Video",
|
||||
IconText = "🎬"
|
||||
};
|
||||
|
||||
// Result buttons
|
||||
public static ButtonConfig Clear => new()
|
||||
{
|
||||
Text = "Clear",
|
||||
IconText = "🗑"
|
||||
};
|
||||
|
||||
public static ButtonConfig Redo => new()
|
||||
{
|
||||
Text = "Redo",
|
||||
IconText = "↺"
|
||||
};
|
||||
|
||||
public static ButtonConfig Reset => new()
|
||||
{
|
||||
Text = "Reset",
|
||||
IconText = "↺"
|
||||
};
|
||||
|
||||
public static ButtonConfig BackToImage => new()
|
||||
{
|
||||
Text = "Image",
|
||||
IconText = "🖼"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Container for all transformer button configurations.
|
||||
/// Allows grouping and customizing button appearance in a single bindable object.
|
||||
/// </summary>
|
||||
public class TransformerButtonConfigs : BindableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when any button configuration changes.
|
||||
/// </summary>
|
||||
public event EventHandler? ConfigurationChanged;
|
||||
|
||||
#region Bindable Properties
|
||||
|
||||
public static readonly BindableProperty DefaultDisplayModeProperty =
|
||||
BindableProperty.Create(nameof(DefaultDisplayMode), typeof(ButtonDisplayMode), typeof(TransformerButtonConfigs),
|
||||
ButtonDisplayMode.Both, propertyChanged: OnPropertyChanged);
|
||||
|
||||
// Overlay buttons
|
||||
public static readonly BindableProperty OverlaySelectProperty =
|
||||
BindableProperty.Create(nameof(OverlaySelect), typeof(ButtonConfig), typeof(TransformerButtonConfigs),
|
||||
null, propertyChanged: OnButtonConfigChanged);
|
||||
|
||||
public static readonly BindableProperty OverlayTransformProperty =
|
||||
BindableProperty.Create(nameof(OverlayTransform), typeof(ButtonConfig), typeof(TransformerButtonConfigs),
|
||||
null, propertyChanged: OnButtonConfigChanged);
|
||||
|
||||
// SideBySide buttons
|
||||
public static readonly BindableProperty SideBySideCaptureProperty =
|
||||
BindableProperty.Create(nameof(SideBySideCapture), typeof(ButtonConfig), typeof(TransformerButtonConfigs),
|
||||
null, propertyChanged: OnButtonConfigChanged);
|
||||
|
||||
public static readonly BindableProperty SideBySidePickProperty =
|
||||
BindableProperty.Create(nameof(SideBySidePick), typeof(ButtonConfig), typeof(TransformerButtonConfigs),
|
||||
null, propertyChanged: OnButtonConfigChanged);
|
||||
|
||||
public static readonly BindableProperty SideBySideTransformProperty =
|
||||
BindableProperty.Create(nameof(SideBySideTransform), typeof(ButtonConfig), typeof(TransformerButtonConfigs),
|
||||
null, propertyChanged: OnButtonConfigChanged);
|
||||
|
||||
public static readonly BindableProperty SideBySideClearProperty =
|
||||
BindableProperty.Create(nameof(SideBySideClear), typeof(ButtonConfig), typeof(TransformerButtonConfigs),
|
||||
null, propertyChanged: OnButtonConfigChanged);
|
||||
|
||||
public static readonly BindableProperty SideBySideRedoProperty =
|
||||
BindableProperty.Create(nameof(SideBySideRedo), typeof(ButtonConfig), typeof(TransformerButtonConfigs),
|
||||
null, propertyChanged: OnButtonConfigChanged);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Accessors
|
||||
|
||||
/// <summary>
|
||||
/// Default display mode for all buttons. Individual buttons can override this.
|
||||
/// </summary>
|
||||
public ButtonDisplayMode DefaultDisplayMode
|
||||
{
|
||||
get => (ButtonDisplayMode)GetValue(DefaultDisplayModeProperty);
|
||||
set => SetValue(DefaultDisplayModeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the overlay select button.
|
||||
/// </summary>
|
||||
public ButtonConfig OverlaySelect
|
||||
{
|
||||
get => (ButtonConfig?)GetValue(OverlaySelectProperty) ?? CreateDefaultOverlaySelect();
|
||||
set => SetValue(OverlaySelectProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the overlay transform button.
|
||||
/// </summary>
|
||||
public ButtonConfig OverlayTransform
|
||||
{
|
||||
get => (ButtonConfig?)GetValue(OverlayTransformProperty) ?? CreateDefaultOverlayTransform();
|
||||
set => SetValue(OverlayTransformProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the side-by-side capture button.
|
||||
/// </summary>
|
||||
public ButtonConfig SideBySideCapture
|
||||
{
|
||||
get => (ButtonConfig?)GetValue(SideBySideCaptureProperty) ?? CreateDefaultSideBySideCapture();
|
||||
set => SetValue(SideBySideCaptureProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the side-by-side pick button.
|
||||
/// </summary>
|
||||
public ButtonConfig SideBySidePick
|
||||
{
|
||||
get => (ButtonConfig?)GetValue(SideBySidePickProperty) ?? CreateDefaultSideBySidePick();
|
||||
set => SetValue(SideBySidePickProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the side-by-side transform button.
|
||||
/// </summary>
|
||||
public ButtonConfig SideBySideTransform
|
||||
{
|
||||
get => (ButtonConfig?)GetValue(SideBySideTransformProperty) ?? CreateDefaultSideBySideTransform();
|
||||
set => SetValue(SideBySideTransformProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the side-by-side clear button.
|
||||
/// </summary>
|
||||
public ButtonConfig SideBySideClear
|
||||
{
|
||||
get => (ButtonConfig?)GetValue(SideBySideClearProperty) ?? CreateDefaultSideBySideClear();
|
||||
set => SetValue(SideBySideClearProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the side-by-side redo button.
|
||||
/// </summary>
|
||||
public ButtonConfig SideBySideRedo
|
||||
{
|
||||
get => (ButtonConfig?)GetValue(SideBySideRedoProperty) ?? CreateDefaultSideBySideRedo();
|
||||
set => SetValue(SideBySideRedoProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Default Factory Methods
|
||||
|
||||
private static ButtonConfig CreateDefaultOverlaySelect() => new() { Text = "Select", IconText = "🖼" };
|
||||
private static ButtonConfig CreateDefaultOverlayTransform() => new() { Text = "Transform", IconText = "✨" };
|
||||
private static ButtonConfig CreateDefaultSideBySideCapture() => new() { Text = "Take", IconText = "📸" };
|
||||
private static ButtonConfig CreateDefaultSideBySidePick() => new() { Text = "Pick", IconText = "🖼" };
|
||||
private static ButtonConfig CreateDefaultSideBySideTransform() => new() { Text = "Transform", IconText = "✨" };
|
||||
private static ButtonConfig CreateDefaultSideBySideClear() => new() { Text = "Clear", IconText = "🗑" };
|
||||
private static ButtonConfig CreateDefaultSideBySideRedo() => new() { Text = "Redo", IconText = "↺" };
|
||||
|
||||
#endregion
|
||||
|
||||
#region Change Handlers
|
||||
|
||||
private static void OnPropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||
{
|
||||
if (bindable is TransformerButtonConfigs configs)
|
||||
{
|
||||
configs.ConfigurationChanged?.Invoke(configs, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnButtonConfigChanged(BindableObject bindable, object oldValue, object newValue)
|
||||
{
|
||||
if (bindable is TransformerButtonConfigs configs)
|
||||
{
|
||||
// Unsubscribe from old config
|
||||
if (oldValue is ButtonConfig oldConfig)
|
||||
{
|
||||
oldConfig.ConfigurationChanged -= configs.OnChildConfigurationChanged;
|
||||
}
|
||||
|
||||
// Subscribe to new config
|
||||
if (newValue is ButtonConfig newConfig)
|
||||
{
|
||||
newConfig.ConfigurationChanged += configs.OnChildConfigurationChanged;
|
||||
}
|
||||
|
||||
configs.ConfigurationChanged?.Invoke(configs, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChildConfigurationChanged(object? sender, EventArgs e)
|
||||
{
|
||||
ConfigurationChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Apply a button configuration to a button control.
|
||||
/// </summary>
|
||||
public void ApplyConfig(ButtonConfig config, Button button)
|
||||
{
|
||||
config.ApplyTo(button, DefaultDisplayMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a default TransformerButtonConfigs instance with all defaults applied.
|
||||
/// </summary>
|
||||
public static TransformerButtonConfigs CreateDefault()
|
||||
{
|
||||
return new TransformerButtonConfigs
|
||||
{
|
||||
DefaultDisplayMode = ButtonDisplayMode.Both,
|
||||
OverlaySelect = CreateDefaultOverlaySelect(),
|
||||
OverlayTransform = CreateDefaultOverlayTransform(),
|
||||
SideBySideCapture = CreateDefaultSideBySideCapture(),
|
||||
SideBySidePick = CreateDefaultSideBySidePick(),
|
||||
SideBySideTransform = CreateDefaultSideBySideTransform(),
|
||||
SideBySideClear = CreateDefaultSideBySideClear(),
|
||||
SideBySideRedo = CreateDefaultSideBySideRedo()
|
||||
};
|
||||
}
|
||||
}
|
||||
10
Replicate.Maui/Controls/ConfigurableButton.xaml
Normal file
10
Replicate.Maui/Controls/ConfigurableButton.xaml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="MarketAlly.Replicate.Maui.Controls.ConfigurableButton">
|
||||
|
||||
<Button x:Name="InnerButton"
|
||||
Clicked="OnButtonClicked"
|
||||
ContentLayout="Left,6"/>
|
||||
|
||||
</ContentView>
|
||||
230
Replicate.Maui/Controls/ConfigurableButton.xaml.cs
Normal file
230
Replicate.Maui/Controls/ConfigurableButton.xaml.cs
Normal file
@ -0,0 +1,230 @@
|
||||
namespace MarketAlly.Replicate.Maui.Controls;
|
||||
|
||||
public partial class ConfigurableButton : ContentView
|
||||
{
|
||||
public static readonly BindableProperty TextProperty =
|
||||
BindableProperty.Create(nameof(Text), typeof(string), typeof(ConfigurableButton), string.Empty,
|
||||
propertyChanged: OnAppearancePropertyChanged);
|
||||
|
||||
public static readonly BindableProperty IconTextProperty =
|
||||
BindableProperty.Create(nameof(IconText), typeof(string), typeof(ConfigurableButton), string.Empty,
|
||||
propertyChanged: OnAppearancePropertyChanged);
|
||||
|
||||
public static readonly BindableProperty IconSourceProperty =
|
||||
BindableProperty.Create(nameof(IconSource), typeof(ImageSource), typeof(ConfigurableButton), null,
|
||||
propertyChanged: OnAppearancePropertyChanged);
|
||||
|
||||
public static readonly BindableProperty DisplayModeProperty =
|
||||
BindableProperty.Create(nameof(DisplayMode), typeof(ButtonDisplayMode), typeof(ConfigurableButton), ButtonDisplayMode.Both,
|
||||
propertyChanged: OnAppearancePropertyChanged);
|
||||
|
||||
public static readonly BindableProperty ButtonBackgroundColorProperty =
|
||||
BindableProperty.Create(nameof(ButtonBackgroundColor), typeof(Color), typeof(ConfigurableButton), null,
|
||||
propertyChanged: OnStylePropertyChanged);
|
||||
|
||||
public static readonly BindableProperty ButtonTextColorProperty =
|
||||
BindableProperty.Create(nameof(ButtonTextColor), typeof(Color), typeof(ConfigurableButton), Colors.White,
|
||||
propertyChanged: OnStylePropertyChanged);
|
||||
|
||||
public static readonly BindableProperty CornerRadiusProperty =
|
||||
BindableProperty.Create(nameof(CornerRadius), typeof(int), typeof(ConfigurableButton), 0,
|
||||
propertyChanged: OnStylePropertyChanged);
|
||||
|
||||
public static readonly BindableProperty ButtonHeightRequestProperty =
|
||||
BindableProperty.Create(nameof(ButtonHeightRequest), typeof(double), typeof(ConfigurableButton), 44.0,
|
||||
propertyChanged: OnStylePropertyChanged);
|
||||
|
||||
public static readonly BindableProperty FontSizeProperty =
|
||||
BindableProperty.Create(nameof(FontSize), typeof(double), typeof(ConfigurableButton), 14.0,
|
||||
propertyChanged: OnStylePropertyChanged);
|
||||
|
||||
public static readonly BindableProperty CommandProperty =
|
||||
BindableProperty.Create(nameof(Command), typeof(System.Windows.Input.ICommand), typeof(ConfigurableButton), null,
|
||||
propertyChanged: OnCommandPropertyChanged);
|
||||
|
||||
public static readonly BindableProperty CommandParameterProperty =
|
||||
BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(ConfigurableButton), null,
|
||||
propertyChanged: OnCommandPropertyChanged);
|
||||
|
||||
/// <summary>
|
||||
/// The text label for the button.
|
||||
/// </summary>
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unicode/emoji icon text (e.g., "📸", "🖼").
|
||||
/// Used when IconSource is null and DisplayMode includes icon.
|
||||
/// </summary>
|
||||
public string IconText
|
||||
{
|
||||
get => (string)GetValue(IconTextProperty);
|
||||
set => SetValue(IconTextProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Image source for the button icon.
|
||||
/// Takes precedence over IconText when set.
|
||||
/// </summary>
|
||||
public ImageSource? IconSource
|
||||
{
|
||||
get => (ImageSource?)GetValue(IconSourceProperty);
|
||||
set => SetValue(IconSourceProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How to display the button content (Label, Icon, or Both).
|
||||
/// </summary>
|
||||
public ButtonDisplayMode DisplayMode
|
||||
{
|
||||
get => (ButtonDisplayMode)GetValue(DisplayModeProperty);
|
||||
set => SetValue(DisplayModeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Background color for the button.
|
||||
/// </summary>
|
||||
public Color? ButtonBackgroundColor
|
||||
{
|
||||
get => (Color?)GetValue(ButtonBackgroundColorProperty);
|
||||
set => SetValue(ButtonBackgroundColorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text color for the button.
|
||||
/// </summary>
|
||||
public Color ButtonTextColor
|
||||
{
|
||||
get => (Color)GetValue(ButtonTextColorProperty);
|
||||
set => SetValue(ButtonTextColorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Corner radius for the button.
|
||||
/// </summary>
|
||||
public int CornerRadius
|
||||
{
|
||||
get => (int)GetValue(CornerRadiusProperty);
|
||||
set => SetValue(CornerRadiusProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Height request for the button.
|
||||
/// </summary>
|
||||
public double ButtonHeightRequest
|
||||
{
|
||||
get => (double)GetValue(ButtonHeightRequestProperty);
|
||||
set => SetValue(ButtonHeightRequestProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Font size for button text.
|
||||
/// </summary>
|
||||
public double FontSize
|
||||
{
|
||||
get => (double)GetValue(FontSizeProperty);
|
||||
set => SetValue(FontSizeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Command to execute when button is clicked.
|
||||
/// </summary>
|
||||
public System.Windows.Input.ICommand? Command
|
||||
{
|
||||
get => (System.Windows.Input.ICommand?)GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameter to pass to the command.
|
||||
/// </summary>
|
||||
public object? CommandParameter
|
||||
{
|
||||
get => GetValue(CommandParameterProperty);
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when the button is clicked.
|
||||
/// </summary>
|
||||
public event EventHandler? Clicked;
|
||||
|
||||
public ConfigurableButton()
|
||||
{
|
||||
InitializeComponent();
|
||||
UpdateButtonAppearance();
|
||||
UpdateButtonStyle();
|
||||
}
|
||||
|
||||
private static void OnAppearancePropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||
{
|
||||
if (bindable is ConfigurableButton button)
|
||||
button.UpdateButtonAppearance();
|
||||
}
|
||||
|
||||
private static void OnStylePropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||
{
|
||||
if (bindable is ConfigurableButton button)
|
||||
button.UpdateButtonStyle();
|
||||
}
|
||||
|
||||
private static void OnCommandPropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||
{
|
||||
if (bindable is ConfigurableButton button)
|
||||
{
|
||||
button.InnerButton.Command = button.Command;
|
||||
button.InnerButton.CommandParameter = button.CommandParameter;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateButtonAppearance()
|
||||
{
|
||||
string displayText = DisplayMode switch
|
||||
{
|
||||
ButtonDisplayMode.Label => Text,
|
||||
ButtonDisplayMode.Icon => IconSource == null ? IconText : string.Empty,
|
||||
ButtonDisplayMode.Both => IconSource == null ? $"{IconText} {Text}".Trim() : Text,
|
||||
_ => Text
|
||||
};
|
||||
|
||||
InnerButton.Text = displayText;
|
||||
|
||||
// Set image source if using Icon or Both mode with an ImageSource
|
||||
if (IconSource != null && DisplayMode != ButtonDisplayMode.Label)
|
||||
{
|
||||
InnerButton.ImageSource = IconSource;
|
||||
}
|
||||
else
|
||||
{
|
||||
InnerButton.ImageSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateButtonStyle()
|
||||
{
|
||||
if (ButtonBackgroundColor != null)
|
||||
InnerButton.BackgroundColor = ButtonBackgroundColor;
|
||||
|
||||
InnerButton.TextColor = ButtonTextColor;
|
||||
InnerButton.CornerRadius = CornerRadius;
|
||||
InnerButton.HeightRequest = ButtonHeightRequest;
|
||||
InnerButton.FontSize = FontSize;
|
||||
}
|
||||
|
||||
private void OnButtonClicked(object? sender, EventArgs e)
|
||||
{
|
||||
Clicked?.Invoke(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable the button.
|
||||
/// </summary>
|
||||
public new bool IsEnabled
|
||||
{
|
||||
get => InnerButton.IsEnabled;
|
||||
set => InnerButton.IsEnabled = value;
|
||||
}
|
||||
}
|
||||
444
Replicate.Maui/Controls/ReplicateTransformerView.xaml
Normal file
444
Replicate.Maui/Controls/ReplicateTransformerView.xaml
Normal file
@ -0,0 +1,444 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
x:Class="MarketAlly.Replicate.Maui.Controls.ReplicateTransformerView">
|
||||
|
||||
<Grid x:Name="RootGrid" RowDefinitions="*,Auto,Auto" Padding="0" RowSpacing="8">
|
||||
|
||||
<!-- ============================================== -->
|
||||
<!-- SINGLE IMAGE LAYOUT (Default, Overlay, Below) -->
|
||||
<!-- ============================================== -->
|
||||
<Grid x:Name="SingleImageContainer" Grid.Row="0">
|
||||
<Border x:Name="MainImageBorder"
|
||||
StrokeShape="RoundRectangle 12"
|
||||
Stroke="{AppThemeBinding Light=#E0E0E0, Dark=#404040}"
|
||||
StrokeThickness="2"
|
||||
Background="{AppThemeBinding Light=#F5F5F5, Dark=#1A1A1A}"
|
||||
MinimumHeightRequest="200">
|
||||
<Grid>
|
||||
<!-- Placeholder when no image -->
|
||||
<VerticalStackLayout x:Name="PlaceholderView"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="8">
|
||||
<Label Text="📷"
|
||||
FontSize="48"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label x:Name="PlaceholderLabel"
|
||||
Text="Select or capture an image"
|
||||
FontSize="14"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Source Image -->
|
||||
<Image x:Name="SourceImage"
|
||||
Aspect="AspectFit"
|
||||
IsVisible="False"/>
|
||||
|
||||
<!-- Result Image -->
|
||||
<Image x:Name="ResultImage"
|
||||
Aspect="AspectFit"
|
||||
IsVisible="False"/>
|
||||
|
||||
<!-- Video Player (for video results) -->
|
||||
<toolkit:MediaElement x:Name="VideoPlayer"
|
||||
Aspect="AspectFit"
|
||||
ShouldAutoPlay="True"
|
||||
ShouldLoopPlayback="True"
|
||||
IsVisible="False"/>
|
||||
|
||||
<!-- Play Button Overlay (shows on result image when video is available) -->
|
||||
<Border x:Name="PlayButtonOverlay"
|
||||
IsVisible="False"
|
||||
Background="Transparent"
|
||||
StrokeThickness="0">
|
||||
<Button x:Name="PlayVideoButton"
|
||||
Text="▶"
|
||||
FontSize="48"
|
||||
BackgroundColor="#88000000"
|
||||
TextColor="White"
|
||||
CornerRadius="40"
|
||||
HeightRequest="80"
|
||||
WidthRequest="80"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
Clicked="OnPlayVideoClicked"/>
|
||||
</Border>
|
||||
|
||||
<!-- Processing Overlay -->
|
||||
<Border x:Name="ProcessingOverlay"
|
||||
IsVisible="False"
|
||||
Background="#80000000"
|
||||
StrokeThickness="0">
|
||||
<VerticalStackLayout VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="16">
|
||||
<ActivityIndicator IsRunning="True"
|
||||
Color="White"
|
||||
HeightRequest="48"
|
||||
WidthRequest="48"/>
|
||||
<Label x:Name="ProcessingLabel"
|
||||
Text="Transforming..."
|
||||
TextColor="White"
|
||||
FontSize="16"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label x:Name="ProcessingSubLabel"
|
||||
Text="This may take a moment"
|
||||
TextColor="#CCCCCC"
|
||||
FontSize="12"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<!-- Error Overlay -->
|
||||
<Border x:Name="ErrorOverlay"
|
||||
IsVisible="False"
|
||||
Background="#80000000"
|
||||
StrokeThickness="0">
|
||||
<VerticalStackLayout VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="12"
|
||||
Padding="20">
|
||||
<Label Text="⚠"
|
||||
FontSize="36"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label x:Name="ErrorLabel"
|
||||
Text="An error occurred"
|
||||
TextColor="White"
|
||||
FontSize="14"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"/>
|
||||
<Button x:Name="DismissErrorButton"
|
||||
Text="Dismiss"
|
||||
BackgroundColor="#666666"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="36"
|
||||
Clicked="OnDismissErrorClicked"/>
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<!-- OVERLAY BUTTONS (flush at bottom of image) -->
|
||||
<Grid x:Name="OverlayButtonsContainer"
|
||||
IsVisible="False"
|
||||
VerticalOptions="End"
|
||||
ColumnDefinitions="*,*"
|
||||
ColumnSpacing="0">
|
||||
<Button x:Name="OverlayPickButton"
|
||||
Grid.Column="0"
|
||||
Text="🖼 Select"
|
||||
FontSize="14"
|
||||
BackgroundColor="#CC007AFF"
|
||||
TextColor="White"
|
||||
CornerRadius="0"
|
||||
HeightRequest="48"
|
||||
Clicked="OnPickImageClicked"/>
|
||||
<Button x:Name="OverlayTransformButton"
|
||||
Grid.Column="1"
|
||||
Text="✨ Transform"
|
||||
FontSize="14"
|
||||
BackgroundColor="#CCFF2D55"
|
||||
TextColor="White"
|
||||
CornerRadius="0"
|
||||
HeightRequest="48"
|
||||
Clicked="OnOverlayTransformClicked"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- ============================================== -->
|
||||
<!-- SIDE BY SIDE LAYOUT -->
|
||||
<!-- ============================================== -->
|
||||
<Grid x:Name="SideBySideContainer"
|
||||
Grid.Row="0"
|
||||
IsVisible="False"
|
||||
ColumnDefinitions="*,*"
|
||||
ColumnSpacing="8">
|
||||
|
||||
<!-- Source Image Side -->
|
||||
<Border Grid.Column="0"
|
||||
StrokeShape="RoundRectangle 12"
|
||||
Stroke="{AppThemeBinding Light=#E0E0E0, Dark=#404040}"
|
||||
StrokeThickness="2"
|
||||
Background="{AppThemeBinding Light=#F5F5F5, Dark=#1A1A1A}"
|
||||
MinimumHeightRequest="200">
|
||||
<Grid>
|
||||
<!-- Placeholder for source -->
|
||||
<VerticalStackLayout x:Name="SideBySidePlaceholder"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="8">
|
||||
<Label Text="📷"
|
||||
FontSize="36"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label x:Name="SideBySidePlaceholderLabel"
|
||||
Text="Select image"
|
||||
FontSize="12"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Source Image -->
|
||||
<Image x:Name="SideBySideSourceImage"
|
||||
Aspect="AspectFit"
|
||||
IsVisible="False"/>
|
||||
|
||||
<!-- Label -->
|
||||
<Label Text="SOURCE"
|
||||
FontSize="10"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{AppThemeBinding Light=#999999, Dark=#666666}"
|
||||
VerticalOptions="Start"
|
||||
HorizontalOptions="Start"
|
||||
Margin="8,4"/>
|
||||
|
||||
<!-- Source Side Buttons (Take/Pick) -->
|
||||
<Grid x:Name="SideBySideSourceButtons"
|
||||
VerticalOptions="End"
|
||||
ColumnDefinitions="*,*"
|
||||
ColumnSpacing="0">
|
||||
<Button x:Name="SideBySideCaptureButton"
|
||||
Grid.Column="0"
|
||||
Text="📸 Take"
|
||||
FontSize="12"
|
||||
BackgroundColor="#CC007AFF"
|
||||
TextColor="White"
|
||||
CornerRadius="0"
|
||||
HeightRequest="40"
|
||||
Clicked="OnCapturePhotoClicked"/>
|
||||
<Button x:Name="SideBySidePickButton"
|
||||
Grid.Column="1"
|
||||
Text="🖼 Pick"
|
||||
FontSize="12"
|
||||
BackgroundColor="#CC34C759"
|
||||
TextColor="White"
|
||||
CornerRadius="0"
|
||||
HeightRequest="40"
|
||||
Clicked="OnPickImageClicked"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Result Image Side -->
|
||||
<Border Grid.Column="1"
|
||||
StrokeShape="RoundRectangle 12"
|
||||
Stroke="{AppThemeBinding Light=#E0E0E0, Dark=#404040}"
|
||||
StrokeThickness="2"
|
||||
Background="{AppThemeBinding Light=#F5F5F5, Dark=#1A1A1A}"
|
||||
MinimumHeightRequest="200">
|
||||
<Grid>
|
||||
<!-- Placeholder for result -->
|
||||
<VerticalStackLayout x:Name="SideBySideResultPlaceholder"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="8">
|
||||
<Label Text="✨"
|
||||
FontSize="36"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label Text="Result"
|
||||
FontSize="12"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Result Image -->
|
||||
<Image x:Name="SideBySideResultImage"
|
||||
Aspect="AspectFit"
|
||||
IsVisible="False"/>
|
||||
|
||||
<!-- Video Player (for video results in SideBySide) -->
|
||||
<toolkit:MediaElement x:Name="SideBySideVideoPlayer"
|
||||
Aspect="AspectFit"
|
||||
ShouldAutoPlay="True"
|
||||
ShouldLoopPlayback="True"
|
||||
IsVisible="False"/>
|
||||
|
||||
<!-- Play Button Overlay for SideBySide -->
|
||||
<Border x:Name="SideBySidePlayButtonOverlay"
|
||||
IsVisible="False"
|
||||
Background="Transparent"
|
||||
StrokeThickness="0">
|
||||
<Button x:Name="SideBySidePlayVideoButton"
|
||||
Text="▶"
|
||||
FontSize="32"
|
||||
BackgroundColor="#88000000"
|
||||
TextColor="White"
|
||||
CornerRadius="30"
|
||||
HeightRequest="60"
|
||||
WidthRequest="60"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
Clicked="OnSideBySidePlayVideoClicked"/>
|
||||
</Border>
|
||||
|
||||
<!-- Processing indicator for result side -->
|
||||
<Border x:Name="SideBySideProcessingOverlay"
|
||||
IsVisible="False"
|
||||
Background="#80000000"
|
||||
StrokeThickness="0">
|
||||
<VerticalStackLayout VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="8">
|
||||
<ActivityIndicator IsRunning="True"
|
||||
Color="White"
|
||||
HeightRequest="32"
|
||||
WidthRequest="32"/>
|
||||
<Label x:Name="SideBySideProcessingLabel"
|
||||
Text="Processing..."
|
||||
TextColor="White"
|
||||
FontSize="12"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<!-- Label -->
|
||||
<Label Text="RESULT"
|
||||
FontSize="10"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{AppThemeBinding Light=#999999, Dark=#666666}"
|
||||
VerticalOptions="Start"
|
||||
HorizontalOptions="Start"
|
||||
Margin="8,4"/>
|
||||
|
||||
<!-- Result Side Buttons (Clear/Redo) -->
|
||||
<Grid x:Name="SideBySideResultButtons"
|
||||
VerticalOptions="End"
|
||||
ColumnDefinitions="*,*"
|
||||
ColumnSpacing="0"
|
||||
IsVisible="False">
|
||||
<Button x:Name="SideBySideClearButton"
|
||||
Grid.Column="0"
|
||||
Text="🗑 Clear"
|
||||
FontSize="12"
|
||||
BackgroundColor="#CC8E8E93"
|
||||
TextColor="White"
|
||||
CornerRadius="0"
|
||||
HeightRequest="40"
|
||||
Clicked="OnSideBySideClearClicked"/>
|
||||
<Button x:Name="SideBySideRedoButton"
|
||||
Grid.Column="1"
|
||||
Text="↺ Redo"
|
||||
FontSize="12"
|
||||
BackgroundColor="#CCFF9500"
|
||||
TextColor="White"
|
||||
CornerRadius="0"
|
||||
HeightRequest="40"
|
||||
Clicked="OnSideBySideRedoClicked"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- ============================================== -->
|
||||
<!-- SIDE BY SIDE TRANSFORM BUTTON -->
|
||||
<!-- ============================================== -->
|
||||
<Button x:Name="SideBySideTransformButton"
|
||||
Grid.Row="1"
|
||||
Text="✨ Transform"
|
||||
FontSize="16"
|
||||
BackgroundColor="{AppThemeBinding Light=#FF2D55, Dark=#FF375F}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="48"
|
||||
IsVisible="False"
|
||||
Clicked="OnSideBySideTransformClicked"/>
|
||||
|
||||
<!-- ============================================== -->
|
||||
<!-- BUTTONS BELOW (Source Buttons Row) -->
|
||||
<!-- ============================================== -->
|
||||
<Grid Grid.Row="1"
|
||||
x:Name="SourceButtonsGrid"
|
||||
ColumnDefinitions="*,*,*,*"
|
||||
ColumnSpacing="8">
|
||||
|
||||
<Button x:Name="CapturePhotoButton"
|
||||
Grid.Column="0"
|
||||
Text="📸"
|
||||
FontSize="20"
|
||||
ToolTipProperties.Text="Take Photo"
|
||||
BackgroundColor="{AppThemeBinding Light=#007AFF, Dark=#0A84FF}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="44"
|
||||
Clicked="OnCapturePhotoClicked"/>
|
||||
|
||||
<Button x:Name="PickImageButton"
|
||||
Grid.Column="1"
|
||||
Text="🖼"
|
||||
FontSize="20"
|
||||
ToolTipProperties.Text="Pick Image"
|
||||
BackgroundColor="{AppThemeBinding Light=#34C759, Dark=#30D158}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="44"
|
||||
Clicked="OnPickImageClicked"/>
|
||||
|
||||
<Button x:Name="CaptureVideoButton"
|
||||
Grid.Column="2"
|
||||
Text="🎬"
|
||||
FontSize="20"
|
||||
ToolTipProperties.Text="Record Video"
|
||||
BackgroundColor="{AppThemeBinding Light=#FF9500, Dark=#FF9F0A}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="44"
|
||||
IsVisible="False"
|
||||
Clicked="OnCaptureVideoClicked"/>
|
||||
|
||||
<Button x:Name="PickVideoButton"
|
||||
Grid.Column="3"
|
||||
Text="🎥"
|
||||
FontSize="20"
|
||||
ToolTipProperties.Text="Pick Video"
|
||||
BackgroundColor="{AppThemeBinding Light=#AF52DE, Dark=#BF5AF2}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="44"
|
||||
IsVisible="False"
|
||||
Clicked="OnPickVideoClicked"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Transform Buttons Row -->
|
||||
<Grid Grid.Row="2"
|
||||
ColumnDefinitions="*,*,Auto"
|
||||
ColumnSpacing="8"
|
||||
x:Name="TransformButtonsGrid"
|
||||
IsVisible="False">
|
||||
|
||||
<Button x:Name="TransformAnimeButton"
|
||||
Grid.Column="0"
|
||||
Text="Transform to Anime"
|
||||
FontSize="14"
|
||||
BackgroundColor="{AppThemeBinding Light=#FF2D55, Dark=#FF375F}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="44"
|
||||
Clicked="OnTransformAnimeClicked"/>
|
||||
|
||||
<Button x:Name="TransformVideoButton"
|
||||
Grid.Column="1"
|
||||
Text="Generate Video"
|
||||
FontSize="14"
|
||||
BackgroundColor="{AppThemeBinding Light=#5856D6, Dark=#5E5CE6}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="44"
|
||||
Clicked="OnTransformVideoClicked"/>
|
||||
|
||||
<Button x:Name="ResetButton"
|
||||
Grid.Column="2"
|
||||
Text="↺"
|
||||
FontSize="20"
|
||||
ToolTipProperties.Text="Reset"
|
||||
BackgroundColor="{AppThemeBinding Light=#8E8E93, Dark=#636366}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="44"
|
||||
WidthRequest="44"
|
||||
Clicked="OnResetClicked"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentView>
|
||||
2448
Replicate.Maui/Controls/ReplicateTransformerView.xaml.cs
Normal file
2448
Replicate.Maui/Controls/ReplicateTransformerView.xaml.cs
Normal file
File diff suppressed because it is too large
Load Diff
177
Replicate.Maui/EventArgs.cs
Normal file
177
Replicate.Maui/EventArgs.cs
Normal file
@ -0,0 +1,177 @@
|
||||
namespace MarketAlly.Replicate.Maui;
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when a transformation starts.
|
||||
/// </summary>
|
||||
public class TransformationStartedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of transformation being started.
|
||||
/// </summary>
|
||||
public TransformationType Type { get; }
|
||||
|
||||
public TransformationStartedEventArgs(TransformationType type) => Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when a transformation completes successfully.
|
||||
/// </summary>
|
||||
public class TransformationCompletedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of transformation that completed.
|
||||
/// </summary>
|
||||
public TransformationType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the result output.
|
||||
/// </summary>
|
||||
public string? ResultUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full prediction result.
|
||||
/// </summary>
|
||||
public PredictionResult Result { get; }
|
||||
|
||||
public TransformationCompletedEventArgs(TransformationType type, PredictionResult result)
|
||||
{
|
||||
Type = type;
|
||||
Result = result;
|
||||
ResultUrl = result.Output;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when a transformation fails.
|
||||
/// </summary>
|
||||
public class TransformationErrorEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of transformation that failed.
|
||||
/// </summary>
|
||||
public TransformationType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The exception that occurred.
|
||||
/// </summary>
|
||||
public Exception Error { get; }
|
||||
|
||||
public TransformationErrorEventArgs(TransformationType type, Exception error)
|
||||
{
|
||||
Type = type;
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when an image is selected.
|
||||
/// </summary>
|
||||
public class ImageSelectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The selected image bytes.
|
||||
/// </summary>
|
||||
public byte[] ImageBytes { get; }
|
||||
|
||||
public ImageSelectedEventArgs(byte[] imageBytes) => ImageBytes = imageBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when an image is tapped.
|
||||
/// </summary>
|
||||
public class ImageTappedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this is the source image (true) or result image (false).
|
||||
/// </summary>
|
||||
public bool IsSourceImage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the result, if applicable.
|
||||
/// </summary>
|
||||
public string? ResultUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The image bytes, if available.
|
||||
/// </summary>
|
||||
public byte[]? ImageBytes { get; }
|
||||
|
||||
public ImageTappedEventArgs(bool isSourceImage, string? resultUrl = null, byte[]? imageBytes = null)
|
||||
{
|
||||
IsSourceImage = isSourceImage;
|
||||
ResultUrl = resultUrl;
|
||||
ImageBytes = imageBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when a file is saved successfully.
|
||||
/// </summary>
|
||||
public class FileSavedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The full path to the saved file.
|
||||
/// </summary>
|
||||
public string FilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of file saved (Image or Video).
|
||||
/// </summary>
|
||||
public TransformationType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the saved file in bytes.
|
||||
/// </summary>
|
||||
public long FileSizeBytes { get; }
|
||||
|
||||
public FileSavedEventArgs(string filePath, TransformationType type, long fileSizeBytes)
|
||||
{
|
||||
FilePath = filePath;
|
||||
Type = type;
|
||||
FileSizeBytes = fileSizeBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when a file save fails.
|
||||
/// </summary>
|
||||
public class FileSaveErrorEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of file that failed to save.
|
||||
/// </summary>
|
||||
public TransformationType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The exception that occurred.
|
||||
/// </summary>
|
||||
public Exception Error { get; }
|
||||
|
||||
public FileSaveErrorEventArgs(TransformationType type, Exception error)
|
||||
{
|
||||
Type = type;
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when the transformer state changes.
|
||||
/// </summary>
|
||||
public class StateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The previous state of the control.
|
||||
/// </summary>
|
||||
public TransformerState PreviousState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The new/current state of the control.
|
||||
/// </summary>
|
||||
public TransformerState NewState { get; }
|
||||
|
||||
public StateChangedEventArgs(TransformerState previousState, TransformerState newState)
|
||||
{
|
||||
PreviousState = previousState;
|
||||
NewState = newState;
|
||||
}
|
||||
}
|
||||
23
Replicate.Maui/Exceptions.cs
Normal file
23
Replicate.Maui/Exceptions.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Net;
|
||||
|
||||
namespace MarketAlly.Replicate.Maui
|
||||
{
|
||||
public class ReplicateApiException : Exception
|
||||
{
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
public string ResponseBody { get; }
|
||||
|
||||
public ReplicateApiException(string message, HttpStatusCode statusCode, string responseBody)
|
||||
: base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
ResponseBody = responseBody;
|
||||
}
|
||||
}
|
||||
|
||||
public class ReplicateTransformationException : Exception
|
||||
{
|
||||
public ReplicateTransformationException(string message) : base(message) { }
|
||||
public ReplicateTransformationException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
}
|
||||
426
Replicate.Maui/IReplicateTransformer.cs
Normal file
426
Replicate.Maui/IReplicateTransformer.cs
Normal file
@ -0,0 +1,426 @@
|
||||
namespace MarketAlly.Replicate.Maui
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of transformation being performed.
|
||||
/// </summary>
|
||||
public enum TransformationType
|
||||
{
|
||||
Image,
|
||||
Video
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of a prediction in the Replicate API.
|
||||
/// </summary>
|
||||
public enum PredictionStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Prediction is starting up.
|
||||
/// </summary>
|
||||
Starting,
|
||||
|
||||
/// <summary>
|
||||
/// Prediction is actively processing.
|
||||
/// </summary>
|
||||
Processing,
|
||||
|
||||
/// <summary>
|
||||
/// Prediction completed successfully.
|
||||
/// </summary>
|
||||
Succeeded,
|
||||
|
||||
/// <summary>
|
||||
/// Prediction failed with an error.
|
||||
/// </summary>
|
||||
Failed,
|
||||
|
||||
/// <summary>
|
||||
/// Prediction was canceled by the user.
|
||||
/// </summary>
|
||||
Canceled,
|
||||
|
||||
/// <summary>
|
||||
/// Unknown or unrecognized status.
|
||||
/// </summary>
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper extensions for PredictionStatus.
|
||||
/// </summary>
|
||||
public static class PredictionStatusExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert a status string from the API to a PredictionStatus enum.
|
||||
/// </summary>
|
||||
public static PredictionStatus ToPredictionStatus(this string? status)
|
||||
{
|
||||
return status?.ToLowerInvariant() switch
|
||||
{
|
||||
"starting" => PredictionStatus.Starting,
|
||||
"processing" => PredictionStatus.Processing,
|
||||
"succeeded" => PredictionStatus.Succeeded,
|
||||
"failed" => PredictionStatus.Failed,
|
||||
"canceled" => PredictionStatus.Canceled,
|
||||
_ => PredictionStatus.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a PredictionStatus enum to the API string value.
|
||||
/// </summary>
|
||||
public static string ToApiString(this PredictionStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
PredictionStatus.Starting => "starting",
|
||||
PredictionStatus.Processing => "processing",
|
||||
PredictionStatus.Succeeded => "succeeded",
|
||||
PredictionStatus.Failed => "failed",
|
||||
PredictionStatus.Canceled => "canceled",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this status represents a completed prediction (succeeded, failed, or canceled).
|
||||
/// </summary>
|
||||
public static bool IsCompleted(this PredictionStatus status)
|
||||
{
|
||||
return status is PredictionStatus.Succeeded or PredictionStatus.Failed or PredictionStatus.Canceled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this status represents a pending prediction (starting or processing).
|
||||
/// </summary>
|
||||
public static bool IsPending(this PredictionStatus status)
|
||||
{
|
||||
return status is PredictionStatus.Starting or PredictionStatus.Processing;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when a prediction is created (before polling starts).
|
||||
/// </summary>
|
||||
public class PredictionCreatedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The initial prediction result with ID and starting status.
|
||||
/// </summary>
|
||||
public PredictionResult Prediction { get; }
|
||||
|
||||
public PredictionCreatedEventArgs(PredictionResult prediction)
|
||||
{
|
||||
Prediction = prediction;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for Replicate API image and video transformations.
|
||||
/// </summary>
|
||||
public interface IReplicateTransformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when a prediction is created (before polling starts).
|
||||
/// Use this to track predictions immediately when they're submitted.
|
||||
/// </summary>
|
||||
event EventHandler<PredictionCreatedEventArgs>? PredictionCreated;
|
||||
|
||||
/// <summary>
|
||||
/// Transform an image to anime style using raw bytes.
|
||||
/// </summary>
|
||||
/// <param name="imageBytes">The image bytes to transform.</param>
|
||||
/// <param name="customPrompt">Optional custom prompt override.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Prediction result with output URL and metadata.</returns>
|
||||
Task<PredictionResult> TransformToAnimeAsync(
|
||||
byte[] imageBytes,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Transform an image to anime style using base64 encoded string.
|
||||
/// </summary>
|
||||
/// <param name="base64Image">Base64 encoded image (with or without data URI prefix).</param>
|
||||
/// <param name="customPrompt">Optional custom prompt override.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Prediction result with output URL and metadata.</returns>
|
||||
Task<PredictionResult> TransformToAnimeFromBase64Async(
|
||||
string base64Image,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Transform an image to anime style from a URL.
|
||||
/// </summary>
|
||||
/// <param name="imageUrl">URL of the image to transform.</param>
|
||||
/// <param name="customPrompt">Optional custom prompt override.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Prediction result with output URL and metadata.</returns>
|
||||
Task<PredictionResult> TransformToAnimeFromUrlAsync(
|
||||
string imageUrl,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a video from an image using raw bytes.
|
||||
/// </summary>
|
||||
/// <param name="imageBytes">The image bytes to animate.</param>
|
||||
/// <param name="customPrompt">Optional custom prompt override.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Prediction result with output URL and metadata.</returns>
|
||||
Task<PredictionResult> TransformToVideoAsync(
|
||||
byte[] imageBytes,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a video from a base64 encoded image.
|
||||
/// </summary>
|
||||
/// <param name="base64Image">Base64 encoded image (with or without data URI prefix).</param>
|
||||
/// <param name="customPrompt">Optional custom prompt override.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Prediction result with output URL and metadata.</returns>
|
||||
Task<PredictionResult> TransformToVideoFromBase64Async(
|
||||
string base64Image,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a video from an image URL.
|
||||
/// </summary>
|
||||
/// <param name="imageUrl">URL of the image to animate.</param>
|
||||
/// <param name="customPrompt">Optional custom prompt override.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Prediction result with output URL and metadata.</returns>
|
||||
Task<PredictionResult> TransformToVideoFromUrlAsync(
|
||||
string imageUrl,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel a running prediction.
|
||||
/// </summary>
|
||||
/// <param name="predictionId">The prediction ID to cancel.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task CancelPredictionAsync(string predictionId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the status of a prediction.
|
||||
/// </summary>
|
||||
/// <param name="predictionId">The prediction ID to check.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Current prediction status and result if completed.</returns>
|
||||
Task<PredictionResult> GetPredictionAsync(string predictionId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Run a prediction using a model preset with image bytes.
|
||||
/// </summary>
|
||||
/// <param name="preset">The model preset to use.</param>
|
||||
/// <param name="imageBytes">The image bytes.</param>
|
||||
/// <param name="customPrompt">Optional custom prompt override.</param>
|
||||
/// <param name="customParameters">Optional parameters to override preset defaults.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<PredictionResult> RunPresetAsync(
|
||||
ModelPreset preset,
|
||||
byte[] imageBytes,
|
||||
string? customPrompt = null,
|
||||
Dictionary<string, object>? customParameters = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Run a prediction using a model preset with base64 image.
|
||||
/// </summary>
|
||||
/// <param name="preset">The model preset to use.</param>
|
||||
/// <param name="base64Image">Base64 encoded image (with or without data URI prefix).</param>
|
||||
/// <param name="customPrompt">Optional custom prompt override.</param>
|
||||
/// <param name="customParameters">Optional parameters to override preset defaults.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<PredictionResult> RunPresetFromBase64Async(
|
||||
ModelPreset preset,
|
||||
string base64Image,
|
||||
string? customPrompt = null,
|
||||
Dictionary<string, object>? customParameters = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Run a prediction using a model preset with image URL.
|
||||
/// </summary>
|
||||
/// <param name="preset">The model preset to use.</param>
|
||||
/// <param name="imageUrl">URL of the image.</param>
|
||||
/// <param name="customPrompt">Optional custom prompt override.</param>
|
||||
/// <param name="customParameters">Optional parameters to override preset defaults.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<PredictionResult> RunPresetFromUrlAsync(
|
||||
ModelPreset preset,
|
||||
string imageUrl,
|
||||
string? customPrompt = null,
|
||||
Dictionary<string, object>? customParameters = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Run a text-only prediction using a model preset (no image input).
|
||||
/// </summary>
|
||||
/// <param name="preset">The model preset to use.</param>
|
||||
/// <param name="prompt">The prompt for generation.</param>
|
||||
/// <param name="customParameters">Optional parameters to override preset defaults.</param>
|
||||
/// <param name="options">Optional prediction options (webhook, sync mode, etc.).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<PredictionResult> RunPresetTextOnlyAsync(
|
||||
ModelPreset preset,
|
||||
string prompt,
|
||||
Dictionary<string, object>? customParameters = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for creating a prediction.
|
||||
/// </summary>
|
||||
public class PredictionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Webhook URL to receive prediction updates.
|
||||
/// </summary>
|
||||
public string? WebhookUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filter which events trigger webhook calls.
|
||||
/// Options: "start", "output", "logs", "completed"
|
||||
/// </summary>
|
||||
public string[]? WebhookEventsFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Use sync mode - wait for result up to specified seconds (1-60).
|
||||
/// If null, uses async mode with polling.
|
||||
/// </summary>
|
||||
public int? SyncModeWaitSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true and using sync mode, don't poll - just return the initial response.
|
||||
/// Useful when using webhooks.
|
||||
/// </summary>
|
||||
public bool WebhookOnly { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a prediction.
|
||||
/// </summary>
|
||||
public class PredictionResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique prediction identifier.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Current status as string: starting, processing, succeeded, failed, canceled.
|
||||
/// Consider using StatusEnum for type-safe comparisons.
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Current status as a strongly-typed enum.
|
||||
/// </summary>
|
||||
public PredictionStatus StatusEnum => Status.ToPredictionStatus();
|
||||
|
||||
/// <summary>
|
||||
/// Output URL(s) if succeeded.
|
||||
/// </summary>
|
||||
public string? Output { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All output URLs if multiple outputs.
|
||||
/// </summary>
|
||||
public string[]? Outputs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if failed.
|
||||
/// </summary>
|
||||
public string? Error { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Prediction metrics (predict_time, total_time).
|
||||
/// </summary>
|
||||
public PredictionMetrics? Metrics { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the prediction was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the prediction started processing.
|
||||
/// </summary>
|
||||
public DateTimeOffset? StartedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the prediction completed.
|
||||
/// </summary>
|
||||
public DateTimeOffset? CompletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to cancel the prediction.
|
||||
/// </summary>
|
||||
public string? CancelUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the prediction completed (succeeded, failed, or canceled).
|
||||
/// </summary>
|
||||
public bool IsCompleted => StatusEnum.IsCompleted();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the prediction succeeded.
|
||||
/// </summary>
|
||||
public bool IsSucceeded => StatusEnum == PredictionStatus.Succeeded;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the prediction is pending (starting or processing).
|
||||
/// </summary>
|
||||
public bool IsPending => StatusEnum.IsPending();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the prediction failed.
|
||||
/// </summary>
|
||||
public bool IsFailed => StatusEnum == PredictionStatus.Failed;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the prediction was canceled.
|
||||
/// </summary>
|
||||
public bool IsCanceled => StatusEnum == PredictionStatus.Canceled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prediction performance metrics.
|
||||
/// </summary>
|
||||
public class PredictionMetrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Time spent running the model (seconds).
|
||||
/// </summary>
|
||||
public double? PredictTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total time including queue wait (seconds).
|
||||
/// </summary>
|
||||
public double? TotalTime { get; set; }
|
||||
}
|
||||
}
|
||||
608
Replicate.Maui/Localization/ReplicateStrings.cs
Normal file
608
Replicate.Maui/Localization/ReplicateStrings.cs
Normal file
@ -0,0 +1,608 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MarketAlly.Replicate.Maui.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Provides localized strings for the Replicate.Maui controls.
|
||||
/// Supports English, Spanish, French, German, Chinese, Japanese, Portuguese, and Italian.
|
||||
/// Custom translations can be added or overridden using RegisterTranslations.
|
||||
/// </summary>
|
||||
public static class ReplicateStrings
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> _translations = new();
|
||||
private static string _currentCulture = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
|
||||
|
||||
/// <summary>
|
||||
/// String keys used throughout the control.
|
||||
/// </summary>
|
||||
public static class Keys
|
||||
{
|
||||
// Button labels
|
||||
public const string Take = nameof(Take);
|
||||
public const string Pick = nameof(Pick);
|
||||
public const string Select = nameof(Select);
|
||||
public const string Transform = nameof(Transform);
|
||||
public const string Clear = nameof(Clear);
|
||||
public const string Redo = nameof(Redo);
|
||||
public const string Reset = nameof(Reset);
|
||||
public const string Cancel = nameof(Cancel);
|
||||
public const string Image = nameof(Image);
|
||||
public const string GenerateVideo = nameof(GenerateVideo);
|
||||
public const string Record = nameof(Record);
|
||||
public const string PickVideo = nameof(PickVideo);
|
||||
|
||||
// Status messages
|
||||
public const string Transforming = nameof(Transforming);
|
||||
public const string GeneratingVideo = nameof(GeneratingVideo);
|
||||
public const string TransformingImage = nameof(TransformingImage);
|
||||
public const string Processing = nameof(Processing);
|
||||
public const string PleaseWait = nameof(PleaseWait);
|
||||
public const string MayTakeAMoment = nameof(MayTakeAMoment);
|
||||
public const string MayTakeSeveralMinutes = nameof(MayTakeSeveralMinutes);
|
||||
public const string TransformationComplete = nameof(TransformationComplete);
|
||||
public const string VideoGenerationComplete = nameof(VideoGenerationComplete);
|
||||
public const string TransformationCancelled = nameof(TransformationCancelled);
|
||||
|
||||
// Placeholders
|
||||
public const string TapToSelectImage = nameof(TapToSelectImage);
|
||||
public const string SourceImage = nameof(SourceImage);
|
||||
public const string Result = nameof(Result);
|
||||
public const string NoImageSelected = nameof(NoImageSelected);
|
||||
|
||||
// Errors
|
||||
public const string Error = nameof(Error);
|
||||
public const string Dismiss = nameof(Dismiss);
|
||||
public const string TransformationFailed = nameof(TransformationFailed);
|
||||
public const string NoTransformerInitialized = nameof(NoTransformerInitialized);
|
||||
|
||||
// Prediction status
|
||||
public const string Starting = nameof(Starting);
|
||||
public const string Succeeded = nameof(Succeeded);
|
||||
public const string Failed = nameof(Failed);
|
||||
public const string Canceled = nameof(Canceled);
|
||||
public const string Unknown = nameof(Unknown);
|
||||
public const string Pending = nameof(Pending);
|
||||
}
|
||||
|
||||
static ReplicateStrings()
|
||||
{
|
||||
InitializeDefaultTranslations();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current culture for localization.
|
||||
/// Defaults to the system's current UI culture.
|
||||
/// </summary>
|
||||
public static string CurrentCulture
|
||||
{
|
||||
get => _currentCulture;
|
||||
set => _currentCulture = value?.ToLowerInvariant() ?? "en";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the current culture from a CultureInfo.
|
||||
/// </summary>
|
||||
public static void SetCulture(CultureInfo culture)
|
||||
{
|
||||
CurrentCulture = culture.TwoLetterISOLanguageName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the current culture to match the system's current UI culture.
|
||||
/// </summary>
|
||||
public static void UseSystemCulture()
|
||||
{
|
||||
CurrentCulture = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a localized string by key.
|
||||
/// Falls back to English if the key is not found in the current culture.
|
||||
/// </summary>
|
||||
/// <param name="key">The string key.</param>
|
||||
/// <returns>The localized string, or the key itself if not found.</returns>
|
||||
public static string Get(string key)
|
||||
{
|
||||
// Try current culture
|
||||
if (_translations.TryGetValue(_currentCulture, out var currentStrings) &&
|
||||
currentStrings.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// Fall back to English
|
||||
if (_translations.TryGetValue("en", out var englishStrings) &&
|
||||
englishStrings.TryGetValue(key, out var englishValue))
|
||||
{
|
||||
return englishValue;
|
||||
}
|
||||
|
||||
// Return key as last resort
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a localized string by key for a specific culture.
|
||||
/// </summary>
|
||||
/// <param name="key">The string key.</param>
|
||||
/// <param name="culture">The culture code (e.g., "en", "es", "fr").</param>
|
||||
/// <returns>The localized string, or falls back to English, or the key itself.</returns>
|
||||
public static string Get(string key, string culture)
|
||||
{
|
||||
culture = culture?.ToLowerInvariant() ?? "en";
|
||||
|
||||
if (_translations.TryGetValue(culture, out var strings) &&
|
||||
strings.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// Fall back to English
|
||||
if (_translations.TryGetValue("en", out var englishStrings) &&
|
||||
englishStrings.TryGetValue(key, out var englishValue))
|
||||
{
|
||||
return englishValue;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register or override translations for a specific culture.
|
||||
/// </summary>
|
||||
/// <param name="culture">The culture code (e.g., "en", "es", "fr").</param>
|
||||
/// <param name="translations">Dictionary of key-value translations.</param>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // Add custom Spanish translations
|
||||
/// ReplicateStrings.RegisterTranslations("es", new Dictionary<string, string>
|
||||
/// {
|
||||
/// [ReplicateStrings.Keys.Transform] = "Convertir",
|
||||
/// [ReplicateStrings.Keys.GenerateVideo] = "Crear vídeo"
|
||||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static void RegisterTranslations(string culture, Dictionary<string, string> translations)
|
||||
{
|
||||
culture = culture?.ToLowerInvariant() ?? "en";
|
||||
|
||||
_translations.AddOrUpdate(
|
||||
culture,
|
||||
translations,
|
||||
(key, existing) =>
|
||||
{
|
||||
foreach (var kvp in translations)
|
||||
{
|
||||
existing[kvp.Key] = kvp.Value;
|
||||
}
|
||||
return existing;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register or override a single translation for a specific culture.
|
||||
/// </summary>
|
||||
/// <param name="culture">The culture code.</param>
|
||||
/// <param name="key">The string key.</param>
|
||||
/// <param name="value">The translated value.</param>
|
||||
public static void RegisterTranslation(string culture, string key, string value)
|
||||
{
|
||||
culture = culture?.ToLowerInvariant() ?? "en";
|
||||
|
||||
_translations.AddOrUpdate(
|
||||
culture,
|
||||
new Dictionary<string, string> { [key] = value },
|
||||
(k, existing) =>
|
||||
{
|
||||
existing[key] = value;
|
||||
return existing;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a culture has any translations registered.
|
||||
/// </summary>
|
||||
public static bool HasCulture(string culture)
|
||||
{
|
||||
return _translations.ContainsKey(culture?.ToLowerInvariant() ?? "en");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all supported culture codes.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> SupportedCultures => _translations.Keys;
|
||||
|
||||
/// <summary>
|
||||
/// Clear all custom translations and restore defaults.
|
||||
/// </summary>
|
||||
public static void ResetToDefaults()
|
||||
{
|
||||
_translations.Clear();
|
||||
InitializeDefaultTranslations();
|
||||
}
|
||||
|
||||
private static void InitializeDefaultTranslations()
|
||||
{
|
||||
// English (en)
|
||||
RegisterTranslations("en", new Dictionary<string, string>
|
||||
{
|
||||
// Button labels
|
||||
[Keys.Take] = "Take",
|
||||
[Keys.Pick] = "Pick",
|
||||
[Keys.Select] = "Select",
|
||||
[Keys.Transform] = "Transform",
|
||||
[Keys.Clear] = "Clear",
|
||||
[Keys.Redo] = "Redo",
|
||||
[Keys.Reset] = "Reset",
|
||||
[Keys.Cancel] = "Cancel",
|
||||
[Keys.Image] = "Image",
|
||||
[Keys.GenerateVideo] = "Generate Video",
|
||||
[Keys.Record] = "Record",
|
||||
[Keys.PickVideo] = "Pick Video",
|
||||
|
||||
// Status messages
|
||||
[Keys.Transforming] = "Transforming...",
|
||||
[Keys.GeneratingVideo] = "Generating video...",
|
||||
[Keys.TransformingImage] = "Transforming image...",
|
||||
[Keys.Processing] = "Processing",
|
||||
[Keys.PleaseWait] = "Please wait",
|
||||
[Keys.MayTakeAMoment] = "This may take a moment",
|
||||
[Keys.MayTakeSeveralMinutes] = "This may take several minutes",
|
||||
[Keys.TransformationComplete] = "Transformation complete",
|
||||
[Keys.VideoGenerationComplete] = "Video generation complete",
|
||||
[Keys.TransformationCancelled] = "Transformation cancelled",
|
||||
|
||||
// Placeholders
|
||||
[Keys.TapToSelectImage] = "Tap to select image",
|
||||
[Keys.SourceImage] = "Source",
|
||||
[Keys.Result] = "Result",
|
||||
[Keys.NoImageSelected] = "No image selected",
|
||||
|
||||
// Errors
|
||||
[Keys.Error] = "Error",
|
||||
[Keys.Dismiss] = "Dismiss",
|
||||
[Keys.TransformationFailed] = "Transformation failed",
|
||||
[Keys.NoTransformerInitialized] = "No transformer initialized",
|
||||
|
||||
// Prediction status
|
||||
[Keys.Starting] = "Starting",
|
||||
[Keys.Succeeded] = "Succeeded",
|
||||
[Keys.Failed] = "Failed",
|
||||
[Keys.Canceled] = "Canceled",
|
||||
[Keys.Unknown] = "Unknown",
|
||||
[Keys.Pending] = "Pending"
|
||||
});
|
||||
|
||||
// Spanish (es)
|
||||
RegisterTranslations("es", new Dictionary<string, string>
|
||||
{
|
||||
[Keys.Take] = "Tomar",
|
||||
[Keys.Pick] = "Elegir",
|
||||
[Keys.Select] = "Seleccionar",
|
||||
[Keys.Transform] = "Transformar",
|
||||
[Keys.Clear] = "Borrar",
|
||||
[Keys.Redo] = "Rehacer",
|
||||
[Keys.Reset] = "Reiniciar",
|
||||
[Keys.Cancel] = "Cancelar",
|
||||
[Keys.Image] = "Imagen",
|
||||
[Keys.GenerateVideo] = "Generar vídeo",
|
||||
[Keys.Record] = "Grabar",
|
||||
[Keys.PickVideo] = "Elegir vídeo",
|
||||
|
||||
[Keys.Transforming] = "Transformando...",
|
||||
[Keys.GeneratingVideo] = "Generando vídeo...",
|
||||
[Keys.TransformingImage] = "Transformando imagen...",
|
||||
[Keys.Processing] = "Procesando",
|
||||
[Keys.PleaseWait] = "Por favor espere",
|
||||
[Keys.MayTakeAMoment] = "Esto puede tardar un momento",
|
||||
[Keys.MayTakeSeveralMinutes] = "Esto puede tardar varios minutos",
|
||||
[Keys.TransformationComplete] = "Transformación completada",
|
||||
[Keys.VideoGenerationComplete] = "Generación de vídeo completada",
|
||||
[Keys.TransformationCancelled] = "Transformación cancelada",
|
||||
|
||||
[Keys.TapToSelectImage] = "Toque para seleccionar imagen",
|
||||
[Keys.SourceImage] = "Origen",
|
||||
[Keys.Result] = "Resultado",
|
||||
[Keys.NoImageSelected] = "Ninguna imagen seleccionada",
|
||||
|
||||
[Keys.Error] = "Error",
|
||||
[Keys.Dismiss] = "Cerrar",
|
||||
[Keys.TransformationFailed] = "La transformación falló",
|
||||
[Keys.NoTransformerInitialized] = "Transformador no inicializado",
|
||||
|
||||
[Keys.Starting] = "Iniciando",
|
||||
[Keys.Succeeded] = "Exitoso",
|
||||
[Keys.Failed] = "Fallido",
|
||||
[Keys.Canceled] = "Cancelado",
|
||||
[Keys.Unknown] = "Desconocido",
|
||||
[Keys.Pending] = "Pendiente"
|
||||
});
|
||||
|
||||
// French (fr)
|
||||
RegisterTranslations("fr", new Dictionary<string, string>
|
||||
{
|
||||
[Keys.Take] = "Prendre",
|
||||
[Keys.Pick] = "Choisir",
|
||||
[Keys.Select] = "Sélectionner",
|
||||
[Keys.Transform] = "Transformer",
|
||||
[Keys.Clear] = "Effacer",
|
||||
[Keys.Redo] = "Refaire",
|
||||
[Keys.Reset] = "Réinitialiser",
|
||||
[Keys.Cancel] = "Annuler",
|
||||
[Keys.Image] = "Image",
|
||||
[Keys.GenerateVideo] = "Générer vidéo",
|
||||
[Keys.Record] = "Enregistrer",
|
||||
[Keys.PickVideo] = "Choisir vidéo",
|
||||
|
||||
[Keys.Transforming] = "Transformation...",
|
||||
[Keys.GeneratingVideo] = "Génération de vidéo...",
|
||||
[Keys.TransformingImage] = "Transformation de l'image...",
|
||||
[Keys.Processing] = "Traitement",
|
||||
[Keys.PleaseWait] = "Veuillez patienter",
|
||||
[Keys.MayTakeAMoment] = "Cela peut prendre un moment",
|
||||
[Keys.MayTakeSeveralMinutes] = "Cela peut prendre plusieurs minutes",
|
||||
[Keys.TransformationComplete] = "Transformation terminée",
|
||||
[Keys.VideoGenerationComplete] = "Génération vidéo terminée",
|
||||
[Keys.TransformationCancelled] = "Transformation annulée",
|
||||
|
||||
[Keys.TapToSelectImage] = "Appuyez pour sélectionner une image",
|
||||
[Keys.SourceImage] = "Source",
|
||||
[Keys.Result] = "Résultat",
|
||||
[Keys.NoImageSelected] = "Aucune image sélectionnée",
|
||||
|
||||
[Keys.Error] = "Erreur",
|
||||
[Keys.Dismiss] = "Fermer",
|
||||
[Keys.TransformationFailed] = "La transformation a échoué",
|
||||
[Keys.NoTransformerInitialized] = "Transformateur non initialisé",
|
||||
|
||||
[Keys.Starting] = "Démarrage",
|
||||
[Keys.Succeeded] = "Réussi",
|
||||
[Keys.Failed] = "Échoué",
|
||||
[Keys.Canceled] = "Annulé",
|
||||
[Keys.Unknown] = "Inconnu",
|
||||
[Keys.Pending] = "En attente"
|
||||
});
|
||||
|
||||
// German (de)
|
||||
RegisterTranslations("de", new Dictionary<string, string>
|
||||
{
|
||||
[Keys.Take] = "Aufnehmen",
|
||||
[Keys.Pick] = "Auswählen",
|
||||
[Keys.Select] = "Wählen",
|
||||
[Keys.Transform] = "Transformieren",
|
||||
[Keys.Clear] = "Löschen",
|
||||
[Keys.Redo] = "Wiederholen",
|
||||
[Keys.Reset] = "Zurücksetzen",
|
||||
[Keys.Cancel] = "Abbrechen",
|
||||
[Keys.Image] = "Bild",
|
||||
[Keys.GenerateVideo] = "Video erstellen",
|
||||
[Keys.Record] = "Aufzeichnen",
|
||||
[Keys.PickVideo] = "Video auswählen",
|
||||
|
||||
[Keys.Transforming] = "Transformiere...",
|
||||
[Keys.GeneratingVideo] = "Video wird erstellt...",
|
||||
[Keys.TransformingImage] = "Bild wird transformiert...",
|
||||
[Keys.Processing] = "Verarbeitung",
|
||||
[Keys.PleaseWait] = "Bitte warten",
|
||||
[Keys.MayTakeAMoment] = "Dies kann einen Moment dauern",
|
||||
[Keys.MayTakeSeveralMinutes] = "Dies kann mehrere Minuten dauern",
|
||||
[Keys.TransformationComplete] = "Transformation abgeschlossen",
|
||||
[Keys.VideoGenerationComplete] = "Videogenerierung abgeschlossen",
|
||||
[Keys.TransformationCancelled] = "Transformation abgebrochen",
|
||||
|
||||
[Keys.TapToSelectImage] = "Tippen um Bild auszuwählen",
|
||||
[Keys.SourceImage] = "Quelle",
|
||||
[Keys.Result] = "Ergebnis",
|
||||
[Keys.NoImageSelected] = "Kein Bild ausgewählt",
|
||||
|
||||
[Keys.Error] = "Fehler",
|
||||
[Keys.Dismiss] = "Schließen",
|
||||
[Keys.TransformationFailed] = "Transformation fehlgeschlagen",
|
||||
[Keys.NoTransformerInitialized] = "Transformer nicht initialisiert",
|
||||
|
||||
[Keys.Starting] = "Startet",
|
||||
[Keys.Succeeded] = "Erfolgreich",
|
||||
[Keys.Failed] = "Fehlgeschlagen",
|
||||
[Keys.Canceled] = "Abgebrochen",
|
||||
[Keys.Unknown] = "Unbekannt",
|
||||
[Keys.Pending] = "Ausstehend"
|
||||
});
|
||||
|
||||
// Chinese Simplified (zh)
|
||||
RegisterTranslations("zh", new Dictionary<string, string>
|
||||
{
|
||||
[Keys.Take] = "拍照",
|
||||
[Keys.Pick] = "选择",
|
||||
[Keys.Select] = "选择",
|
||||
[Keys.Transform] = "转换",
|
||||
[Keys.Clear] = "清除",
|
||||
[Keys.Redo] = "重做",
|
||||
[Keys.Reset] = "重置",
|
||||
[Keys.Cancel] = "取消",
|
||||
[Keys.Image] = "图片",
|
||||
[Keys.GenerateVideo] = "生成视频",
|
||||
[Keys.Record] = "录制",
|
||||
[Keys.PickVideo] = "选择视频",
|
||||
|
||||
[Keys.Transforming] = "正在转换...",
|
||||
[Keys.GeneratingVideo] = "正在生成视频...",
|
||||
[Keys.TransformingImage] = "正在转换图片...",
|
||||
[Keys.Processing] = "处理中",
|
||||
[Keys.PleaseWait] = "请稍候",
|
||||
[Keys.MayTakeAMoment] = "这可能需要一点时间",
|
||||
[Keys.MayTakeSeveralMinutes] = "这可能需要几分钟",
|
||||
[Keys.TransformationComplete] = "转换完成",
|
||||
[Keys.VideoGenerationComplete] = "视频生成完成",
|
||||
[Keys.TransformationCancelled] = "转换已取消",
|
||||
|
||||
[Keys.TapToSelectImage] = "点击选择图片",
|
||||
[Keys.SourceImage] = "原图",
|
||||
[Keys.Result] = "结果",
|
||||
[Keys.NoImageSelected] = "未选择图片",
|
||||
|
||||
[Keys.Error] = "错误",
|
||||
[Keys.Dismiss] = "关闭",
|
||||
[Keys.TransformationFailed] = "转换失败",
|
||||
[Keys.NoTransformerInitialized] = "转换器未初始化",
|
||||
|
||||
[Keys.Starting] = "启动中",
|
||||
[Keys.Succeeded] = "成功",
|
||||
[Keys.Failed] = "失败",
|
||||
[Keys.Canceled] = "已取消",
|
||||
[Keys.Unknown] = "未知",
|
||||
[Keys.Pending] = "等待中"
|
||||
});
|
||||
|
||||
// Japanese (ja)
|
||||
RegisterTranslations("ja", new Dictionary<string, string>
|
||||
{
|
||||
[Keys.Take] = "撮影",
|
||||
[Keys.Pick] = "選択",
|
||||
[Keys.Select] = "選択",
|
||||
[Keys.Transform] = "変換",
|
||||
[Keys.Clear] = "クリア",
|
||||
[Keys.Redo] = "やり直し",
|
||||
[Keys.Reset] = "リセット",
|
||||
[Keys.Cancel] = "キャンセル",
|
||||
[Keys.Image] = "画像",
|
||||
[Keys.GenerateVideo] = "動画生成",
|
||||
[Keys.Record] = "録画",
|
||||
[Keys.PickVideo] = "動画を選択",
|
||||
|
||||
[Keys.Transforming] = "変換中...",
|
||||
[Keys.GeneratingVideo] = "動画を生成中...",
|
||||
[Keys.TransformingImage] = "画像を変換中...",
|
||||
[Keys.Processing] = "処理中",
|
||||
[Keys.PleaseWait] = "お待ちください",
|
||||
[Keys.MayTakeAMoment] = "しばらくお待ちください",
|
||||
[Keys.MayTakeSeveralMinutes] = "数分かかる場合があります",
|
||||
[Keys.TransformationComplete] = "変換完了",
|
||||
[Keys.VideoGenerationComplete] = "動画生成完了",
|
||||
[Keys.TransformationCancelled] = "変換がキャンセルされました",
|
||||
|
||||
[Keys.TapToSelectImage] = "タップして画像を選択",
|
||||
[Keys.SourceImage] = "元画像",
|
||||
[Keys.Result] = "結果",
|
||||
[Keys.NoImageSelected] = "画像が選択されていません",
|
||||
|
||||
[Keys.Error] = "エラー",
|
||||
[Keys.Dismiss] = "閉じる",
|
||||
[Keys.TransformationFailed] = "変換に失敗しました",
|
||||
[Keys.NoTransformerInitialized] = "トランスフォーマーが初期化されていません",
|
||||
|
||||
[Keys.Starting] = "開始中",
|
||||
[Keys.Succeeded] = "成功",
|
||||
[Keys.Failed] = "失敗",
|
||||
[Keys.Canceled] = "キャンセル済み",
|
||||
[Keys.Unknown] = "不明",
|
||||
[Keys.Pending] = "保留中"
|
||||
});
|
||||
|
||||
// Portuguese (pt)
|
||||
RegisterTranslations("pt", new Dictionary<string, string>
|
||||
{
|
||||
[Keys.Take] = "Tirar",
|
||||
[Keys.Pick] = "Escolher",
|
||||
[Keys.Select] = "Selecionar",
|
||||
[Keys.Transform] = "Transformar",
|
||||
[Keys.Clear] = "Limpar",
|
||||
[Keys.Redo] = "Refazer",
|
||||
[Keys.Reset] = "Redefinir",
|
||||
[Keys.Cancel] = "Cancelar",
|
||||
[Keys.Image] = "Imagem",
|
||||
[Keys.GenerateVideo] = "Gerar vídeo",
|
||||
[Keys.Record] = "Gravar",
|
||||
[Keys.PickVideo] = "Escolher vídeo",
|
||||
|
||||
[Keys.Transforming] = "Transformando...",
|
||||
[Keys.GeneratingVideo] = "Gerando vídeo...",
|
||||
[Keys.TransformingImage] = "Transformando imagem...",
|
||||
[Keys.Processing] = "Processando",
|
||||
[Keys.PleaseWait] = "Por favor aguarde",
|
||||
[Keys.MayTakeAMoment] = "Isso pode levar um momento",
|
||||
[Keys.MayTakeSeveralMinutes] = "Isso pode levar vários minutos",
|
||||
[Keys.TransformationComplete] = "Transformação concluída",
|
||||
[Keys.VideoGenerationComplete] = "Geração de vídeo concluída",
|
||||
[Keys.TransformationCancelled] = "Transformação cancelada",
|
||||
|
||||
[Keys.TapToSelectImage] = "Toque para selecionar imagem",
|
||||
[Keys.SourceImage] = "Origem",
|
||||
[Keys.Result] = "Resultado",
|
||||
[Keys.NoImageSelected] = "Nenhuma imagem selecionada",
|
||||
|
||||
[Keys.Error] = "Erro",
|
||||
[Keys.Dismiss] = "Fechar",
|
||||
[Keys.TransformationFailed] = "A transformação falhou",
|
||||
[Keys.NoTransformerInitialized] = "Transformador não inicializado",
|
||||
|
||||
[Keys.Starting] = "Iniciando",
|
||||
[Keys.Succeeded] = "Sucesso",
|
||||
[Keys.Failed] = "Falhou",
|
||||
[Keys.Canceled] = "Cancelado",
|
||||
[Keys.Unknown] = "Desconhecido",
|
||||
[Keys.Pending] = "Pendente"
|
||||
});
|
||||
|
||||
// Italian (it)
|
||||
RegisterTranslations("it", new Dictionary<string, string>
|
||||
{
|
||||
[Keys.Take] = "Scatta",
|
||||
[Keys.Pick] = "Scegli",
|
||||
[Keys.Select] = "Seleziona",
|
||||
[Keys.Transform] = "Trasforma",
|
||||
[Keys.Clear] = "Cancella",
|
||||
[Keys.Redo] = "Ripeti",
|
||||
[Keys.Reset] = "Reimposta",
|
||||
[Keys.Cancel] = "Annulla",
|
||||
[Keys.Image] = "Immagine",
|
||||
[Keys.GenerateVideo] = "Genera video",
|
||||
[Keys.Record] = "Registra",
|
||||
[Keys.PickVideo] = "Scegli video",
|
||||
|
||||
[Keys.Transforming] = "Trasformazione...",
|
||||
[Keys.GeneratingVideo] = "Generazione video...",
|
||||
[Keys.TransformingImage] = "Trasformazione immagine...",
|
||||
[Keys.Processing] = "Elaborazione",
|
||||
[Keys.PleaseWait] = "Attendere prego",
|
||||
[Keys.MayTakeAMoment] = "Potrebbe richiedere un momento",
|
||||
[Keys.MayTakeSeveralMinutes] = "Potrebbe richiedere diversi minuti",
|
||||
[Keys.TransformationComplete] = "Trasformazione completata",
|
||||
[Keys.VideoGenerationComplete] = "Generazione video completata",
|
||||
[Keys.TransformationCancelled] = "Trasformazione annullata",
|
||||
|
||||
[Keys.TapToSelectImage] = "Tocca per selezionare l'immagine",
|
||||
[Keys.SourceImage] = "Origine",
|
||||
[Keys.Result] = "Risultato",
|
||||
[Keys.NoImageSelected] = "Nessuna immagine selezionata",
|
||||
|
||||
[Keys.Error] = "Errore",
|
||||
[Keys.Dismiss] = "Chiudi",
|
||||
[Keys.TransformationFailed] = "Trasformazione fallita",
|
||||
[Keys.NoTransformerInitialized] = "Trasformatore non inizializzato",
|
||||
|
||||
[Keys.Starting] = "Avvio",
|
||||
[Keys.Succeeded] = "Riuscito",
|
||||
[Keys.Failed] = "Fallito",
|
||||
[Keys.Canceled] = "Annullato",
|
||||
[Keys.Unknown] = "Sconosciuto",
|
||||
[Keys.Pending] = "In attesa"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension method for easy localization access.
|
||||
/// </summary>
|
||||
public static class LocalizationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the localized string for this key.
|
||||
/// </summary>
|
||||
public static string Localized(this string key) => ReplicateStrings.Get(key);
|
||||
|
||||
/// <summary>
|
||||
/// Get the localized string for this key in a specific culture.
|
||||
/// </summary>
|
||||
public static string Localized(this string key, string culture) => ReplicateStrings.Get(key, culture);
|
||||
}
|
||||
410
Replicate.Maui/ModelPresets.cs
Normal file
410
Replicate.Maui/ModelPresets.cs
Normal file
@ -0,0 +1,410 @@
|
||||
namespace MarketAlly.Replicate.Maui
|
||||
{
|
||||
/// <summary>
|
||||
/// Predefined model presets for popular Replicate models.
|
||||
/// </summary>
|
||||
public static class ModelPresets
|
||||
{
|
||||
#region Image Models
|
||||
|
||||
/// <summary>
|
||||
/// HiDream E1.1 by PrunaAI - Fast high-quality image generation/editing.
|
||||
/// </summary>
|
||||
public static ModelPreset HiDreamE1 => new()
|
||||
{
|
||||
Name = "HiDream E1.1",
|
||||
ModelName = "prunaai/hidream-e1.1",
|
||||
ModelVersion = "433436facdc1172b6efcb801eb6f345d7858a32200d24e5febaccfb4b44ad66f",
|
||||
Type = ModelType.Image,
|
||||
DefaultPrompt = "anime style portrait, soft clean lines, studio ghibli inspired, friendly expression, cinematic lighting",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "speed_mode", "Juiced 🔥 (more speed)" },
|
||||
{ "seed", -1 },
|
||||
{ "output_format", "webp" },
|
||||
{ "output_quality", 100 },
|
||||
{ "guidance_scale", 2.5 },
|
||||
{ "num_inference_steps", 28 },
|
||||
{ "image_guidance_scale", 1.0 },
|
||||
{ "refine_strength", 0.3 },
|
||||
{ "clip_cfg_norm", true }
|
||||
},
|
||||
ImageInputKey = "image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 300,
|
||||
PollingDelayMs = 1500
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Stable Diffusion XL - Popular open-source image generation model.
|
||||
/// </summary>
|
||||
public static ModelPreset StableDiffusionXL => new()
|
||||
{
|
||||
Name = "Stable Diffusion XL",
|
||||
ModelName = "stability-ai/sdxl",
|
||||
ModelVersion = "7762fd07cf82c948538e41f63f77d685e02b063e37e496e96eefd46c929f9bdc",
|
||||
Type = ModelType.Image,
|
||||
DefaultPrompt = "high quality, detailed",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "width", 1024 },
|
||||
{ "height", 1024 },
|
||||
{ "num_outputs", 1 },
|
||||
{ "scheduler", "K_EULER" },
|
||||
{ "num_inference_steps", 25 },
|
||||
{ "guidance_scale", 7.5 },
|
||||
{ "refine", "expert_ensemble_refiner" }
|
||||
},
|
||||
ImageInputKey = "image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 300,
|
||||
PollingDelayMs = 1500
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Ideogram V3 Turbo - Best model for generating images with realistic, legible text.
|
||||
/// </summary>
|
||||
public static ModelPreset IdeogramV3Turbo => new()
|
||||
{
|
||||
Name = "Ideogram V3 Turbo",
|
||||
ModelName = "ideogram-ai/ideogram-v3-turbo",
|
||||
ModelVersion = "d9b3748f95c0fe3e71f010f8cc5d80e8f5252acd0e74b1c294ee889eea52a47b",
|
||||
Type = ModelType.Image,
|
||||
DefaultPrompt = "high quality image",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "aspect_ratio", "1:1" },
|
||||
{ "magic_prompt_option", "Auto" }
|
||||
},
|
||||
ImageInputKey = "image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 300,
|
||||
PollingDelayMs = 1500
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Recraft V3 SVG - First major text-to-image model that generates high quality SVG images, logos, and icons.
|
||||
/// </summary>
|
||||
public static ModelPreset RecraftV3Svg => new()
|
||||
{
|
||||
Name = "Recraft V3 SVG",
|
||||
ModelName = "recraft-ai/recraft-v3-svg",
|
||||
ModelVersion = "df041379628fa1d16bd406409930775b0904dc2bc0f3e3f38ecd2a4389e9329d",
|
||||
Type = ModelType.Image,
|
||||
DefaultPrompt = "clean vector illustration",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "size", "1024x1024" },
|
||||
{ "style", "any" }
|
||||
},
|
||||
ImageInputKey = "image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 300,
|
||||
PollingDelayMs = 1500
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Video Models
|
||||
|
||||
/// <summary>
|
||||
/// Seedance 1 Pro by ByteDance - High-quality video generation with image-to-video support.
|
||||
/// </summary>
|
||||
public static ModelPreset SeedancePro => new()
|
||||
{
|
||||
Name = "Seedance 1 Pro",
|
||||
ModelName = "bytedance/seedance-1-pro",
|
||||
ModelVersion = "5fe042776269a7262e69b14f0b835b88b8e5eff9f990cadf31b8f984ed0419ad",
|
||||
Type = ModelType.Video,
|
||||
DefaultPrompt = "animate smoothly, maintain consistency",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "duration", 5 },
|
||||
{ "resolution", "720p" },
|
||||
{ "aspect_ratio", "16:9" },
|
||||
{ "fps", 24 },
|
||||
{ "camera_fixed", false }
|
||||
},
|
||||
ImageInputKey = "image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 600,
|
||||
PollingDelayMs = 3000
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Ray Flash 2 720p by Luma - Fast video generation with camera control concepts.
|
||||
/// </summary>
|
||||
public static ModelPreset LumaRayFlash => new()
|
||||
{
|
||||
Name = "Luma Ray Flash 2",
|
||||
ModelName = "luma/ray-flash-2-720p",
|
||||
ModelVersion = "1cf3c6a9e26e7dadc776bca7497e003267acf1ec5b9ad3794d2a10a4bfa84751",
|
||||
Type = ModelType.Video,
|
||||
DefaultPrompt = "cinematic motion, smooth animation",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "duration", 5 },
|
||||
{ "aspect_ratio", "16:9" },
|
||||
{ "loop", false }
|
||||
},
|
||||
ImageInputKey = "start_image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 600,
|
||||
PollingDelayMs = 3000
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Kling 1.6 Pro by Kuaishou - Professional video generation.
|
||||
/// </summary>
|
||||
public static ModelPreset Kling16Pro => new()
|
||||
{
|
||||
Name = "Kling 1.6 Pro",
|
||||
ModelName = "kwaivgi/kling-v1.6-pro",
|
||||
ModelVersion = "974c9c5bc69f8f9c178ddea80d8936ba46c48081ad6b6ccca8843d44010c0642",
|
||||
Type = ModelType.Video,
|
||||
DefaultPrompt = "smooth cinematic video",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "duration", 5 },
|
||||
{ "aspect_ratio", "16:9" }
|
||||
},
|
||||
ImageInputKey = "start_image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 600,
|
||||
PollingDelayMs = 3000
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Wan 2.1 by Alibaba - Image-to-video with 480p/720p support.
|
||||
/// </summary>
|
||||
public static ModelPreset Wan21 => new()
|
||||
{
|
||||
Name = "Wan 2.1",
|
||||
ModelName = "alibaba/wan-2.1-i2v-480p",
|
||||
ModelVersion = "67035bc8dd4a6058c0dcdeec7de3e65e4a5d0bf78258aeb2bb5e67096cb5cd45",
|
||||
Type = ModelType.Video,
|
||||
DefaultPrompt = "animate the image smoothly",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "max_area", "480p" },
|
||||
{ "num_frames", 81 },
|
||||
{ "fast_mode", "Balanced" }
|
||||
},
|
||||
ImageInputKey = "image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 600,
|
||||
PollingDelayMs = 3000
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// MiniMax Video 01 - Text and image to video generation.
|
||||
/// </summary>
|
||||
public static ModelPreset MiniMaxVideo => new()
|
||||
{
|
||||
Name = "MiniMax Video 01",
|
||||
ModelName = "minimax/video-01",
|
||||
ModelVersion = "c8bcc4751328608bb75043b3af7bed677d1e77e765a065bb298b8b137b94e86b",
|
||||
Type = ModelType.Video,
|
||||
DefaultPrompt = "cinematic video generation",
|
||||
DefaultParameters = new Dictionary<string, object>(),
|
||||
ImageInputKey = "first_frame_image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 600,
|
||||
PollingDelayMs = 3000
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Google Veo 3 Fast - State of the art video generation from Google.
|
||||
/// </summary>
|
||||
public static ModelPreset GoogleVeo3Fast => new()
|
||||
{
|
||||
Name = "Google Veo 3 Fast",
|
||||
ModelName = "google/veo-3-fast",
|
||||
ModelVersion = "368d4063e21ecf73746b8e6d27989837d97ba07b5eca43a4e5488c852e10c2ec",
|
||||
Type = ModelType.Video,
|
||||
DefaultPrompt = "cinematic video with natural motion",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "duration", 8 },
|
||||
{ "aspect_ratio", "16:9" },
|
||||
{ "resolution", "1080p" },
|
||||
{ "generate_audio", true }
|
||||
},
|
||||
ImageInputKey = "image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 600,
|
||||
PollingDelayMs = 3000
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Kling 2.5 Turbo Pro by Kuaishou - Latest high-quality video generation.
|
||||
/// </summary>
|
||||
public static ModelPreset Kling25TurboPro => new()
|
||||
{
|
||||
Name = "Kling 2.5 Turbo Pro",
|
||||
ModelName = "kwaivgi/kling-v2.5-turbo-pro",
|
||||
ModelVersion = "939cd1851c5b112f284681b57ee9b0f36d0f913ba97de5845a7eef92d52837df",
|
||||
Type = ModelType.Video,
|
||||
DefaultPrompt = "smooth cinematic video",
|
||||
DefaultParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "duration", 5 },
|
||||
{ "aspect_ratio", "16:9" },
|
||||
{ "guidance_scale", 0.5 }
|
||||
},
|
||||
ImageInputKey = "start_image",
|
||||
PromptKey = "prompt",
|
||||
TimeoutSeconds = 600,
|
||||
PollingDelayMs = 3000
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Get all available presets.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ModelPreset> All => new[]
|
||||
{
|
||||
// Image models
|
||||
HiDreamE1,
|
||||
StableDiffusionXL,
|
||||
IdeogramV3Turbo,
|
||||
RecraftV3Svg,
|
||||
// Video models
|
||||
GoogleVeo3Fast,
|
||||
Kling25TurboPro,
|
||||
Kling16Pro,
|
||||
SeedancePro,
|
||||
LumaRayFlash,
|
||||
Wan21,
|
||||
MiniMaxVideo
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get all image model presets.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ModelPreset> ImageModels => All.Where(p => p.Type == ModelType.Image).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Get all video model presets.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ModelPreset> VideoModels => All.Where(p => p.Type == ModelType.Video).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Find a preset by model name.
|
||||
/// </summary>
|
||||
public static ModelPreset? FindByName(string modelName)
|
||||
{
|
||||
return All.FirstOrDefault(p =>
|
||||
p.ModelName.Equals(modelName, StringComparison.OrdinalIgnoreCase) ||
|
||||
p.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of model output.
|
||||
/// </summary>
|
||||
public enum ModelType
|
||||
{
|
||||
Image,
|
||||
Video
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A preset configuration for a specific Replicate model.
|
||||
/// </summary>
|
||||
public class ModelPreset
|
||||
{
|
||||
/// <summary>
|
||||
/// Display name for the preset.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Replicate model identifier (owner/model-name).
|
||||
/// </summary>
|
||||
public string ModelName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Specific model version hash.
|
||||
/// </summary>
|
||||
public string ModelVersion { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Type of output (Image or Video).
|
||||
/// </summary>
|
||||
public ModelType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default prompt for this model.
|
||||
/// </summary>
|
||||
public string DefaultPrompt { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Default parameters for this model's API.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> DefaultParameters { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The key name used for image input (e.g., "image", "start_image", "first_frame_image").
|
||||
/// </summary>
|
||||
public string ImageInputKey { get; set; } = "image";
|
||||
|
||||
/// <summary>
|
||||
/// The key name used for the prompt (usually "prompt").
|
||||
/// </summary>
|
||||
public string PromptKey { get; set; } = "prompt";
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in seconds for this model.
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Polling delay in milliseconds.
|
||||
/// </summary>
|
||||
public int PollingDelayMs { get; set; } = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of the default parameters that can be modified.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> CloneParameters()
|
||||
{
|
||||
return new Dictionary<string, object>(DefaultParameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the input dictionary for an API call.
|
||||
/// </summary>
|
||||
/// <param name="prompt">The prompt to use.</param>
|
||||
/// <param name="imageData">Optional image data (base64 or URL).</param>
|
||||
/// <param name="customParameters">Optional custom parameters to override defaults.</param>
|
||||
public Dictionary<string, object> BuildInput(
|
||||
string? prompt = null,
|
||||
string? imageData = null,
|
||||
Dictionary<string, object>? customParameters = null)
|
||||
{
|
||||
var input = CloneParameters();
|
||||
|
||||
// Set prompt
|
||||
input[PromptKey] = prompt ?? DefaultPrompt;
|
||||
|
||||
// Set image if provided
|
||||
if (!string.IsNullOrEmpty(imageData))
|
||||
{
|
||||
input[ImageInputKey] = imageData;
|
||||
}
|
||||
|
||||
// Apply custom parameter overrides
|
||||
if (customParameters != null)
|
||||
{
|
||||
foreach (var kvp in customParameters)
|
||||
{
|
||||
input[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
440
Replicate.Maui/PredictionTracker.cs
Normal file
440
Replicate.Maui/PredictionTracker.cs
Normal file
@ -0,0 +1,440 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MarketAlly.Replicate.Maui
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for tracking prediction history and monitoring pending predictions.
|
||||
/// </summary>
|
||||
public interface IPredictionTracker : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Interval between status checks for pending predictions in milliseconds.
|
||||
/// </summary>
|
||||
int PollingIntervalMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of predictions to keep in history.
|
||||
/// </summary>
|
||||
int MaxHistorySize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a prediction's status changes.
|
||||
/// </summary>
|
||||
event EventHandler<PredictionStatusChangedEventArgs>? PredictionStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a prediction completes (succeeded, failed, or canceled).
|
||||
/// </summary>
|
||||
event EventHandler<PredictionCompletedEventArgs>? PredictionCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all tracked predictions ordered by creation time (newest first).
|
||||
/// </summary>
|
||||
IReadOnlyList<TrackedPrediction> History { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets predictions that are still pending (starting or processing).
|
||||
/// </summary>
|
||||
IReadOnlyList<TrackedPrediction> PendingPredictions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Start tracking a prediction. Automatically begins polling if not already running.
|
||||
/// </summary>
|
||||
TrackedPrediction Track(PredictionResult result, TransformationType type, byte[]? sourceImageBytes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get a tracked prediction by ID.
|
||||
/// </summary>
|
||||
TrackedPrediction? Get(string predictionId);
|
||||
|
||||
/// <summary>
|
||||
/// Manually refresh a prediction's status.
|
||||
/// </summary>
|
||||
Task<TrackedPrediction?> RefreshAsync(string predictionId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Clear all completed predictions from history.
|
||||
/// </summary>
|
||||
void ClearCompleted();
|
||||
|
||||
/// <summary>
|
||||
/// Clear all predictions from history.
|
||||
/// </summary>
|
||||
void ClearAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks prediction history and monitors pending predictions for completion.
|
||||
/// </summary>
|
||||
public class PredictionTracker : IPredictionTracker
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, TrackedPrediction> _predictions = new();
|
||||
private readonly IReplicateTransformer _transformer;
|
||||
private readonly CancellationTokenSource _pollingCts = new();
|
||||
private Task? _pollingTask;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Interval between status checks for pending predictions (default 3 seconds).
|
||||
/// </summary>
|
||||
public int PollingIntervalMs { get; set; } = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of predictions to keep in history (default 50).
|
||||
/// </summary>
|
||||
public int MaxHistorySize { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a prediction's status changes.
|
||||
/// </summary>
|
||||
public event EventHandler<PredictionStatusChangedEventArgs>? PredictionStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a prediction completes (succeeded, failed, or canceled).
|
||||
/// </summary>
|
||||
public event EventHandler<PredictionCompletedEventArgs>? PredictionCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all tracked predictions ordered by creation time (newest first).
|
||||
/// </summary>
|
||||
public IReadOnlyList<TrackedPrediction> History =>
|
||||
_predictions.Values.OrderByDescending(p => p.CreatedAt).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets predictions that are still pending (starting or processing).
|
||||
/// </summary>
|
||||
public IReadOnlyList<TrackedPrediction> PendingPredictions =>
|
||||
_predictions.Values.Where(p => !p.IsCompleted).OrderByDescending(p => p.CreatedAt).ToList();
|
||||
|
||||
public PredictionTracker(IReplicateTransformer transformer)
|
||||
{
|
||||
_transformer = transformer;
|
||||
|
||||
// Subscribe to prediction creation to track immediately
|
||||
_transformer.PredictionCreated += OnPredictionCreated;
|
||||
}
|
||||
|
||||
private void OnPredictionCreated(object? sender, PredictionCreatedEventArgs e)
|
||||
{
|
||||
// Auto-track predictions as soon as they're created
|
||||
// This ensures they appear in history immediately with "starting" status
|
||||
var prediction = e.Prediction;
|
||||
|
||||
// Only track if not already tracked (avoid duplicates)
|
||||
if (!_predictions.ContainsKey(prediction.Id))
|
||||
{
|
||||
var tracked = new TrackedPrediction
|
||||
{
|
||||
Id = prediction.Id,
|
||||
Type = TransformationType.Image, // Will be updated by explicit Track() call
|
||||
Status = prediction.Status,
|
||||
Output = prediction.Output,
|
||||
Outputs = prediction.Outputs,
|
||||
Error = prediction.Error,
|
||||
Metrics = prediction.Metrics,
|
||||
CreatedAt = prediction.CreatedAt ?? DateTimeOffset.Now,
|
||||
StartedAt = prediction.StartedAt,
|
||||
CompletedAt = prediction.CompletedAt,
|
||||
SourceImageBytes = null // Will be updated by explicit Track() call
|
||||
};
|
||||
|
||||
_predictions[prediction.Id] = tracked;
|
||||
TrimHistory();
|
||||
|
||||
// Fire status event immediately
|
||||
PredictionStatusChanged?.Invoke(this, new PredictionStatusChangedEventArgs(
|
||||
tracked, string.Empty, prediction.Status));
|
||||
|
||||
// Start polling for this prediction
|
||||
if (!tracked.IsCompleted)
|
||||
{
|
||||
StartPolling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start tracking a prediction. Automatically begins polling if not already running.
|
||||
/// If the prediction was already auto-tracked via PredictionCreated event, updates it with type and source image.
|
||||
/// </summary>
|
||||
public TrackedPrediction Track(PredictionResult result, TransformationType type, byte[]? sourceImageBytes = null)
|
||||
{
|
||||
// Check if already tracked (auto-tracked via PredictionCreated event)
|
||||
if (_predictions.TryGetValue(result.Id, out var existing))
|
||||
{
|
||||
// Update with additional info that wasn't available at creation time
|
||||
existing.Type = type;
|
||||
existing.SourceImageBytes = sourceImageBytes;
|
||||
|
||||
// Update with latest result data
|
||||
var oldStatus = existing.Status;
|
||||
existing.Status = result.Status;
|
||||
existing.Output = result.Output;
|
||||
existing.Outputs = result.Outputs;
|
||||
existing.Error = result.Error;
|
||||
existing.Metrics = result.Metrics;
|
||||
existing.StartedAt = result.StartedAt;
|
||||
existing.CompletedAt = result.CompletedAt;
|
||||
|
||||
// Fire status changed if status actually changed
|
||||
if (oldStatus != result.Status)
|
||||
{
|
||||
PredictionStatusChanged?.Invoke(this, new PredictionStatusChangedEventArgs(
|
||||
existing, oldStatus, result.Status));
|
||||
}
|
||||
|
||||
// Fire completed event if now done
|
||||
if (existing.IsCompleted)
|
||||
{
|
||||
PredictionCompleted?.Invoke(this, new PredictionCompletedEventArgs(existing));
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Not yet tracked - create new entry
|
||||
var tracked = new TrackedPrediction
|
||||
{
|
||||
Id = result.Id,
|
||||
Type = type,
|
||||
Status = result.Status,
|
||||
Output = result.Output,
|
||||
Outputs = result.Outputs,
|
||||
Error = result.Error,
|
||||
Metrics = result.Metrics,
|
||||
CreatedAt = result.CreatedAt ?? DateTimeOffset.Now,
|
||||
StartedAt = result.StartedAt,
|
||||
CompletedAt = result.CompletedAt,
|
||||
SourceImageBytes = sourceImageBytes
|
||||
};
|
||||
|
||||
_predictions[result.Id] = tracked;
|
||||
TrimHistory();
|
||||
|
||||
// Fire initial status event so listeners can track from the start
|
||||
PredictionStatusChanged?.Invoke(this, new PredictionStatusChangedEventArgs(
|
||||
tracked, string.Empty, result.Status));
|
||||
|
||||
// Fire completed event if already done
|
||||
if (tracked.IsCompleted)
|
||||
{
|
||||
PredictionCompleted?.Invoke(this, new PredictionCompletedEventArgs(tracked));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Start polling if we have pending predictions
|
||||
StartPolling();
|
||||
}
|
||||
|
||||
return tracked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a tracked prediction by ID.
|
||||
/// </summary>
|
||||
public TrackedPrediction? Get(string predictionId)
|
||||
{
|
||||
return _predictions.TryGetValue(predictionId, out var prediction) ? prediction : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually refresh a prediction's status.
|
||||
/// </summary>
|
||||
public async Task<TrackedPrediction?> RefreshAsync(string predictionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_predictions.TryGetValue(predictionId, out var tracked))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _transformer.GetPredictionAsync(predictionId, cancellationToken);
|
||||
UpdateTrackedPrediction(tracked, result);
|
||||
return tracked;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return tracked;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all completed predictions from history.
|
||||
/// </summary>
|
||||
public void ClearCompleted()
|
||||
{
|
||||
var completedIds = _predictions.Values
|
||||
.Where(p => p.IsCompleted)
|
||||
.Select(p => p.Id)
|
||||
.ToList();
|
||||
|
||||
foreach (var id in completedIds)
|
||||
{
|
||||
_predictions.TryRemove(id, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all predictions from history.
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
_predictions.Clear();
|
||||
}
|
||||
|
||||
private void StartPolling()
|
||||
{
|
||||
if (_pollingTask != null && !_pollingTask.IsCompleted)
|
||||
return;
|
||||
|
||||
_pollingTask = Task.Run(async () =>
|
||||
{
|
||||
while (!_pollingCts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(PollingIntervalMs, _pollingCts.Token);
|
||||
|
||||
var pending = PendingPredictions;
|
||||
if (pending.Count == 0)
|
||||
break;
|
||||
|
||||
foreach (var prediction in pending)
|
||||
{
|
||||
if (_pollingCts.Token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _transformer.GetPredictionAsync(prediction.Id, _pollingCts.Token);
|
||||
UpdateTrackedPrediction(prediction, result);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue polling other predictions
|
||||
}
|
||||
}
|
||||
}
|
||||
}, _pollingCts.Token);
|
||||
}
|
||||
|
||||
private void UpdateTrackedPrediction(TrackedPrediction tracked, PredictionResult result)
|
||||
{
|
||||
var previousStatus = tracked.Status;
|
||||
var wasCompleted = tracked.IsCompleted;
|
||||
|
||||
tracked.Status = result.Status;
|
||||
tracked.Output = result.Output;
|
||||
tracked.Outputs = result.Outputs;
|
||||
tracked.Error = result.Error;
|
||||
tracked.Metrics = result.Metrics;
|
||||
tracked.StartedAt = result.StartedAt;
|
||||
tracked.CompletedAt = result.CompletedAt;
|
||||
tracked.LastCheckedAt = DateTimeOffset.Now;
|
||||
|
||||
// Raise status changed event
|
||||
if (previousStatus != result.Status)
|
||||
{
|
||||
PredictionStatusChanged?.Invoke(this, new PredictionStatusChangedEventArgs(
|
||||
tracked, previousStatus, result.Status));
|
||||
}
|
||||
|
||||
// Raise completed event
|
||||
if (!wasCompleted && tracked.IsCompleted)
|
||||
{
|
||||
PredictionCompleted?.Invoke(this, new PredictionCompletedEventArgs(tracked));
|
||||
}
|
||||
}
|
||||
|
||||
private void TrimHistory()
|
||||
{
|
||||
if (_predictions.Count <= MaxHistorySize)
|
||||
return;
|
||||
|
||||
var toRemove = _predictions.Values
|
||||
.Where(p => p.IsCompleted)
|
||||
.OrderBy(p => p.CreatedAt)
|
||||
.Take(_predictions.Count - MaxHistorySize)
|
||||
.Select(p => p.Id)
|
||||
.ToList();
|
||||
|
||||
foreach (var id in toRemove)
|
||||
{
|
||||
_predictions.TryRemove(id, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// Unsubscribe from transformer events
|
||||
_transformer.PredictionCreated -= OnPredictionCreated;
|
||||
|
||||
_pollingCts.Cancel();
|
||||
_pollingCts.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A tracked prediction with history information.
|
||||
/// </summary>
|
||||
public class TrackedPrediction
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public TransformationType Type { get; set; }
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public PredictionStatus StatusEnum => Status.ToPredictionStatus();
|
||||
public string? Output { get; set; }
|
||||
public string[]? Outputs { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public PredictionMetrics? Metrics { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset? StartedAt { get; set; }
|
||||
public DateTimeOffset? CompletedAt { get; set; }
|
||||
public DateTimeOffset? LastCheckedAt { get; set; }
|
||||
public byte[]? SourceImageBytes { get; set; }
|
||||
|
||||
public bool IsCompleted => StatusEnum.IsCompleted();
|
||||
public bool IsSucceeded => StatusEnum == PredictionStatus.Succeeded;
|
||||
public bool IsFailed => StatusEnum == PredictionStatus.Failed;
|
||||
public bool IsCanceled => StatusEnum == PredictionStatus.Canceled;
|
||||
public bool IsPending => StatusEnum.IsPending();
|
||||
|
||||
/// <summary>
|
||||
/// Time elapsed since creation.
|
||||
/// </summary>
|
||||
public TimeSpan Elapsed => (CompletedAt ?? DateTimeOffset.Now) - CreatedAt;
|
||||
}
|
||||
|
||||
public class PredictionStatusChangedEventArgs : EventArgs
|
||||
{
|
||||
public TrackedPrediction Prediction { get; }
|
||||
public string PreviousStatus { get; }
|
||||
public string NewStatus { get; }
|
||||
public PredictionStatus PreviousStatusEnum => PreviousStatus.ToPredictionStatus();
|
||||
public PredictionStatus NewStatusEnum => NewStatus.ToPredictionStatus();
|
||||
|
||||
public PredictionStatusChangedEventArgs(TrackedPrediction prediction, string previousStatus, string newStatus)
|
||||
{
|
||||
Prediction = prediction;
|
||||
PreviousStatus = previousStatus;
|
||||
NewStatus = newStatus;
|
||||
}
|
||||
}
|
||||
|
||||
public class PredictionCompletedEventArgs : EventArgs
|
||||
{
|
||||
public TrackedPrediction Prediction { get; }
|
||||
public bool Succeeded => Prediction.IsSucceeded;
|
||||
public bool Failed => Prediction.IsFailed;
|
||||
public bool Canceled => Prediction.IsCanceled;
|
||||
|
||||
public PredictionCompletedEventArgs(TrackedPrediction prediction)
|
||||
{
|
||||
Prediction = prediction;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
Replicate.Maui/Replicate.Maui.csproj
Normal file
88
Replicate.Maui/Replicate.Maui.csproj
Normal file
@ -0,0 +1,88 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Include net9.0 for non-MAUI usage (API only) -->
|
||||
<TargetFrameworks>net9.0;net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
|
||||
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
|
||||
<UseMaui Condition="$(TargetFramework.Contains('-'))">true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageOutputPath>C:\Users\logik\Dropbox\Nugets</PackageOutputPath>
|
||||
<RootNamespace>MarketAlly.Replicate.Maui</RootNamespace>
|
||||
<AssemblyName>MarketAlly.Replicate.Maui</AssemblyName>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
||||
|
||||
|
||||
<!-- NuGet Package Properties -->
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageId>MarketAlly.Replicate.Maui</PackageId>
|
||||
<Title>MarketAlly Replicate.Maui - AI Image & Video Generation for .NET</Title>
|
||||
<Version>1.5.0</Version>
|
||||
<Authors>MarketAlly</Authors>
|
||||
<Company>MarketAlly</Company>
|
||||
<Description>A production-ready .NET library for Replicate AI API integration. Generate AI images and videos using models like Stable Diffusion XL, HiDream, Google Veo 3, Kling, Luma Ray, and more. Includes ready-to-use MAUI controls for iOS, Android, Windows, and macOS, or use the API-only package for console apps, ASP.NET Core, and Blazor. Features: model presets, prediction tracking, BYOK support, localization (8 languages), and webhook integration.</Description>
|
||||
<Copyright>Copyright © MarketAlly 2025</Copyright>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageTags>replicate;ai;image-generation;video-generation;stable-diffusion;maui;dotnet;api;machine-learning;generative-ai;sdxl;kling;veo;luma;hidream</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/MarketAlly/Replicate.Maui</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/MarketAlly/Replicate.Maui</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>
|
||||
Version 1.5.0:
|
||||
- Added net9.0 target for API-only usage (console apps, ASP.NET Core, Blazor)
|
||||
- Changed namespace to MarketAlly.Replicate.Maui
|
||||
- Added localization support for 8 languages (en, es, fr, de, zh, ja, pt, it)
|
||||
- Added overlay button position control (Top/Bottom)
|
||||
- Added PredictionCreated event for immediate prediction tracking
|
||||
- Added Google Veo 3 Fast model preset
|
||||
- Improved prediction history with real-time status updates
|
||||
</PackageReleaseNotes>
|
||||
<RequireLicenseAcceptance>false</RequireLicenseAcceptance>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="icon.png">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Visible>true</Visible>
|
||||
</None>
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
|
||||
<!-- Core dependencies for all targets -->
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- MAUI-specific dependencies (only for platform targets) -->
|
||||
<ItemGroup Condition="$(TargetFramework.Contains('-'))">
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
|
||||
<PackageReference Include="CommunityToolkit.Maui.MediaElement" Version="6.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Exclude MAUI-specific files from net9.0 build -->
|
||||
<ItemGroup Condition="!$(TargetFramework.Contains('-'))">
|
||||
<Compile Remove="Controls\**" />
|
||||
<Compile Remove="ViewModels\**" />
|
||||
<Compile Remove="Services\**" />
|
||||
<Compile Remove="ServiceCollectionExtensions.cs" />
|
||||
<None Remove="Controls\**" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
72
Replicate.Maui/ReplicateSettings.cs
Normal file
72
Replicate.Maui/ReplicateSettings.cs
Normal file
@ -0,0 +1,72 @@
|
||||
namespace MarketAlly.Replicate.Maui
|
||||
{
|
||||
public class ReplicateSettings
|
||||
{
|
||||
public string ApiToken { get; set; } = string.Empty;
|
||||
public string ApiUrl { get; set; } = "https://api.replicate.com/v1/predictions";
|
||||
|
||||
/// <summary>
|
||||
/// Authentication scheme to use. Options: "Bearer" (default per API docs) or "Token".
|
||||
/// </summary>
|
||||
public string AuthScheme { get; set; } = "Token";
|
||||
|
||||
// Image/Anime transformation settings
|
||||
public string ModelVersion { get; set; } = string.Empty;
|
||||
public string ModelName { get; set; } = string.Empty;
|
||||
public int TimeoutSeconds { get; set; } = 300;
|
||||
public int PollingDelayMs { get; set; } = 1500;
|
||||
public string ImagePrompt { get; set; } = "anime style portrait, soft clean lines, studio ghibli or makoto shinkai inspired, friendly expression, elegant and simplified features, cinematic lighting";
|
||||
|
||||
// Video generation settings
|
||||
public string VideoModelName { get; set; } = string.Empty;
|
||||
public string VideoModelVersion { get; set; } = string.Empty;
|
||||
public string VideoPrompt { get; set; } = "animate and loop if possible, do not change the text.";
|
||||
public int VideoTimeoutSeconds { get; set; } = 600;
|
||||
public int VideoPollingDelayMs { get; set; } = 3000;
|
||||
|
||||
public ImageTransformSettings DefaultSettings { get; set; } = new();
|
||||
public VideoTransformSettings VideoSettings { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ImageTransformSettings
|
||||
{
|
||||
public int Seed { get; set; } = 42;
|
||||
public double GuidanceScale { get; set; } = 9;
|
||||
public double Strength { get; set; } = 0.8;
|
||||
public int NumInferenceSteps { get; set; } = 30;
|
||||
}
|
||||
|
||||
public class VideoTransformSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Random seed for reproducible generation.
|
||||
/// </summary>
|
||||
public int? Seed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration of the generated video in seconds. Only 5 or 10 are valid.
|
||||
/// </summary>
|
||||
public int Duration { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Video resolution and aspect ratio.
|
||||
/// Valid values: "1280*720", "720*1280", "1920*1080", "1080*1920"
|
||||
/// </summary>
|
||||
public string Size { get; set; } = "1280*720";
|
||||
|
||||
/// <summary>
|
||||
/// Audio file URL (wav/mp3, 3-30s, ≤15MB) for voice/music synchronization.
|
||||
/// </summary>
|
||||
public string? AudioUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Negative prompt to avoid certain elements in the video.
|
||||
/// </summary>
|
||||
public string? NegativePrompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the prompt optimizer will be enabled.
|
||||
/// </summary>
|
||||
public bool EnablePromptExpansion { get; set; } = true;
|
||||
}
|
||||
}
|
||||
522
Replicate.Maui/ReplicateTransformer.cs
Normal file
522
Replicate.Maui/ReplicateTransformer.cs
Normal file
@ -0,0 +1,522 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MarketAlly.Replicate.Maui
|
||||
{
|
||||
public class ReplicateTransformer : IReplicateTransformer
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ReplicateSettings _settings;
|
||||
private readonly ILogger<ReplicateTransformer>? _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private const string BaseUrl = "https://api.replicate.com/v1";
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<PredictionCreatedEventArgs>? PredictionCreated;
|
||||
|
||||
public ReplicateTransformer(
|
||||
HttpClient httpClient,
|
||||
IOptions<ReplicateSettings> settings,
|
||||
ILogger<ReplicateTransformer>? logger = null)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_settings = settings.Value;
|
||||
_logger = logger;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = false
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> TransformToAnimeAsync(
|
||||
byte[] imageBytes,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var base64Image = Convert.ToBase64String(imageBytes);
|
||||
return await TransformToAnimeFromBase64Async(base64Image, customPrompt, options, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> TransformToAnimeFromBase64Async(
|
||||
string base64Image,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!base64Image.StartsWith("data:"))
|
||||
{
|
||||
base64Image = $"data:image/jpeg;base64,{base64Image}";
|
||||
}
|
||||
|
||||
var prompt = customPrompt ?? _settings.ImagePrompt;
|
||||
|
||||
var input = new Dictionary<string, object>
|
||||
{
|
||||
{ "image", base64Image },
|
||||
{ "prompt", prompt },
|
||||
{ "seed", _settings.DefaultSettings.Seed },
|
||||
{ "guidance_scale", _settings.DefaultSettings.GuidanceScale },
|
||||
{ "strength", _settings.DefaultSettings.Strength },
|
||||
{ "num_inference_steps", _settings.DefaultSettings.NumInferenceSteps }
|
||||
};
|
||||
|
||||
return await CreatePredictionAsync(
|
||||
_settings.ModelVersion,
|
||||
input,
|
||||
options,
|
||||
_settings.TimeoutSeconds,
|
||||
_settings.PollingDelayMs,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> TransformToAnimeFromUrlAsync(
|
||||
string imageUrl,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Pass URL directly - Replicate accepts HTTP URLs for inputs
|
||||
var prompt = customPrompt ?? _settings.ImagePrompt;
|
||||
|
||||
var input = new Dictionary<string, object>
|
||||
{
|
||||
{ "image", imageUrl },
|
||||
{ "prompt", prompt },
|
||||
{ "seed", _settings.DefaultSettings.Seed },
|
||||
{ "guidance_scale", _settings.DefaultSettings.GuidanceScale },
|
||||
{ "strength", _settings.DefaultSettings.Strength },
|
||||
{ "num_inference_steps", _settings.DefaultSettings.NumInferenceSteps }
|
||||
};
|
||||
|
||||
return await CreatePredictionAsync(
|
||||
_settings.ModelVersion,
|
||||
input,
|
||||
options,
|
||||
_settings.TimeoutSeconds,
|
||||
_settings.PollingDelayMs,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> TransformToVideoAsync(
|
||||
byte[] imageBytes,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var base64Image = Convert.ToBase64String(imageBytes);
|
||||
return await TransformToVideoFromBase64Async(base64Image, customPrompt, options, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> TransformToVideoFromBase64Async(
|
||||
string base64Image,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!base64Image.StartsWith("data:"))
|
||||
{
|
||||
base64Image = $"data:image/jpeg;base64,{base64Image}";
|
||||
}
|
||||
|
||||
var prompt = customPrompt ?? _settings.VideoPrompt;
|
||||
|
||||
var input = new Dictionary<string, object>
|
||||
{
|
||||
{ "prompt", prompt },
|
||||
{ "image", base64Image },
|
||||
{ "duration", _settings.VideoSettings.Duration },
|
||||
{ "size", _settings.VideoSettings.Size },
|
||||
{ "enable_prompt_expansion", _settings.VideoSettings.EnablePromptExpansion }
|
||||
};
|
||||
|
||||
if (_settings.VideoSettings.Seed.HasValue)
|
||||
{
|
||||
input["seed"] = _settings.VideoSettings.Seed.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_settings.VideoSettings.AudioUrl))
|
||||
{
|
||||
input["audio"] = _settings.VideoSettings.AudioUrl;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_settings.VideoSettings.NegativePrompt))
|
||||
{
|
||||
input["negative_prompt"] = _settings.VideoSettings.NegativePrompt;
|
||||
}
|
||||
|
||||
return await CreatePredictionAsync(
|
||||
_settings.VideoModelVersion,
|
||||
input,
|
||||
options,
|
||||
_settings.VideoTimeoutSeconds,
|
||||
_settings.VideoPollingDelayMs,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> TransformToVideoFromUrlAsync(
|
||||
string imageUrl,
|
||||
string? customPrompt = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var prompt = customPrompt ?? _settings.VideoPrompt;
|
||||
|
||||
var input = new Dictionary<string, object>
|
||||
{
|
||||
{ "prompt", prompt },
|
||||
{ "image", imageUrl },
|
||||
{ "duration", _settings.VideoSettings.Duration },
|
||||
{ "size", _settings.VideoSettings.Size },
|
||||
{ "enable_prompt_expansion", _settings.VideoSettings.EnablePromptExpansion }
|
||||
};
|
||||
|
||||
if (_settings.VideoSettings.Seed.HasValue)
|
||||
{
|
||||
input["seed"] = _settings.VideoSettings.Seed.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_settings.VideoSettings.AudioUrl))
|
||||
{
|
||||
input["audio"] = _settings.VideoSettings.AudioUrl;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_settings.VideoSettings.NegativePrompt))
|
||||
{
|
||||
input["negative_prompt"] = _settings.VideoSettings.NegativePrompt;
|
||||
}
|
||||
|
||||
return await CreatePredictionAsync(
|
||||
_settings.VideoModelVersion,
|
||||
input,
|
||||
options,
|
||||
_settings.VideoTimeoutSeconds,
|
||||
_settings.VideoPollingDelayMs,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task CancelPredictionAsync(string predictionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var url = $"{BaseUrl}/predictions/{predictionId}/cancel";
|
||||
_logger?.LogInformation("Canceling prediction {PredictionId}", predictionId);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(_settings.AuthScheme, _settings.ApiToken);
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger?.LogError("Failed to cancel prediction: {StatusCode} - {Response}", response.StatusCode, responseBody);
|
||||
throw new ReplicateApiException($"Failed to cancel prediction: {response.StatusCode}", response.StatusCode, responseBody);
|
||||
}
|
||||
|
||||
_logger?.LogInformation("Prediction {PredictionId} canceled", predictionId);
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> GetPredictionAsync(string predictionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var url = $"{BaseUrl}/predictions/{predictionId}";
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(_settings.AuthScheme, _settings.ApiToken);
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new ReplicateApiException($"Failed to get prediction: {response.StatusCode}", response.StatusCode, responseBody);
|
||||
}
|
||||
|
||||
// Check for auth errors in response body (API sometimes returns 200 with error)
|
||||
if (responseBody.Contains("\"status\":401") || responseBody.Contains("Unauthenticated"))
|
||||
{
|
||||
throw new ReplicateApiException("Authentication failed - check your API token", System.Net.HttpStatusCode.Unauthorized, responseBody);
|
||||
}
|
||||
|
||||
return ParsePredictionResponse(responseBody);
|
||||
}
|
||||
|
||||
private async Task<PredictionResult> CreatePredictionAsync(
|
||||
string version,
|
||||
Dictionary<string, object> input,
|
||||
PredictionOptions? options,
|
||||
int timeoutSeconds,
|
||||
int pollingDelayMs,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
{ "version", version },
|
||||
{ "input", input }
|
||||
};
|
||||
|
||||
// Add webhook configuration if provided
|
||||
if (!string.IsNullOrEmpty(options?.WebhookUrl))
|
||||
{
|
||||
payload["webhook"] = options.WebhookUrl;
|
||||
if (options.WebhookEventsFilter?.Length > 0)
|
||||
{
|
||||
payload["webhook_events_filter"] = options.WebhookEventsFilter;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger?.LogInformation("Creating prediction with version {Version}", version);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/predictions");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(_settings.AuthScheme, _settings.ApiToken);
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(payload, _jsonOptions), Encoding.UTF8, "application/json");
|
||||
|
||||
// Add sync mode header if requested
|
||||
if (options?.SyncModeWaitSeconds.HasValue == true)
|
||||
{
|
||||
var waitSeconds = Math.Clamp(options.SyncModeWaitSeconds.Value, 1, 60);
|
||||
request.Headers.Add("Prefer", $"wait={waitSeconds}");
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger?.LogError("Replicate API error: {StatusCode} - {Response}", response.StatusCode, responseBody);
|
||||
throw new ReplicateApiException($"Replicate API Error: {response.StatusCode}", response.StatusCode, responseBody);
|
||||
}
|
||||
|
||||
var result = ParsePredictionResponse(responseBody);
|
||||
_logger?.LogInformation("Prediction created with ID: {PredictionId}, Status: {Status}", result.Id, result.Status);
|
||||
|
||||
// Raise event immediately so tracking can start before polling
|
||||
PredictionCreated?.Invoke(this, new PredictionCreatedEventArgs(result));
|
||||
|
||||
// If webhook only mode or already completed (sync mode success), return immediately
|
||||
if (options?.WebhookOnly == true || result.IsCompleted)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Poll for completion
|
||||
return await PollForCompletionAsync(result.Id, timeoutSeconds, pollingDelayMs, cancellationToken);
|
||||
}
|
||||
catch (ReplicateApiException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error creating prediction");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<PredictionResult> PollForCompletionAsync(
|
||||
string predictionId,
|
||||
int timeoutSeconds,
|
||||
int pollingDelayMs,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
|
||||
var attempts = 0;
|
||||
|
||||
while (!linkedCts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(pollingDelayMs, linkedCts.Token);
|
||||
attempts++;
|
||||
|
||||
var result = await GetPredictionAsync(predictionId, linkedCts.Token);
|
||||
_logger?.LogDebug("Status check {Attempt}: {Status}", attempts, result.Status);
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
if (result.Status == "failed")
|
||||
{
|
||||
_logger?.LogError("Prediction failed: {Error}", result.Error);
|
||||
throw new ReplicateTransformationException($"Prediction failed: {result.Error}");
|
||||
}
|
||||
if (result.Status == "canceled")
|
||||
{
|
||||
_logger?.LogWarning("Prediction was canceled");
|
||||
throw new ReplicateTransformationException("Prediction was canceled");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new TimeoutException($"Prediction timed out after {timeoutSeconds} seconds");
|
||||
}
|
||||
|
||||
private PredictionResult ParsePredictionResponse(string responseBody)
|
||||
{
|
||||
var json = JsonDocument.Parse(responseBody);
|
||||
var root = json.RootElement;
|
||||
|
||||
var result = new PredictionResult
|
||||
{
|
||||
Id = root.GetProperty("id").GetString() ?? string.Empty,
|
||||
Status = root.GetProperty("status").GetString() ?? string.Empty
|
||||
};
|
||||
|
||||
// Parse output
|
||||
if (root.TryGetProperty("output", out var output) && output.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
if (output.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var outputs = new List<string>();
|
||||
foreach (var item in output.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
outputs.Add(item.GetString()!);
|
||||
}
|
||||
}
|
||||
result.Outputs = outputs.ToArray();
|
||||
result.Output = outputs.FirstOrDefault();
|
||||
}
|
||||
else if (output.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
result.Output = output.GetString();
|
||||
result.Outputs = result.Output != null ? new[] { result.Output } : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse error
|
||||
if (root.TryGetProperty("error", out var error) && error.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
result.Error = error.GetString();
|
||||
}
|
||||
|
||||
// Parse metrics
|
||||
if (root.TryGetProperty("metrics", out var metrics) && metrics.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
result.Metrics = new PredictionMetrics();
|
||||
if (metrics.TryGetProperty("predict_time", out var predictTime))
|
||||
{
|
||||
result.Metrics.PredictTime = predictTime.GetDouble();
|
||||
}
|
||||
if (metrics.TryGetProperty("total_time", out var totalTime))
|
||||
{
|
||||
result.Metrics.TotalTime = totalTime.GetDouble();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse timestamps
|
||||
if (root.TryGetProperty("created_at", out var createdAt) && createdAt.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
if (DateTimeOffset.TryParse(createdAt.GetString(), out var dt))
|
||||
result.CreatedAt = dt;
|
||||
}
|
||||
if (root.TryGetProperty("started_at", out var startedAt) && startedAt.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
if (DateTimeOffset.TryParse(startedAt.GetString(), out var dt))
|
||||
result.StartedAt = dt;
|
||||
}
|
||||
if (root.TryGetProperty("completed_at", out var completedAt) && completedAt.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
if (DateTimeOffset.TryParse(completedAt.GetString(), out var dt))
|
||||
result.CompletedAt = dt;
|
||||
}
|
||||
|
||||
// Parse cancel URL
|
||||
if (root.TryGetProperty("urls", out var urls) && urls.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (urls.TryGetProperty("cancel", out var cancelUrl) && cancelUrl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
result.CancelUrl = cancelUrl.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#region Preset-based Methods
|
||||
|
||||
public async Task<PredictionResult> RunPresetAsync(
|
||||
ModelPreset preset,
|
||||
byte[] imageBytes,
|
||||
string? customPrompt = null,
|
||||
Dictionary<string, object>? customParameters = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var base64Image = Convert.ToBase64String(imageBytes);
|
||||
return await RunPresetFromBase64Async(preset, base64Image, customPrompt, customParameters, options, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> RunPresetFromBase64Async(
|
||||
ModelPreset preset,
|
||||
string base64Image,
|
||||
string? customPrompt = null,
|
||||
Dictionary<string, object>? customParameters = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!base64Image.StartsWith("data:"))
|
||||
{
|
||||
base64Image = $"data:image/jpeg;base64,{base64Image}";
|
||||
}
|
||||
|
||||
var input = preset.BuildInput(customPrompt, base64Image, customParameters);
|
||||
|
||||
return await CreatePredictionAsync(
|
||||
preset.ModelVersion,
|
||||
input,
|
||||
options,
|
||||
preset.TimeoutSeconds,
|
||||
preset.PollingDelayMs,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> RunPresetFromUrlAsync(
|
||||
ModelPreset preset,
|
||||
string imageUrl,
|
||||
string? customPrompt = null,
|
||||
Dictionary<string, object>? customParameters = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var input = preset.BuildInput(customPrompt, imageUrl, customParameters);
|
||||
|
||||
return await CreatePredictionAsync(
|
||||
preset.ModelVersion,
|
||||
input,
|
||||
options,
|
||||
preset.TimeoutSeconds,
|
||||
preset.PollingDelayMs,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PredictionResult> RunPresetTextOnlyAsync(
|
||||
ModelPreset preset,
|
||||
string prompt,
|
||||
Dictionary<string, object>? customParameters = null,
|
||||
PredictionOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var input = preset.BuildInput(prompt, imageData: null, customParameters);
|
||||
|
||||
return await CreatePredictionAsync(
|
||||
preset.ModelVersion,
|
||||
input,
|
||||
options,
|
||||
preset.TimeoutSeconds,
|
||||
preset.PollingDelayMs,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
109
Replicate.Maui/ReplicateTransformerFactory.cs
Normal file
109
Replicate.Maui/ReplicateTransformerFactory.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MarketAlly.Replicate.Maui
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating IReplicateTransformer instances with custom API tokens.
|
||||
/// Useful for BYOK (Bring Your Own Key) scenarios.
|
||||
/// </summary>
|
||||
public interface IReplicateTransformerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a transformer using the default configured settings.
|
||||
/// </summary>
|
||||
IReplicateTransformer Create();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a transformer with a custom API token, using default settings for everything else.
|
||||
/// </summary>
|
||||
/// <param name="apiToken">The Replicate API token to use.</param>
|
||||
IReplicateTransformer CreateWithToken(string apiToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a transformer with fully custom settings.
|
||||
/// </summary>
|
||||
/// <param name="configure">Action to configure the settings.</param>
|
||||
IReplicateTransformer CreateWithSettings(Action<ReplicateSettings> configure);
|
||||
}
|
||||
|
||||
public class ReplicateTransformerFactory : IReplicateTransformerFactory
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ReplicateSettings _defaultSettings;
|
||||
private readonly ILoggerFactory? _loggerFactory;
|
||||
|
||||
public ReplicateTransformerFactory(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptions<ReplicateSettings> defaultSettings,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_defaultSettings = defaultSettings.Value;
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public IReplicateTransformer Create()
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient();
|
||||
var logger = _loggerFactory?.CreateLogger<ReplicateTransformer>();
|
||||
return new ReplicateTransformer(httpClient, Options.Create(_defaultSettings), logger);
|
||||
}
|
||||
|
||||
public IReplicateTransformer CreateWithToken(string apiToken)
|
||||
{
|
||||
var settings = CloneSettings(_defaultSettings);
|
||||
settings.ApiToken = apiToken;
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient();
|
||||
var logger = _loggerFactory?.CreateLogger<ReplicateTransformer>();
|
||||
return new ReplicateTransformer(httpClient, Options.Create(settings), logger);
|
||||
}
|
||||
|
||||
public IReplicateTransformer CreateWithSettings(Action<ReplicateSettings> configure)
|
||||
{
|
||||
var settings = CloneSettings(_defaultSettings);
|
||||
configure(settings);
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient();
|
||||
var logger = _loggerFactory?.CreateLogger<ReplicateTransformer>();
|
||||
return new ReplicateTransformer(httpClient, Options.Create(settings), logger);
|
||||
}
|
||||
|
||||
private static ReplicateSettings CloneSettings(ReplicateSettings source)
|
||||
{
|
||||
return new ReplicateSettings
|
||||
{
|
||||
ApiToken = source.ApiToken,
|
||||
ApiUrl = source.ApiUrl,
|
||||
AuthScheme = source.AuthScheme,
|
||||
ModelVersion = source.ModelVersion,
|
||||
ModelName = source.ModelName,
|
||||
TimeoutSeconds = source.TimeoutSeconds,
|
||||
PollingDelayMs = source.PollingDelayMs,
|
||||
ImagePrompt = source.ImagePrompt,
|
||||
VideoModelName = source.VideoModelName,
|
||||
VideoModelVersion = source.VideoModelVersion,
|
||||
VideoPrompt = source.VideoPrompt,
|
||||
VideoTimeoutSeconds = source.VideoTimeoutSeconds,
|
||||
VideoPollingDelayMs = source.VideoPollingDelayMs,
|
||||
DefaultSettings = new ImageTransformSettings
|
||||
{
|
||||
Seed = source.DefaultSettings.Seed,
|
||||
GuidanceScale = source.DefaultSettings.GuidanceScale,
|
||||
Strength = source.DefaultSettings.Strength,
|
||||
NumInferenceSteps = source.DefaultSettings.NumInferenceSteps
|
||||
},
|
||||
VideoSettings = new VideoTransformSettings
|
||||
{
|
||||
Seed = source.VideoSettings.Seed,
|
||||
Duration = source.VideoSettings.Duration,
|
||||
Size = source.VideoSettings.Size,
|
||||
AudioUrl = source.VideoSettings.AudioUrl,
|
||||
NegativePrompt = source.VideoSettings.NegativePrompt,
|
||||
EnablePromptExpansion = source.VideoSettings.EnablePromptExpansion
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
125
Replicate.Maui/ServiceCollectionExtensions.cs
Normal file
125
Replicate.Maui/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,125 @@
|
||||
using CommunityToolkit.Maui;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MarketAlly.Replicate.Maui.Controls;
|
||||
using MarketAlly.Replicate.Maui.Services;
|
||||
using MarketAlly.Replicate.Maui.ViewModels;
|
||||
|
||||
namespace MarketAlly.Replicate.Maui
|
||||
{
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Replicate transformer services to the service collection.
|
||||
/// Registers both IReplicateTransformer (for default usage) and IReplicateTransformerFactory (for BYOK scenarios).
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configure">Action to configure ReplicateSettings.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddReplicateTransformer(
|
||||
this IServiceCollection services,
|
||||
Action<ReplicateSettings> configure)
|
||||
{
|
||||
services.Configure(configure);
|
||||
services.AddHttpClient<IReplicateTransformer, ReplicateTransformer>();
|
||||
services.AddHttpClient<IFileSaveService, FileSaveService>();
|
||||
services.AddHttpClient(); // For factory usage
|
||||
services.AddSingleton<IReplicateTransformerFactory, ReplicateTransformerFactory>();
|
||||
services.AddTransient<TransformerViewModel>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Replicate transformer services to the service collection using configuration section.
|
||||
/// Registers both IReplicateTransformer (for default usage) and IReplicateTransformerFactory (for BYOK scenarios).
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="settings">The pre-configured settings instance.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddReplicateTransformer(
|
||||
this IServiceCollection services,
|
||||
ReplicateSettings settings)
|
||||
{
|
||||
services.Configure<ReplicateSettings>(opt =>
|
||||
{
|
||||
opt.ApiToken = settings.ApiToken;
|
||||
opt.ApiUrl = settings.ApiUrl;
|
||||
opt.ModelVersion = settings.ModelVersion;
|
||||
opt.ModelName = settings.ModelName;
|
||||
opt.TimeoutSeconds = settings.TimeoutSeconds;
|
||||
opt.PollingDelayMs = settings.PollingDelayMs;
|
||||
opt.ImagePrompt = settings.ImagePrompt;
|
||||
opt.VideoModelName = settings.VideoModelName;
|
||||
opt.VideoModelVersion = settings.VideoModelVersion;
|
||||
opt.VideoPrompt = settings.VideoPrompt;
|
||||
opt.VideoTimeoutSeconds = settings.VideoTimeoutSeconds;
|
||||
opt.VideoPollingDelayMs = settings.VideoPollingDelayMs;
|
||||
opt.DefaultSettings = settings.DefaultSettings;
|
||||
opt.VideoSettings = settings.VideoSettings;
|
||||
});
|
||||
services.AddHttpClient<IReplicateTransformer, ReplicateTransformer>();
|
||||
services.AddHttpClient<IFileSaveService, FileSaveService>();
|
||||
services.AddHttpClient(); // For factory usage
|
||||
services.AddSingleton<IReplicateTransformerFactory, ReplicateTransformerFactory>();
|
||||
services.AddTransient<TransformerViewModel>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MauiAppBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures MAUI to use Replicate.Maui controls including MediaElement for video playback.
|
||||
/// Call this in your MauiProgram.cs CreateMauiApp() method.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// var builder = MauiApp.CreateBuilder();
|
||||
/// builder
|
||||
/// .UseMauiApp<App>()
|
||||
/// .UseReplicateMaui()
|
||||
/// .ConfigureFonts(...);
|
||||
/// </example>
|
||||
public static MauiAppBuilder UseReplicateMaui(this MauiAppBuilder builder)
|
||||
{
|
||||
builder.UseMauiCommunityToolkitMediaElement();
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures MAUI to use Replicate.Maui controls and registers transformer services.
|
||||
/// Call this in your MauiProgram.cs CreateMauiApp() method.
|
||||
/// </summary>
|
||||
/// <param name="builder">The MauiAppBuilder.</param>
|
||||
/// <param name="configure">Action to configure ReplicateSettings.</param>
|
||||
/// <example>
|
||||
/// var builder = MauiApp.CreateBuilder();
|
||||
/// builder
|
||||
/// .UseMauiApp<App>()
|
||||
/// .UseReplicateMaui(settings => settings.ApiToken = "your-token")
|
||||
/// .ConfigureFonts(...);
|
||||
/// </example>
|
||||
public static MauiAppBuilder UseReplicateMaui(
|
||||
this MauiAppBuilder builder,
|
||||
Action<ReplicateSettings> configure)
|
||||
{
|
||||
builder.UseMauiCommunityToolkitMediaElement();
|
||||
builder.Services.AddReplicateTransformer(configure);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures MAUI to use Replicate.Maui controls and registers transformer services.
|
||||
/// Call this in your MauiProgram.cs CreateMauiApp() method.
|
||||
/// </summary>
|
||||
/// <param name="builder">The MauiAppBuilder.</param>
|
||||
/// <param name="settings">The pre-configured settings instance.</param>
|
||||
public static MauiAppBuilder UseReplicateMaui(
|
||||
this MauiAppBuilder builder,
|
||||
ReplicateSettings settings)
|
||||
{
|
||||
builder.UseMauiCommunityToolkitMediaElement();
|
||||
builder.Services.AddReplicateTransformer(settings);
|
||||
return builder;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
154
Replicate.Maui/Services/FileSaveService.cs
Normal file
154
Replicate.Maui/Services/FileSaveService.cs
Normal file
@ -0,0 +1,154 @@
|
||||
namespace MarketAlly.Replicate.Maui.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of IFileSaveService.
|
||||
/// </summary>
|
||||
public class FileSaveService : IFileSaveService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public FileSaveService(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<FileSaveResult> SaveFromUrlAsync(
|
||||
string url,
|
||||
TransformationType type,
|
||||
string? savePath,
|
||||
string filenamePattern,
|
||||
string? predictionId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Determine save path
|
||||
var basePath = savePath ?? FileSystem.CacheDirectory;
|
||||
|
||||
// Ensure directory exists
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
Directory.CreateDirectory(basePath);
|
||||
}
|
||||
|
||||
// Download the file
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||
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 filename = GenerateFilename(filenamePattern, predictionId, extension);
|
||||
|
||||
// Full path
|
||||
var filePath = Path.Combine(basePath, filename);
|
||||
|
||||
// Save the file
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
await File.WriteAllBytesAsync(filePath, bytes, cancellationToken);
|
||||
|
||||
return FileSaveResult.Succeeded(filePath, bytes.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return FileSaveResult.Failed(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FileSaveResult> SaveBytesAsync(
|
||||
byte[] bytes,
|
||||
TransformationType type,
|
||||
string? savePath,
|
||||
string filenamePattern,
|
||||
string extension,
|
||||
string? predictionId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Determine save path
|
||||
var basePath = savePath ?? FileSystem.CacheDirectory;
|
||||
|
||||
// Ensure directory exists
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
Directory.CreateDirectory(basePath);
|
||||
}
|
||||
|
||||
// Generate filename from pattern
|
||||
var filename = GenerateFilename(filenamePattern, predictionId, extension);
|
||||
|
||||
// Full path
|
||||
var filePath = Path.Combine(basePath, filename);
|
||||
|
||||
// Save the file
|
||||
await File.WriteAllBytesAsync(filePath, bytes);
|
||||
|
||||
return FileSaveResult.Succeeded(filePath, bytes.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return FileSaveResult.Failed(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public string GenerateFilename(string pattern, string? predictionId, string extension)
|
||||
{
|
||||
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, '_');
|
||||
}
|
||||
|
||||
// Add extension if not already present
|
||||
if (!result.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result += extension;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string GetFileExtension(string? contentType, string url, TransformationType type)
|
||||
{
|
||||
// Try to get from content type
|
||||
if (!string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
var ext = 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
|
||||
};
|
||||
|
||||
if (ext != null) return ext;
|
||||
}
|
||||
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Replicate.Maui/Services/IFileSaveService.cs
Normal file
98
Replicate.Maui/Services/IFileSaveService.cs
Normal file
@ -0,0 +1,98 @@
|
||||
namespace MarketAlly.Replicate.Maui.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for saving transformation results to local storage.
|
||||
/// </summary>
|
||||
public interface IFileSaveService
|
||||
{
|
||||
/// <summary>
|
||||
/// Save a file from a URL to local storage.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to download from.</param>
|
||||
/// <param name="type">The type of content (Image or Video).</param>
|
||||
/// <param name="savePath">The folder path to save to. If null, uses cache directory.</param>
|
||||
/// <param name="filenamePattern">The filename pattern with placeholders.</param>
|
||||
/// <param name="predictionId">Optional prediction ID for filename pattern.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The full path to the saved file, or null if save failed.</returns>
|
||||
Task<FileSaveResult> SaveFromUrlAsync(
|
||||
string url,
|
||||
TransformationType type,
|
||||
string? savePath,
|
||||
string filenamePattern,
|
||||
string? predictionId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Save bytes directly to local storage.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes to save.</param>
|
||||
/// <param name="type">The type of content (Image or Video).</param>
|
||||
/// <param name="savePath">The folder path to save to. If null, uses cache directory.</param>
|
||||
/// <param name="filenamePattern">The filename pattern with placeholders.</param>
|
||||
/// <param name="extension">The file extension (e.g., ".png", ".mp4").</param>
|
||||
/// <param name="predictionId">Optional prediction ID for filename pattern.</param>
|
||||
/// <returns>The full path to the saved file, or null if save failed.</returns>
|
||||
Task<FileSaveResult> SaveBytesAsync(
|
||||
byte[] bytes,
|
||||
TransformationType type,
|
||||
string? savePath,
|
||||
string filenamePattern,
|
||||
string extension,
|
||||
string? predictionId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a filename from a pattern.
|
||||
/// </summary>
|
||||
/// <param name="pattern">The filename pattern with placeholders.</param>
|
||||
/// <param name="predictionId">Optional prediction ID.</param>
|
||||
/// <param name="extension">The file extension.</param>
|
||||
/// <returns>The generated filename.</returns>
|
||||
string GenerateFilename(string pattern, string? predictionId, string extension);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a file save operation.
|
||||
/// </summary>
|
||||
public class FileSaveResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the save operation succeeded.
|
||||
/// </summary>
|
||||
public bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The full path to the saved file, if successful.
|
||||
/// </summary>
|
||||
public string? FilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the saved file in bytes, if successful.
|
||||
/// </summary>
|
||||
public long FileSizeBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The error that occurred, if the save failed.
|
||||
/// </summary>
|
||||
public Exception? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a successful result.
|
||||
/// </summary>
|
||||
public static FileSaveResult Succeeded(string filePath, long fileSizeBytes) => new()
|
||||
{
|
||||
Success = true,
|
||||
FilePath = filePath,
|
||||
FileSizeBytes = fileSizeBytes
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a failed result.
|
||||
/// </summary>
|
||||
public static FileSaveResult Failed(Exception error) => new()
|
||||
{
|
||||
Success = false,
|
||||
Error = error
|
||||
};
|
||||
}
|
||||
}
|
||||
81
Replicate.Maui/TransformerState.cs
Normal file
81
Replicate.Maui/TransformerState.cs
Normal file
@ -0,0 +1,81 @@
|
||||
namespace MarketAlly.Replicate.Maui
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the current state of the ReplicateTransformerView control.
|
||||
/// </summary>
|
||||
public enum TransformerState
|
||||
{
|
||||
/// <summary>
|
||||
/// No image selected - showing placeholder.
|
||||
/// </summary>
|
||||
Empty,
|
||||
|
||||
/// <summary>
|
||||
/// Source image has been selected and is displayed.
|
||||
/// </summary>
|
||||
ImageSelected,
|
||||
|
||||
/// <summary>
|
||||
/// A transformation is currently in progress.
|
||||
/// </summary>
|
||||
Processing,
|
||||
|
||||
/// <summary>
|
||||
/// An image transformation has completed successfully.
|
||||
/// </summary>
|
||||
ImageResult,
|
||||
|
||||
/// <summary>
|
||||
/// A video transformation has completed successfully.
|
||||
/// </summary>
|
||||
VideoResult,
|
||||
|
||||
/// <summary>
|
||||
/// The video player is actively playing the generated video.
|
||||
/// </summary>
|
||||
PlayingVideo,
|
||||
|
||||
/// <summary>
|
||||
/// An error occurred during transformation.
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for TransformerState.
|
||||
/// </summary>
|
||||
public static class TransformerStateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the state indicates a result is available (image or video).
|
||||
/// </summary>
|
||||
public static bool HasResult(this TransformerState state)
|
||||
{
|
||||
return state is TransformerState.ImageResult or TransformerState.VideoResult or TransformerState.PlayingVideo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the state indicates the control is busy.
|
||||
/// </summary>
|
||||
public static bool IsBusy(this TransformerState state)
|
||||
{
|
||||
return state == TransformerState.Processing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the state allows starting a new transformation.
|
||||
/// </summary>
|
||||
public static bool CanTransform(this TransformerState state)
|
||||
{
|
||||
return state is TransformerState.ImageSelected or TransformerState.ImageResult or TransformerState.VideoResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the state allows selecting a new image.
|
||||
/// </summary>
|
||||
public static bool CanSelectImage(this TransformerState state)
|
||||
{
|
||||
return state != TransformerState.Processing;
|
||||
}
|
||||
}
|
||||
}
|
||||
731
Replicate.Maui/ViewModels/TransformerViewModel.cs
Normal file
731
Replicate.Maui/ViewModels/TransformerViewModel.cs
Normal file
@ -0,0 +1,731 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Input;
|
||||
using MarketAlly.Replicate.Maui;
|
||||
using MarketAlly.Replicate.Maui.Services;
|
||||
|
||||
namespace MarketAlly.Replicate.Maui.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for the ReplicateTransformerView control.
|
||||
/// Provides MVVM support with bindable properties and commands.
|
||||
/// </summary>
|
||||
public class TransformerViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private readonly IReplicateTransformer _transformer;
|
||||
private readonly IFileSaveService? _fileSaveService;
|
||||
private readonly IPredictionTracker? _tracker;
|
||||
|
||||
private byte[]? _sourceImageBytes;
|
||||
private ImageSource? _sourceImageSource;
|
||||
private string? _resultUrl;
|
||||
private ImageSource? _resultImageSource;
|
||||
private string? _videoUrl;
|
||||
private TransformerState _currentState = TransformerState.Empty;
|
||||
private bool _isProcessing;
|
||||
private string? _errorMessage;
|
||||
private string _statusMessage = string.Empty;
|
||||
private ModelPreset? _imagePreset;
|
||||
private ModelPreset? _videoPreset;
|
||||
private Dictionary<string, object>? _customImageParameters;
|
||||
private Dictionary<string, object>? _customVideoParameters;
|
||||
private string? _customImagePrompt;
|
||||
private string? _customVideoPrompt;
|
||||
private CancellationTokenSource? _currentCts;
|
||||
private string? _currentPredictionId;
|
||||
private bool _disposed;
|
||||
private string? _lastSavedImagePath;
|
||||
private string? _lastSavedVideoPath;
|
||||
|
||||
#region Events
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public event EventHandler<TransformationStartedEventArgs>? TransformationStarted;
|
||||
public event EventHandler<TransformationCompletedEventArgs>? TransformationCompleted;
|
||||
public event EventHandler<TransformationErrorEventArgs>? TransformationError;
|
||||
public event EventHandler<ImageSelectedEventArgs>? ImageSelected;
|
||||
public event EventHandler<FileSavedEventArgs>? FileSaved;
|
||||
public event EventHandler<FileSaveErrorEventArgs>? FileSaveError;
|
||||
public event EventHandler<StateChangedEventArgs>? StateChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The current state of the transformer.
|
||||
/// </summary>
|
||||
public TransformerState CurrentState
|
||||
{
|
||||
get => _currentState;
|
||||
private set
|
||||
{
|
||||
if (_currentState != value)
|
||||
{
|
||||
var previous = _currentState;
|
||||
_currentState = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(CanTransform));
|
||||
OnPropertyChanged(nameof(CanSelectImage));
|
||||
OnPropertyChanged(nameof(HasResult));
|
||||
StateChanged?.Invoke(this, new StateChangedEventArgs(previous, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the transformer is currently processing.
|
||||
/// </summary>
|
||||
public bool IsProcessing
|
||||
{
|
||||
get => _isProcessing;
|
||||
private set
|
||||
{
|
||||
if (_isProcessing != value)
|
||||
{
|
||||
_isProcessing = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(CanTransform));
|
||||
OnPropertyChanged(nameof(CanSelectImage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The source image bytes.
|
||||
/// </summary>
|
||||
public byte[]? SourceImageBytes
|
||||
{
|
||||
get => _sourceImageBytes;
|
||||
private set
|
||||
{
|
||||
_sourceImageBytes = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasSourceImage));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The source image as an ImageSource.
|
||||
/// </summary>
|
||||
public ImageSource? SourceImageSource
|
||||
{
|
||||
get => _sourceImageSource;
|
||||
private set
|
||||
{
|
||||
_sourceImageSource = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result URL from the transformation.
|
||||
/// </summary>
|
||||
public string? ResultUrl
|
||||
{
|
||||
get => _resultUrl;
|
||||
private set
|
||||
{
|
||||
_resultUrl = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result image as an ImageSource.
|
||||
/// </summary>
|
||||
public ImageSource? ResultImageSource
|
||||
{
|
||||
get => _resultImageSource;
|
||||
private set
|
||||
{
|
||||
_resultImageSource = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The video URL from video transformations.
|
||||
/// </summary>
|
||||
public string? VideoUrl
|
||||
{
|
||||
get => _videoUrl;
|
||||
private set
|
||||
{
|
||||
_videoUrl = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasVideo));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current error message, if any.
|
||||
/// </summary>
|
||||
public string? ErrorMessage
|
||||
{
|
||||
get => _errorMessage;
|
||||
private set
|
||||
{
|
||||
_errorMessage = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasError));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current status message for display.
|
||||
/// </summary>
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
private set
|
||||
{
|
||||
_statusMessage = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom prompt for image transformations.
|
||||
/// </summary>
|
||||
public string? CustomImagePrompt
|
||||
{
|
||||
get => _customImagePrompt;
|
||||
set
|
||||
{
|
||||
_customImagePrompt = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom prompt for video transformations.
|
||||
/// </summary>
|
||||
public string? CustomVideoPrompt
|
||||
{
|
||||
get => _customVideoPrompt;
|
||||
set
|
||||
{
|
||||
_customVideoPrompt = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path to the last saved image file.
|
||||
/// </summary>
|
||||
public string? LastSavedImagePath
|
||||
{
|
||||
get => _lastSavedImagePath;
|
||||
private set
|
||||
{
|
||||
_lastSavedImagePath = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path to the last saved video file.
|
||||
/// </summary>
|
||||
public string? LastSavedVideoPath
|
||||
{
|
||||
get => _lastSavedVideoPath;
|
||||
private set
|
||||
{
|
||||
_lastSavedVideoPath = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The prediction tracker for accessing history.
|
||||
/// </summary>
|
||||
public IPredictionTracker? Tracker => _tracker;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a source image has been selected.
|
||||
/// </summary>
|
||||
public bool HasSourceImage => _sourceImageBytes != null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a result is available.
|
||||
/// </summary>
|
||||
public bool HasResult => CurrentState.HasResult();
|
||||
|
||||
/// <summary>
|
||||
/// Whether a video result is available.
|
||||
/// </summary>
|
||||
public bool HasVideo => !string.IsNullOrEmpty(_videoUrl);
|
||||
|
||||
/// <summary>
|
||||
/// Whether an error has occurred.
|
||||
/// </summary>
|
||||
public bool HasError => !string.IsNullOrEmpty(_errorMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a transformation can be started.
|
||||
/// </summary>
|
||||
public bool CanTransform => CurrentState.CanTransform() && !IsProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Whether an image can be selected.
|
||||
/// </summary>
|
||||
public bool CanSelectImage => CurrentState.CanSelectImage();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Commands
|
||||
|
||||
/// <summary>
|
||||
/// Command to perform image transformation.
|
||||
/// </summary>
|
||||
public ICommand TransformImageCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Command to perform video transformation.
|
||||
/// </summary>
|
||||
public ICommand TransformVideoCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Command to cancel current transformation.
|
||||
/// </summary>
|
||||
public ICommand CancelCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Command to reset the view model.
|
||||
/// </summary>
|
||||
public ICommand ResetCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Command to clear the result.
|
||||
/// </summary>
|
||||
public ICommand ClearResultCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Command to dismiss error.
|
||||
/// </summary>
|
||||
public ICommand DismissErrorCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Create a new TransformerViewModel with the specified transformer.
|
||||
/// </summary>
|
||||
/// <param name="transformer">The transformer service to use.</param>
|
||||
/// <param name="fileSaveService">Optional file save service for auto-save functionality.</param>
|
||||
/// <param name="enableTracking">Whether to enable prediction tracking.</param>
|
||||
public TransformerViewModel(
|
||||
IReplicateTransformer transformer,
|
||||
IFileSaveService? fileSaveService = null,
|
||||
bool enableTracking = true)
|
||||
{
|
||||
_transformer = transformer ?? throw new ArgumentNullException(nameof(transformer));
|
||||
_fileSaveService = fileSaveService;
|
||||
|
||||
if (enableTracking)
|
||||
{
|
||||
_tracker = new PredictionTracker(transformer);
|
||||
}
|
||||
|
||||
// Initialize commands
|
||||
TransformImageCommand = new RelayCommand(
|
||||
async () => await TransformAsync(TransformationType.Image),
|
||||
() => CanTransform);
|
||||
|
||||
TransformVideoCommand = new RelayCommand(
|
||||
async () => await TransformAsync(TransformationType.Video),
|
||||
() => CanTransform);
|
||||
|
||||
CancelCommand = new RelayCommand(
|
||||
async () => await CancelAsync(),
|
||||
() => IsProcessing);
|
||||
|
||||
ResetCommand = new RelayCommand(Reset);
|
||||
|
||||
ClearResultCommand = new RelayCommand(ClearResult);
|
||||
|
||||
DismissErrorCommand = new RelayCommand(DismissError);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Set the source image from bytes.
|
||||
/// </summary>
|
||||
public async Task SetImageAsync(byte[] imageBytes)
|
||||
{
|
||||
SourceImageBytes = imageBytes;
|
||||
SourceImageSource = ImageSource.FromStream(() => new MemoryStream(imageBytes));
|
||||
ClearResult();
|
||||
CurrentState = TransformerState.ImageSelected;
|
||||
ImageSelected?.Invoke(this, new ImageSelectedEventArgs(imageBytes));
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the source image from a stream.
|
||||
/// </summary>
|
||||
public async Task SetImageAsync(Stream stream)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms);
|
||||
await SetImageAsync(ms.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the source image from a file path.
|
||||
/// </summary>
|
||||
public async Task SetImageAsync(string filePath)
|
||||
{
|
||||
var bytes = await File.ReadAllBytesAsync(filePath);
|
||||
await SetImageAsync(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a transformation.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of transformation.</param>
|
||||
public async Task TransformAsync(TransformationType type)
|
||||
{
|
||||
if (_sourceImageBytes == null)
|
||||
{
|
||||
ErrorMessage = "No image selected.";
|
||||
CurrentState = TransformerState.Error;
|
||||
return;
|
||||
}
|
||||
|
||||
IsProcessing = true;
|
||||
CurrentState = TransformerState.Processing;
|
||||
StatusMessage = type == TransformationType.Image
|
||||
? "Transforming image..."
|
||||
: "Generating video...";
|
||||
ErrorMessage = null;
|
||||
|
||||
_currentCts = new CancellationTokenSource();
|
||||
TransformationStarted?.Invoke(this, new TransformationStartedEventArgs(type));
|
||||
|
||||
try
|
||||
{
|
||||
PredictionResult result;
|
||||
|
||||
if (type == TransformationType.Image)
|
||||
{
|
||||
if (_imagePreset != null)
|
||||
{
|
||||
result = await _transformer.RunPresetAsync(
|
||||
_imagePreset,
|
||||
_sourceImageBytes,
|
||||
CustomImagePrompt,
|
||||
_customImageParameters,
|
||||
cancellationToken: _currentCts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await _transformer.TransformToAnimeAsync(
|
||||
_sourceImageBytes,
|
||||
CustomImagePrompt,
|
||||
cancellationToken: _currentCts.Token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_videoPreset != null)
|
||||
{
|
||||
result = await _transformer.RunPresetAsync(
|
||||
_videoPreset,
|
||||
_sourceImageBytes,
|
||||
CustomVideoPrompt,
|
||||
_customVideoParameters,
|
||||
cancellationToken: _currentCts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await _transformer.TransformToVideoAsync(
|
||||
_sourceImageBytes,
|
||||
CustomVideoPrompt,
|
||||
cancellationToken: _currentCts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
_currentPredictionId = result.Id;
|
||||
ResultUrl = result.Output;
|
||||
|
||||
// Track prediction
|
||||
_tracker?.Track(result, type, _sourceImageBytes);
|
||||
|
||||
// Update state based on result type
|
||||
if (type == TransformationType.Image && result.Output != null)
|
||||
{
|
||||
VideoUrl = null;
|
||||
ResultImageSource = ImageSource.FromUri(new Uri(result.Output));
|
||||
CurrentState = TransformerState.ImageResult;
|
||||
}
|
||||
else if (type == TransformationType.Video && result.Output != null)
|
||||
{
|
||||
VideoUrl = result.Output;
|
||||
ResultImageSource = SourceImageSource; // Use source as thumbnail
|
||||
CurrentState = TransformerState.VideoResult;
|
||||
}
|
||||
|
||||
StatusMessage = type == TransformationType.Image
|
||||
? "Transformation complete"
|
||||
: "Video generation complete";
|
||||
|
||||
TransformationCompleted?.Invoke(this, new TransformationCompletedEventArgs(type, result));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
StatusMessage = "Transformation cancelled";
|
||||
CurrentState = HasSourceImage ? TransformerState.ImageSelected : TransformerState.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = ex.Message;
|
||||
CurrentState = TransformerState.Error;
|
||||
TransformationError?.Invoke(this, new TransformationErrorEventArgs(type, ex));
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsProcessing = false;
|
||||
_currentCts?.Dispose();
|
||||
_currentCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel the current transformation.
|
||||
/// </summary>
|
||||
public async Task CancelAsync()
|
||||
{
|
||||
_currentCts?.Cancel();
|
||||
|
||||
if (_currentPredictionId != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _transformer.CancelPredictionAsync(_currentPredictionId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cancellation errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the view model to initial state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
SourceImageBytes = null;
|
||||
SourceImageSource = null;
|
||||
ClearResult();
|
||||
ErrorMessage = null;
|
||||
StatusMessage = string.Empty;
|
||||
CurrentState = TransformerState.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear only the result, keeping the source image.
|
||||
/// </summary>
|
||||
public void ClearResult()
|
||||
{
|
||||
ResultUrl = null;
|
||||
ResultImageSource = null;
|
||||
VideoUrl = null;
|
||||
|
||||
if (HasSourceImage)
|
||||
{
|
||||
CurrentState = TransformerState.ImageSelected;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dismiss the current error.
|
||||
/// </summary>
|
||||
public void DismissError()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
CurrentState = HasSourceImage ? TransformerState.ImageSelected : TransformerState.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set an image preset for transformations.
|
||||
/// </summary>
|
||||
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 video preset for transformations.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Clear presets and use default settings.
|
||||
/// </summary>
|
||||
public void ClearPresets()
|
||||
{
|
||||
_imagePreset = null;
|
||||
_videoPreset = null;
|
||||
_customImageParameters = null;
|
||||
_customVideoParameters = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the current result to a file.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of result to save.</param>
|
||||
/// <param name="savePath">The folder path to save to. If null, uses cache directory.</param>
|
||||
/// <param name="filenamePattern">The filename pattern to use.</param>
|
||||
/// <returns>The path to the saved file, or null if save failed.</returns>
|
||||
public async Task<string?> SaveResultAsync(
|
||||
TransformationType type,
|
||||
string? savePath = null,
|
||||
string filenamePattern = "{type}_{timestamp}")
|
||||
{
|
||||
var url = type == TransformationType.Image ? ResultUrl : VideoUrl;
|
||||
if (string.IsNullOrEmpty(url))
|
||||
return null;
|
||||
|
||||
if (_fileSaveService == null)
|
||||
return null;
|
||||
|
||||
var result = await _fileSaveService.SaveFromUrlAsync(
|
||||
url,
|
||||
type,
|
||||
savePath,
|
||||
filenamePattern,
|
||||
_currentPredictionId);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
if (type == TransformationType.Image)
|
||||
LastSavedImagePath = result.FilePath;
|
||||
else
|
||||
LastSavedVideoPath = result.FilePath;
|
||||
|
||||
FileSaved?.Invoke(this, new FileSavedEventArgs(result.FilePath!, type, result.FileSizeBytes));
|
||||
return result.FilePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
FileSaveError?.Invoke(this, new FileSaveErrorEventArgs(type, result.Error!));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region INotifyPropertyChanged
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_currentCts?.Cancel();
|
||||
_currentCts?.Dispose();
|
||||
_tracker?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple ICommand implementation for MVVM.
|
||||
/// </summary>
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
||||
|
||||
public void Execute(object? parameter) => _execute();
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async ICommand implementation for MVVM.
|
||||
/// </summary>
|
||||
public class AsyncRelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<Task> _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
private bool _isExecuting;
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter) => !_isExecuting && (_canExecute?.Invoke() ?? true);
|
||||
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (_isExecuting) return;
|
||||
|
||||
_isExecuting = true;
|
||||
RaiseCanExecuteChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await _execute();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecuting = false;
|
||||
RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
BIN
Replicate.Maui/icon.png
Normal file
BIN
Replicate.Maui/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
14
Test.Replicate/App.xaml
Normal file
14
Test.Replicate/App.xaml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version = "1.0" encoding = "UTF-8" ?>
|
||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:Test.Replicate"
|
||||
x:Class="Test.Replicate.App">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
29
Test.Replicate/App.xaml.cs
Normal file
29
Test.Replicate/App.xaml.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Test.Replicate.Services;
|
||||
|
||||
namespace Test.Replicate
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
public App(IServiceProvider serviceProvider)
|
||||
{
|
||||
InitializeComponent();
|
||||
ServiceProvider = serviceProvider;
|
||||
|
||||
// Load saved settings from secure storage
|
||||
_ = LoadSettingsAsync();
|
||||
}
|
||||
|
||||
public static IServiceProvider ServiceProvider { get; private set; } = null!;
|
||||
|
||||
protected override Window CreateWindow(IActivationState? activationState)
|
||||
{
|
||||
return new Window(new AppShell());
|
||||
}
|
||||
|
||||
private async Task LoadSettingsAsync()
|
||||
{
|
||||
var appSettings = ServiceProvider.GetRequiredService<AppSettings>();
|
||||
await appSettings.LoadAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Test.Replicate/AppShell.xaml
Normal file
35
Test.Replicate/AppShell.xaml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Shell
|
||||
x:Class="Test.Replicate.AppShell"
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Test.Replicate.Pages"
|
||||
Title="Replicate Transformer">
|
||||
|
||||
<TabBar>
|
||||
<ShellContent
|
||||
Title="Image"
|
||||
Icon="{OnPlatform Default=''}"
|
||||
ContentTemplate="{DataTemplate pages:ImageTransformPage}"
|
||||
Route="ImageTransform" />
|
||||
|
||||
<ShellContent
|
||||
Title="Video"
|
||||
Icon="{OnPlatform Default=''}"
|
||||
ContentTemplate="{DataTemplate pages:VideoGenerationPage}"
|
||||
Route="VideoGeneration" />
|
||||
|
||||
<ShellContent
|
||||
Title="History"
|
||||
Icon="{OnPlatform Default=''}"
|
||||
ContentTemplate="{DataTemplate pages:HistoryPage}"
|
||||
Route="History" />
|
||||
|
||||
<ShellContent
|
||||
Title="Settings"
|
||||
Icon="{OnPlatform Default=''}"
|
||||
ContentTemplate="{DataTemplate pages:SettingsPage}"
|
||||
Route="Settings" />
|
||||
</TabBar>
|
||||
|
||||
</Shell>
|
||||
10
Test.Replicate/AppShell.xaml.cs
Normal file
10
Test.Replicate/AppShell.xaml.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Test.Replicate
|
||||
{
|
||||
public partial class AppShell : Shell
|
||||
{
|
||||
public AppShell()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Test.Replicate/MauiProgram.cs
Normal file
44
Test.Replicate/MauiProgram.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MarketAlly.Replicate.Maui;
|
||||
using Test.Replicate.Services;
|
||||
|
||||
namespace Test.Replicate
|
||||
{
|
||||
public static class MauiProgram
|
||||
{
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder
|
||||
.UseMauiApp<App>()
|
||||
.ConfigureFonts(fonts =>
|
||||
{
|
||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
||||
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
|
||||
})
|
||||
.UseReplicateMaui(settings =>
|
||||
{
|
||||
// Default settings - will be overridden by AppSettings at runtime
|
||||
settings.ModelName = "prunaai/hidream-e1.1";
|
||||
settings.ModelVersion = "433436facdc1172b6efcb801eb6f345d7858a32200d24e5febaccfb4b44ad66f";
|
||||
settings.VideoModelName = "bytedance/seedance-1-pro";
|
||||
settings.VideoModelVersion = "5fe042776269a7262e69b14f0b835b88b8e5eff9f990cadf31b8f984ed0419ad";
|
||||
settings.TimeoutSeconds = 300;
|
||||
settings.VideoTimeoutSeconds = 600;
|
||||
});
|
||||
|
||||
// Register shared services
|
||||
builder.Services.AddSingleton<AppSettings>();
|
||||
builder.Services.AddSingleton<HistoryService>();
|
||||
|
||||
// Register App for DI
|
||||
builder.Services.AddSingleton<App>();
|
||||
|
||||
#if DEBUG
|
||||
builder.Logging.AddDebug();
|
||||
#endif
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
115
Test.Replicate/Pages/HistoryPage.xaml
Normal file
115
Test.Replicate/Pages/HistoryPage.xaml
Normal file
@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Test.Replicate.Pages.HistoryPage"
|
||||
Title="History">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto" Padding="16" RowSpacing="12">
|
||||
|
||||
<!-- Header -->
|
||||
<VerticalStackLayout Grid.Row="0" Spacing="4">
|
||||
<Label Text="Prediction History"
|
||||
FontSize="22"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label x:Name="SummaryLabel"
|
||||
Text="No predictions yet"
|
||||
FontSize="13"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- History List -->
|
||||
<CollectionView x:Name="HistoryList"
|
||||
Grid.Row="1"
|
||||
SelectionMode="None">
|
||||
<CollectionView.EmptyView>
|
||||
<VerticalStackLayout VerticalOptions="Center" HorizontalOptions="Center" Spacing="8">
|
||||
<Label Text="No predictions in history"
|
||||
FontSize="16"
|
||||
TextColor="{AppThemeBinding Light=#888888, Dark=#666666}"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label Text="Transform images or generate videos to see them here"
|
||||
FontSize="12"
|
||||
TextColor="{AppThemeBinding Light=#999999, Dark=#555555}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</CollectionView.EmptyView>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Frame Margin="0,4" Padding="12" CornerRadius="8"
|
||||
BackgroundColor="{AppThemeBinding Light=#FFFFFF, Dark=#2A2A2A}"
|
||||
BorderColor="{AppThemeBinding Light=#E0E0E0, Dark=#404040}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="12">
|
||||
<!-- Status Indicator -->
|
||||
<BoxView Grid.Column="0"
|
||||
WidthRequest="8"
|
||||
HeightRequest="8"
|
||||
CornerRadius="4"
|
||||
VerticalOptions="Center"
|
||||
Color="{Binding StatusColor}"/>
|
||||
|
||||
<!-- Prediction Info -->
|
||||
<VerticalStackLayout Grid.Column="1" Spacing="2">
|
||||
<Label Text="{Binding TypeDisplay}"
|
||||
FontSize="14"
|
||||
FontAttributes="Bold"/>
|
||||
<Label Text="{Binding Id}"
|
||||
FontSize="10"
|
||||
TextColor="{AppThemeBinding Light=#888888, Dark=#777777}"
|
||||
LineBreakMode="MiddleTruncation"/>
|
||||
<Label Text="{Binding StatusDisplay}"
|
||||
FontSize="12"
|
||||
TextColor="{Binding StatusTextColor}"/>
|
||||
<Label Text="{Binding ElapsedDisplay}"
|
||||
FontSize="10"
|
||||
TextColor="{AppThemeBinding Light=#999999, Dark=#666666}"/>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Output Preview/Link -->
|
||||
<VerticalStackLayout Grid.Column="2" VerticalOptions="Center" Spacing="4">
|
||||
<Label Text="{Binding TimeDisplay}"
|
||||
FontSize="10"
|
||||
TextColor="{AppThemeBinding Light=#888888, Dark=#666666}"
|
||||
HorizontalOptions="End"/>
|
||||
<Label Text="{Binding OutputStatus}"
|
||||
FontSize="10"
|
||||
TextColor="{AppThemeBinding Light=#007AFF, Dark=#0A84FF}"
|
||||
HorizontalOptions="End"/>
|
||||
</VerticalStackLayout>
|
||||
</Grid>
|
||||
</Frame>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
|
||||
<!-- Actions -->
|
||||
<HorizontalStackLayout Grid.Row="2" Spacing="12" HorizontalOptions="Center">
|
||||
<Button x:Name="RefreshButton"
|
||||
Text="Refresh"
|
||||
BackgroundColor="{AppThemeBinding Light=#007AFF, Dark=#0A84FF}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="40"
|
||||
WidthRequest="100"
|
||||
Clicked="OnRefreshClicked"/>
|
||||
<Button x:Name="ClearCompletedButton"
|
||||
Text="Clear Completed"
|
||||
BackgroundColor="{AppThemeBinding Light=#FF9800, Dark=#FF9800}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="40"
|
||||
WidthRequest="130"
|
||||
Clicked="OnClearCompletedClicked"/>
|
||||
<Button x:Name="ClearAllButton"
|
||||
Text="Clear All"
|
||||
BackgroundColor="{AppThemeBinding Light=#F44336, Dark=#EF5350}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="40"
|
||||
WidthRequest="100"
|
||||
Clicked="OnClearAllClicked"/>
|
||||
</HorizontalStackLayout>
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
142
Test.Replicate/Pages/HistoryPage.xaml.cs
Normal file
142
Test.Replicate/Pages/HistoryPage.xaml.cs
Normal file
@ -0,0 +1,142 @@
|
||||
using MarketAlly.Replicate.Maui;
|
||||
using Test.Replicate.Services;
|
||||
|
||||
namespace Test.Replicate.Pages
|
||||
{
|
||||
public partial class HistoryPage : ContentPage
|
||||
{
|
||||
private readonly HistoryService _historyService;
|
||||
|
||||
public HistoryPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_historyService = App.ServiceProvider.GetRequiredService<HistoryService>();
|
||||
_historyService.HistoryChanged += OnHistoryChanged;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
RefreshHistory();
|
||||
}
|
||||
|
||||
private void OnHistoryChanged()
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(RefreshHistory);
|
||||
}
|
||||
|
||||
private void RefreshHistory()
|
||||
{
|
||||
var items = _historyService.GetHistory()
|
||||
.Select(p => new PredictionViewModel(p))
|
||||
.ToList();
|
||||
|
||||
HistoryList.ItemsSource = items;
|
||||
|
||||
var pending = items.Count(i => i.IsPending);
|
||||
var succeeded = items.Count(i => i.IsSucceeded);
|
||||
var failed = items.Count(i => i.IsFailed);
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
SummaryLabel.Text = "No predictions yet";
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (pending > 0) parts.Add($"{pending} pending");
|
||||
if (succeeded > 0) parts.Add($"{succeeded} succeeded");
|
||||
if (failed > 0) parts.Add($"{failed} failed");
|
||||
SummaryLabel.Text = $"{items.Count} predictions: {string.Join(", ", parts)}";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRefreshClicked(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshHistory();
|
||||
}
|
||||
|
||||
private void OnClearCompletedClicked(object? sender, EventArgs e)
|
||||
{
|
||||
_historyService.ClearCompleted();
|
||||
RefreshHistory();
|
||||
}
|
||||
|
||||
private void OnClearAllClicked(object? sender, EventArgs e)
|
||||
{
|
||||
_historyService.ClearAll();
|
||||
RefreshHistory();
|
||||
}
|
||||
}
|
||||
|
||||
public class PredictionViewModel
|
||||
{
|
||||
private readonly TrackedPrediction _prediction;
|
||||
|
||||
public PredictionViewModel(TrackedPrediction prediction)
|
||||
{
|
||||
_prediction = prediction;
|
||||
}
|
||||
|
||||
public string Id => _prediction.Id;
|
||||
public string TypeDisplay => _prediction.Type == TransformationType.Image ? "Image Transform" : "Video Generation";
|
||||
public string StatusDisplay => _prediction.StatusEnum switch
|
||||
{
|
||||
PredictionStatus.Starting => "Starting...",
|
||||
PredictionStatus.Processing => "Processing...",
|
||||
PredictionStatus.Succeeded => "Completed",
|
||||
PredictionStatus.Failed => $"Failed: {_prediction.Error ?? "Unknown error"}",
|
||||
PredictionStatus.Canceled => "Canceled",
|
||||
_ => _prediction.Status
|
||||
};
|
||||
|
||||
public bool IsPending => _prediction.IsPending;
|
||||
public bool IsSucceeded => _prediction.IsSucceeded;
|
||||
public bool IsFailed => _prediction.IsFailed;
|
||||
|
||||
public Color StatusColor => _prediction.StatusEnum switch
|
||||
{
|
||||
PredictionStatus.Starting => Colors.Orange,
|
||||
PredictionStatus.Processing => Colors.Blue,
|
||||
PredictionStatus.Succeeded => Colors.Green,
|
||||
PredictionStatus.Failed => Colors.Red,
|
||||
PredictionStatus.Canceled => Colors.Gray,
|
||||
_ => Colors.Gray
|
||||
};
|
||||
|
||||
public Color StatusTextColor => _prediction.StatusEnum switch
|
||||
{
|
||||
PredictionStatus.Starting => Colors.Orange,
|
||||
PredictionStatus.Processing => Colors.Blue,
|
||||
PredictionStatus.Succeeded => Colors.Green,
|
||||
PredictionStatus.Failed => Colors.Red,
|
||||
PredictionStatus.Canceled => Colors.Gray,
|
||||
_ => Colors.Gray
|
||||
};
|
||||
|
||||
public string TimeDisplay => _prediction.CreatedAt.ToLocalTime().ToString("HH:mm:ss");
|
||||
|
||||
public string ElapsedDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
var elapsed = _prediction.Elapsed;
|
||||
if (elapsed.TotalMinutes >= 1)
|
||||
return $"Elapsed: {elapsed.Minutes}m {elapsed.Seconds}s";
|
||||
return $"Elapsed: {elapsed.TotalSeconds:F1}s";
|
||||
}
|
||||
}
|
||||
|
||||
public string OutputStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_prediction.IsSucceeded && _prediction.Output != null)
|
||||
return "Output ready";
|
||||
if (_prediction.IsPending)
|
||||
return "In progress...";
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
Test.Replicate/Pages/ImageTransformPage.xaml
Normal file
126
Test.Replicate/Pages/ImageTransformPage.xaml
Normal file
@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:replicate="clr-namespace:MarketAlly.Replicate.Maui.Controls;assembly=MarketAlly.Replicate.Maui"
|
||||
x:Class="Test.Replicate.Pages.ImageTransformPage"
|
||||
Title="Image Transform">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto" Padding="16" RowSpacing="12">
|
||||
|
||||
<!-- Header -->
|
||||
<VerticalStackLayout Grid.Row="0" Spacing="4">
|
||||
<Label Text="Image Transformation"
|
||||
FontSize="22"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label Text="Transform your photos using AI models"
|
||||
FontSize="13"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Model Selection and Prompt -->
|
||||
<Frame Grid.Row="1"
|
||||
Padding="12"
|
||||
BackgroundColor="{AppThemeBinding Light=#E3F2FD, Dark=#1A237E}"
|
||||
BorderColor="{AppThemeBinding Light=#64B5F6, Dark=#5C6BC0}"
|
||||
CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="12">
|
||||
<VerticalStackLayout Grid.Column="0" Spacing="4">
|
||||
<Label Text="Model:" FontSize="12" FontAttributes="Bold"/>
|
||||
<Picker x:Name="ImageModelPicker"
|
||||
FontSize="13"
|
||||
SelectedIndexChanged="OnImageModelChanged"/>
|
||||
</VerticalStackLayout>
|
||||
<VerticalStackLayout Grid.Column="1" Spacing="4">
|
||||
<Label Text="Speed Mode:" FontSize="12" FontAttributes="Bold"/>
|
||||
<Picker x:Name="SpeedModePicker" FontSize="13">
|
||||
<Picker.ItemsSource>
|
||||
<x:Array Type="{x:Type x:String}">
|
||||
<x:String>Lightly Juiced 🍊</x:String>
|
||||
<x:String>Juiced 🔥</x:String>
|
||||
<x:String>Extra Juiced 🚀</x:String>
|
||||
</x:Array>
|
||||
</Picker.ItemsSource>
|
||||
</Picker>
|
||||
</VerticalStackLayout>
|
||||
</Grid>
|
||||
<VerticalStackLayout Spacing="4">
|
||||
<Label Text="Prompt (optional override):" FontSize="12" FontAttributes="Bold"/>
|
||||
<Editor x:Name="PromptEditor"
|
||||
Placeholder="Leave empty to use model's default prompt..."
|
||||
HeightRequest="50"
|
||||
FontSize="12"/>
|
||||
</VerticalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Transformer Control (Image only) - Side by Side Layout -->
|
||||
<replicate:ReplicateTransformerView
|
||||
x:Name="TransformerView"
|
||||
Grid.Row="2"
|
||||
LayoutMode="SideBySide"
|
||||
ShowVideoOptions="False"
|
||||
TrackPredictions="True"
|
||||
ShowSideBySideSourceButtons="True"
|
||||
ShowSideBySideResultButtons="True"
|
||||
ShowSideBySideTransformButton="True"
|
||||
DefaultButtonDisplayMode="Both"
|
||||
SideBySideCaptureIconText="📸"
|
||||
SideBySideCaptureLabel="Take"
|
||||
SideBySidePickIconText="🖼"
|
||||
SideBySidePickLabel="Pick"
|
||||
SideBySideTransformIconText="✨"
|
||||
SideBySideTransformLabel="Transform"
|
||||
SideBySideClearIconText="🗑"
|
||||
SideBySideClearLabel="Clear"
|
||||
SideBySideRedoIconText="↺"
|
||||
SideBySideRedoLabel="Redo"
|
||||
PlaceholderText="Select image"/>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<Button x:Name="CancelButton"
|
||||
Grid.Row="3"
|
||||
Text="Cancel Transformation"
|
||||
BackgroundColor="{AppThemeBinding Light=#F44336, Dark=#EF5350}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="44"
|
||||
IsVisible="False"
|
||||
Clicked="OnCancelClicked"/>
|
||||
|
||||
<!-- Status Area -->
|
||||
<Frame Grid.Row="4"
|
||||
Padding="12"
|
||||
BackgroundColor="{AppThemeBinding Light=#F0F0F0, Dark=#2A2A2A}"
|
||||
BorderColor="Transparent"
|
||||
CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="6">
|
||||
<Label x:Name="StatusLabel"
|
||||
Text="Configure your API token in Settings to get started"
|
||||
FontSize="14"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"/>
|
||||
<Label x:Name="PredictionIdLabel"
|
||||
Text=""
|
||||
FontSize="11"
|
||||
TextColor="{AppThemeBinding Light=#888888, Dark=#777777}"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label x:Name="ResultUrlLabel"
|
||||
Text=""
|
||||
FontSize="12"
|
||||
TextColor="{AppThemeBinding Light=#007AFF, Dark=#0A84FF}"
|
||||
HorizontalOptions="Center"
|
||||
LineBreakMode="MiddleTruncation"
|
||||
MaxLines="2"/>
|
||||
<Label x:Name="MetricsLabel"
|
||||
Text=""
|
||||
FontSize="11"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
249
Test.Replicate/Pages/ImageTransformPage.xaml.cs
Normal file
249
Test.Replicate/Pages/ImageTransformPage.xaml.cs
Normal file
@ -0,0 +1,249 @@
|
||||
using MarketAlly.Replicate.Maui;
|
||||
using MarketAlly.Replicate.Maui.Controls;
|
||||
using Test.Replicate.Services;
|
||||
|
||||
namespace Test.Replicate.Pages
|
||||
{
|
||||
public partial class ImageTransformPage : ContentPage
|
||||
{
|
||||
private readonly AppSettings _appSettings;
|
||||
private readonly IReplicateTransformerFactory _factory;
|
||||
private readonly HistoryService _historyService;
|
||||
private readonly List<ModelPreset> _imagePresets;
|
||||
|
||||
public ImageTransformPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_appSettings = App.ServiceProvider.GetRequiredService<AppSettings>();
|
||||
_factory = App.ServiceProvider.GetRequiredService<IReplicateTransformerFactory>();
|
||||
_historyService = App.ServiceProvider.GetRequiredService<HistoryService>();
|
||||
|
||||
// Get available image presets
|
||||
_imagePresets = ModelPresets.ImageModels.ToList();
|
||||
|
||||
// Initialize with factory
|
||||
TransformerView.Initialize(_factory);
|
||||
|
||||
// Populate model picker
|
||||
ImageModelPicker.ItemsSource = _imagePresets.Select(p => p.Name).ToList();
|
||||
ImageModelPicker.SelectedIndex = 0;
|
||||
SpeedModePicker.SelectedIndex = 1; // Default to "Juiced"
|
||||
|
||||
// Subscribe to control events
|
||||
TransformerView.ImageSelected += OnImageSelected;
|
||||
TransformerView.TransformationStarted += OnTransformationStarted;
|
||||
TransformerView.TransformationCompleted += OnTransformationCompleted;
|
||||
TransformerView.TransformationError += OnTransformationError;
|
||||
|
||||
// Subscribe to prediction tracking events (forwarded from internal tracker)
|
||||
TransformerView.PredictionTracked += OnPredictionTracked;
|
||||
TransformerView.PredictionCompleted += OnPredictionCompleted;
|
||||
|
||||
// Show/hide cancel button based on processing state
|
||||
TransformerView.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(TransformerView.IsProcessing))
|
||||
{
|
||||
CancelButton.IsVisible = TransformerView.IsProcessing;
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for settings changes
|
||||
_appSettings.SettingsChanged += OnSettingsChanged;
|
||||
|
||||
// Apply initial settings
|
||||
ApplySettings();
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
ApplySettings();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private void OnSettingsChanged()
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
ApplySettings();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
private void ApplySettings()
|
||||
{
|
||||
if (!_appSettings.IsConfigured)
|
||||
return;
|
||||
|
||||
// Configure API token
|
||||
TransformerView.ConfigureSettings(settings =>
|
||||
{
|
||||
settings.ApiToken = _appSettings.ApiToken;
|
||||
});
|
||||
|
||||
// Apply selected preset
|
||||
ApplySelectedPreset();
|
||||
}
|
||||
|
||||
private void OnImageModelChanged(object? sender, EventArgs e)
|
||||
{
|
||||
ApplySelectedPreset();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private void ApplySelectedPreset()
|
||||
{
|
||||
if (ImageModelPicker.SelectedIndex < 0 || ImageModelPicker.SelectedIndex >= _imagePresets.Count)
|
||||
return;
|
||||
|
||||
var preset = _imagePresets[ImageModelPicker.SelectedIndex];
|
||||
|
||||
// Build custom parameters based on UI selections
|
||||
var customParams = new Dictionary<string, object>();
|
||||
|
||||
// Speed mode (only for HiDream)
|
||||
if (preset.Name.Contains("HiDream") && SpeedModePicker.SelectedIndex >= 0)
|
||||
{
|
||||
var speedModes = new[]
|
||||
{
|
||||
"Lightly Juiced 🍊 (more consistent)",
|
||||
"Juiced 🔥 (more speed)",
|
||||
"Extra Juiced 🚀 (even more speed)"
|
||||
};
|
||||
customParams["speed_mode"] = speedModes[SpeedModePicker.SelectedIndex];
|
||||
}
|
||||
|
||||
TransformerView.SetImagePreset(preset, customParams.Count > 0 ? customParams : null);
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
if (!_appSettings.IsConfigured)
|
||||
{
|
||||
StatusLabel.Text = "Configure your API token in Settings to get started";
|
||||
}
|
||||
else if (ImageModelPicker.SelectedIndex >= 0 && ImageModelPicker.SelectedIndex < _imagePresets.Count)
|
||||
{
|
||||
var preset = _imagePresets[ImageModelPicker.SelectedIndex];
|
||||
StatusLabel.Text = $"Ready with {preset.Name} - select an image to transform";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusLabel.Text = "Ready - select an image to transform";
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnCancelClicked(object? sender, EventArgs e)
|
||||
{
|
||||
StatusLabel.Text = "Canceling...";
|
||||
await TransformerView.CancelTransformationAsync();
|
||||
StatusLabel.Text = "Transformation canceled";
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
private void OnImageSelected(object? sender, ImageSelectedEventArgs e)
|
||||
{
|
||||
if (!_appSettings.IsConfigured)
|
||||
{
|
||||
StatusLabel.Text = "Please configure your API token in Settings first";
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply custom prompt if provided
|
||||
var customPrompt = PromptEditor.Text?.Trim();
|
||||
TransformerView.CustomImagePrompt = string.IsNullOrEmpty(customPrompt) ? null : customPrompt;
|
||||
|
||||
// Re-apply preset with current settings
|
||||
ApplySelectedPreset();
|
||||
|
||||
var modelName = ImageModelPicker.SelectedIndex >= 0 && ImageModelPicker.SelectedIndex < _imagePresets.Count
|
||||
? _imagePresets[ImageModelPicker.SelectedIndex].Name
|
||||
: "selected model";
|
||||
|
||||
StatusLabel.Text = $"Image selected ({e.ImageBytes.Length / 1024:N0} KB) - tap Transform to process with {modelName}";
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
private void OnTransformationStarted(object? sender, TransformationStartedEventArgs e)
|
||||
{
|
||||
// Update prompt right before transformation
|
||||
var customPrompt = PromptEditor.Text?.Trim();
|
||||
TransformerView.CustomImagePrompt = string.IsNullOrEmpty(customPrompt) ? null : customPrompt;
|
||||
|
||||
var modelName = TransformerView.ImagePreset?.Name ?? "model";
|
||||
StatusLabel.Text = $"Transforming with {modelName}...";
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
private void OnTransformationCompleted(object? sender, TransformationCompletedEventArgs e)
|
||||
{
|
||||
var result = e.Result;
|
||||
var modelName = TransformerView.ImagePreset?.Name ?? "transformation";
|
||||
|
||||
StatusLabel.Text = $"{modelName} complete!";
|
||||
PredictionIdLabel.Text = $"Prediction ID: {result.Id}";
|
||||
ResultUrlLabel.Text = result.Output ?? "No output URL";
|
||||
|
||||
if (result.Metrics != null)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (result.Metrics.PredictTime.HasValue)
|
||||
parts.Add($"Predict: {result.Metrics.PredictTime:F1}s");
|
||||
if (result.Metrics.TotalTime.HasValue)
|
||||
parts.Add($"Total: {result.Metrics.TotalTime:F1}s");
|
||||
MetricsLabel.Text = string.Join(" | ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTransformationError(object? sender, TransformationErrorEventArgs e)
|
||||
{
|
||||
StatusLabel.Text = $"Error: {e.Error.Message}";
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
private void ClearResults()
|
||||
{
|
||||
PredictionIdLabel.Text = "";
|
||||
ResultUrlLabel.Text = "";
|
||||
MetricsLabel.Text = "";
|
||||
}
|
||||
|
||||
private void OnPredictionTracked(object? sender, PredictionStatusChangedEventArgs e)
|
||||
{
|
||||
_historyService.TrackPrediction(e.Prediction);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
StatusLabel.Text = e.NewStatusEnum switch
|
||||
{
|
||||
PredictionStatus.Starting => "Starting transformation...",
|
||||
PredictionStatus.Processing => "Processing image...",
|
||||
_ => $"Status: {e.NewStatus}"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPredictionCompleted(object? sender, PredictionCompletedEventArgs e)
|
||||
{
|
||||
_historyService.TrackPrediction(e.Prediction);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
if (e.Succeeded)
|
||||
{
|
||||
StatusLabel.Text = "Image transformation complete!";
|
||||
}
|
||||
else if (e.Failed)
|
||||
{
|
||||
StatusLabel.Text = $"Failed: {e.Prediction.Error ?? "Unknown error"}";
|
||||
}
|
||||
else if (e.Canceled)
|
||||
{
|
||||
StatusLabel.Text = "Transformation was canceled";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Test.Replicate/Pages/SettingsPage.xaml
Normal file
101
Test.Replicate/Pages/SettingsPage.xaml
Normal file
@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Test.Replicate.Pages.SettingsPage"
|
||||
Title="Settings">
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="16" Spacing="16">
|
||||
|
||||
<!-- API Configuration -->
|
||||
<Frame Padding="16"
|
||||
BackgroundColor="{AppThemeBinding Light=#FFF8E1, Dark=#3E2723}"
|
||||
BorderColor="{AppThemeBinding Light=#FFD54F, Dark=#8D6E63}"
|
||||
CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="12">
|
||||
<Label Text="API Configuration"
|
||||
FontSize="18"
|
||||
FontAttributes="Bold"/>
|
||||
<Label Text="Enter your Replicate API token to enable transformations."
|
||||
FontSize="12"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"/>
|
||||
<Entry x:Name="ApiTokenEntry"
|
||||
Placeholder="Enter your Replicate API token"
|
||||
IsPassword="True"/>
|
||||
<Button x:Name="SaveTokenButton"
|
||||
Text="Save Token"
|
||||
BackgroundColor="{AppThemeBinding Light=#FF9800, Dark=#FF9800}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
Clicked="OnSaveTokenClicked"/>
|
||||
<Label x:Name="TokenStatusLabel"
|
||||
Text=""
|
||||
FontSize="12"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Available Models Info -->
|
||||
<Frame Padding="16"
|
||||
BackgroundColor="{AppThemeBinding Light=#E8F5E9, Dark=#1B5E20}"
|
||||
BorderColor="{AppThemeBinding Light=#81C784, Dark=#4CAF50}"
|
||||
CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="12">
|
||||
<Label Text="Available Models"
|
||||
FontSize="18"
|
||||
FontAttributes="Bold"/>
|
||||
<Label Text="Image Models:"
|
||||
FontSize="14"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{AppThemeBinding Light=#2E7D32, Dark=#A5D6A7}"/>
|
||||
<Label x:Name="ImageModelsLabel"
|
||||
FontSize="12"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"/>
|
||||
<Label Text="Video Models:"
|
||||
FontSize="14"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{AppThemeBinding Light=#1565C0, Dark=#90CAF9}"/>
|
||||
<Label x:Name="VideoModelsLabel"
|
||||
FontSize="12"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"/>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- About -->
|
||||
<Frame Padding="16"
|
||||
BackgroundColor="{AppThemeBinding Light=#F5F5F5, Dark=#424242}"
|
||||
BorderColor="{AppThemeBinding Light=#E0E0E0, Dark=#616161}"
|
||||
CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="About Replicate.Maui"
|
||||
FontSize="18"
|
||||
FontAttributes="Bold"/>
|
||||
<Label Text="A .NET MAUI control library for integrating Replicate AI models into your mobile and desktop applications."
|
||||
FontSize="12"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"/>
|
||||
<Label Text="Features:"
|
||||
FontSize="12"
|
||||
FontAttributes="Bold"
|
||||
Margin="0,8,0,0"/>
|
||||
<Label Text="• Drop-in UI control with image capture/pick"
|
||||
FontSize="11"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"/>
|
||||
<Label Text="• Multiple model presets (HiDream, SDXL, Luma, Seedance, etc.)"
|
||||
FontSize="11"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"/>
|
||||
<Label Text="• BYOK (Bring Your Own Key) support"
|
||||
FontSize="11"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"/>
|
||||
<Label Text="• Prediction tracking and history"
|
||||
FontSize="11"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"/>
|
||||
<Label Text="• Webhook and sync mode support"
|
||||
FontSize="11"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"/>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</ContentPage>
|
||||
68
Test.Replicate/Pages/SettingsPage.xaml.cs
Normal file
68
Test.Replicate/Pages/SettingsPage.xaml.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using MarketAlly.Replicate.Maui;
|
||||
using Test.Replicate.Services;
|
||||
|
||||
namespace Test.Replicate.Pages
|
||||
{
|
||||
public partial class SettingsPage : ContentPage
|
||||
{
|
||||
private readonly AppSettings _settings;
|
||||
|
||||
public SettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_settings = App.ServiceProvider.GetRequiredService<AppSettings>();
|
||||
LoadSettings();
|
||||
DisplayAvailableModels();
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
ApiTokenEntry.Text = _settings.ApiToken;
|
||||
UpdateTokenStatus();
|
||||
}
|
||||
|
||||
private void DisplayAvailableModels()
|
||||
{
|
||||
// Display image models
|
||||
var imageModels = ModelPresets.ImageModels
|
||||
.Select(p => $"• {p.Name} ({p.ModelName})")
|
||||
.ToList();
|
||||
ImageModelsLabel.Text = string.Join("\n", imageModels);
|
||||
|
||||
// Display video models
|
||||
var videoModels = ModelPresets.VideoModels
|
||||
.Select(p => $"• {p.Name} ({p.ModelName})")
|
||||
.ToList();
|
||||
VideoModelsLabel.Text = string.Join("\n", videoModels);
|
||||
}
|
||||
|
||||
private void OnSaveTokenClicked(object? sender, EventArgs e)
|
||||
{
|
||||
var token = ApiTokenEntry.Text?.Trim();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
TokenStatusLabel.Text = "Please enter a valid token";
|
||||
TokenStatusLabel.TextColor = Colors.Red;
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.ApiToken = token;
|
||||
_settings.NotifySettingsChanged();
|
||||
UpdateTokenStatus();
|
||||
}
|
||||
|
||||
private void UpdateTokenStatus()
|
||||
{
|
||||
if (_settings.IsConfigured)
|
||||
{
|
||||
TokenStatusLabel.Text = "Token configured - you're ready to go!";
|
||||
TokenStatusLabel.TextColor = Colors.Green;
|
||||
}
|
||||
else
|
||||
{
|
||||
TokenStatusLabel.Text = "Token not set";
|
||||
TokenStatusLabel.TextColor = Colors.Orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
170
Test.Replicate/Pages/VideoGenerationPage.xaml
Normal file
170
Test.Replicate/Pages/VideoGenerationPage.xaml
Normal file
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:replicate="clr-namespace:MarketAlly.Replicate.Maui.Controls;assembly=MarketAlly.Replicate.Maui"
|
||||
x:Class="Test.Replicate.Pages.VideoGenerationPage"
|
||||
Title="Video Generation">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto" Padding="16" RowSpacing="12">
|
||||
|
||||
<!-- Header -->
|
||||
<VerticalStackLayout Grid.Row="0" Spacing="4">
|
||||
<Label Text="Video Generation"
|
||||
FontSize="22"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label Text="Generate animated videos from your images"
|
||||
FontSize="13"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Model Selection and Options -->
|
||||
<Frame Grid.Row="1"
|
||||
Padding="12"
|
||||
BackgroundColor="{AppThemeBinding Light=#F3E5F5, Dark=#4A148C}"
|
||||
BorderColor="{AppThemeBinding Light=#BA68C8, Dark=#9C27B0}"
|
||||
CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<!-- Model Selection -->
|
||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="8">
|
||||
<VerticalStackLayout Grid.Column="0" Spacing="4">
|
||||
<Label Text="Model:" FontSize="12" FontAttributes="Bold"/>
|
||||
<Picker x:Name="VideoModelPicker"
|
||||
FontSize="13"
|
||||
SelectedIndexChanged="OnVideoModelChanged"/>
|
||||
</VerticalStackLayout>
|
||||
<VerticalStackLayout Grid.Column="1" Spacing="4">
|
||||
<Label Text="Duration:" FontSize="12" FontAttributes="Bold"/>
|
||||
<Picker x:Name="DurationPicker" FontSize="13"/>
|
||||
</VerticalStackLayout>
|
||||
</Grid>
|
||||
|
||||
<!-- Model-specific options -->
|
||||
<Grid x:Name="AspectRatioGrid" ColumnDefinitions="*,*" ColumnSpacing="8">
|
||||
<VerticalStackLayout Grid.Column="0" Spacing="4">
|
||||
<Label Text="Aspect Ratio:" FontSize="11"/>
|
||||
<Picker x:Name="AspectRatioPicker" FontSize="12">
|
||||
<Picker.ItemsSource>
|
||||
<x:Array Type="{x:Type x:String}">
|
||||
<x:String>16:9</x:String>
|
||||
<x:String>9:16</x:String>
|
||||
<x:String>4:3</x:String>
|
||||
<x:String>3:4</x:String>
|
||||
<x:String>1:1</x:String>
|
||||
</x:Array>
|
||||
</Picker.ItemsSource>
|
||||
</Picker>
|
||||
</VerticalStackLayout>
|
||||
<VerticalStackLayout x:Name="ResolutionStack" Grid.Column="1" Spacing="4">
|
||||
<Label Text="Resolution:" FontSize="11"/>
|
||||
<Picker x:Name="ResolutionPicker" FontSize="12">
|
||||
<Picker.ItemsSource>
|
||||
<x:Array Type="{x:Type x:String}">
|
||||
<x:String>480p</x:String>
|
||||
<x:String>720p</x:String>
|
||||
<x:String>1080p</x:String>
|
||||
</x:Array>
|
||||
</Picker.ItemsSource>
|
||||
</Picker>
|
||||
</VerticalStackLayout>
|
||||
</Grid>
|
||||
|
||||
<!-- Luma-specific: Loop option -->
|
||||
<HorizontalStackLayout x:Name="LoopStack" Spacing="8" IsVisible="False">
|
||||
<CheckBox x:Name="LoopCheckBox" IsChecked="False"/>
|
||||
<Label Text="Loop video (seamless playback)"
|
||||
FontSize="11"
|
||||
VerticalOptions="Center"/>
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<!-- Seedance-specific: Camera fixed -->
|
||||
<HorizontalStackLayout x:Name="CameraFixedStack" Spacing="8" IsVisible="False">
|
||||
<CheckBox x:Name="CameraFixedCheckBox" IsChecked="False"/>
|
||||
<Label Text="Fix camera position"
|
||||
FontSize="11"
|
||||
VerticalOptions="Center"/>
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<!-- Prompt -->
|
||||
<VerticalStackLayout Spacing="4">
|
||||
<Label Text="Prompt (optional override):" FontSize="12" FontAttributes="Bold"/>
|
||||
<Editor x:Name="PromptEditor"
|
||||
Placeholder="Leave empty to use model's default prompt..."
|
||||
HeightRequest="50"
|
||||
FontSize="12"/>
|
||||
</VerticalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Transformer Control (Video) - SideBySide Layout -->
|
||||
<replicate:ReplicateTransformerView
|
||||
x:Name="TransformerView"
|
||||
Grid.Row="2"
|
||||
LayoutMode="SideBySide"
|
||||
ShowVideoOptions="True"
|
||||
TrackPredictions="True"
|
||||
AutoTransformOnSelect="True"
|
||||
AutoTransformType="Video"
|
||||
SideBySideTransformType="Video"
|
||||
ShowSideBySideSourceButtons="True"
|
||||
ShowSideBySideResultButtons="True"
|
||||
ShowSideBySideTransformButton="True"
|
||||
DefaultButtonDisplayMode="Both"
|
||||
SideBySideCaptureIconText="📸"
|
||||
SideBySideCaptureLabel="Take"
|
||||
SideBySidePickIconText="🖼"
|
||||
SideBySidePickLabel="Pick"
|
||||
SideBySideTransformIconText="🎬"
|
||||
SideBySideTransformLabel="Generate"
|
||||
SideBySideClearIconText="🗑"
|
||||
SideBySideClearLabel="Clear"
|
||||
SideBySideRedoIconText="↺"
|
||||
SideBySideRedoLabel="Redo"
|
||||
PlaceholderText="Select image"/>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<Button x:Name="CancelButton"
|
||||
Grid.Row="3"
|
||||
Text="Cancel Generation"
|
||||
BackgroundColor="{AppThemeBinding Light=#F44336, Dark=#EF5350}"
|
||||
TextColor="White"
|
||||
CornerRadius="8"
|
||||
HeightRequest="44"
|
||||
IsVisible="False"
|
||||
Clicked="OnCancelClicked"/>
|
||||
|
||||
<!-- Status Area -->
|
||||
<Frame Grid.Row="4"
|
||||
Padding="12"
|
||||
BackgroundColor="{AppThemeBinding Light=#F0F0F0, Dark=#2A2A2A}"
|
||||
BorderColor="Transparent"
|
||||
CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="6">
|
||||
<Label x:Name="StatusLabel"
|
||||
Text="Configure your API token in Settings to get started"
|
||||
FontSize="14"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"/>
|
||||
<Label x:Name="PredictionIdLabel"
|
||||
Text=""
|
||||
FontSize="11"
|
||||
TextColor="{AppThemeBinding Light=#888888, Dark=#777777}"
|
||||
HorizontalOptions="Center"/>
|
||||
<Label x:Name="ResultUrlLabel"
|
||||
Text=""
|
||||
FontSize="12"
|
||||
TextColor="{AppThemeBinding Light=#007AFF, Dark=#0A84FF}"
|
||||
HorizontalOptions="Center"
|
||||
LineBreakMode="MiddleTruncation"
|
||||
MaxLines="2"/>
|
||||
<Label x:Name="MetricsLabel"
|
||||
Text=""
|
||||
FontSize="11"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#999999}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
329
Test.Replicate/Pages/VideoGenerationPage.xaml.cs
Normal file
329
Test.Replicate/Pages/VideoGenerationPage.xaml.cs
Normal file
@ -0,0 +1,329 @@
|
||||
using MarketAlly.Replicate.Maui;
|
||||
using MarketAlly.Replicate.Maui.Controls;
|
||||
using Test.Replicate.Services;
|
||||
|
||||
namespace Test.Replicate.Pages
|
||||
{
|
||||
public partial class VideoGenerationPage : ContentPage
|
||||
{
|
||||
private readonly AppSettings _appSettings;
|
||||
private readonly IReplicateTransformerFactory _factory;
|
||||
private readonly HistoryService _historyService;
|
||||
private readonly List<ModelPreset> _videoPresets;
|
||||
|
||||
public VideoGenerationPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_appSettings = App.ServiceProvider.GetRequiredService<AppSettings>();
|
||||
_factory = App.ServiceProvider.GetRequiredService<IReplicateTransformerFactory>();
|
||||
_historyService = App.ServiceProvider.GetRequiredService<HistoryService>();
|
||||
|
||||
// Get available video presets
|
||||
_videoPresets = ModelPresets.VideoModels.ToList();
|
||||
|
||||
// Initialize with factory
|
||||
TransformerView.Initialize(_factory);
|
||||
|
||||
// Populate model picker
|
||||
VideoModelPicker.ItemsSource = _videoPresets.Select(p => p.Name).ToList();
|
||||
VideoModelPicker.SelectedIndex = 0;
|
||||
|
||||
// Set default selections
|
||||
AspectRatioPicker.SelectedIndex = 0;
|
||||
ResolutionPicker.SelectedIndex = 1; // 720p
|
||||
|
||||
// Subscribe to control events
|
||||
TransformerView.ImageSelected += OnImageSelected;
|
||||
TransformerView.TransformationStarted += OnTransformationStarted;
|
||||
TransformerView.TransformationCompleted += OnTransformationCompleted;
|
||||
TransformerView.TransformationError += OnTransformationError;
|
||||
|
||||
// Subscribe to prediction tracking events (forwarded from internal tracker)
|
||||
TransformerView.PredictionTracked += OnPredictionTracked;
|
||||
TransformerView.PredictionCompleted += OnPredictionCompleted;
|
||||
|
||||
// Show/hide cancel button based on processing state
|
||||
TransformerView.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(TransformerView.IsProcessing))
|
||||
{
|
||||
CancelButton.IsVisible = TransformerView.IsProcessing;
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for settings changes
|
||||
_appSettings.SettingsChanged += OnSettingsChanged;
|
||||
|
||||
// Apply initial settings
|
||||
UpdateModelOptions();
|
||||
ApplySettings();
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
ApplySettings();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private void OnSettingsChanged()
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
ApplySettings();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnVideoModelChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateModelOptions();
|
||||
ApplySelectedPreset();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private void UpdateModelOptions()
|
||||
{
|
||||
if (VideoModelPicker.SelectedIndex < 0 || VideoModelPicker.SelectedIndex >= _videoPresets.Count)
|
||||
return;
|
||||
|
||||
var preset = _videoPresets[VideoModelPicker.SelectedIndex];
|
||||
|
||||
// Update duration options based on model
|
||||
if (preset.Name.Contains("Luma"))
|
||||
{
|
||||
DurationPicker.ItemsSource = new List<int> { 5, 9 };
|
||||
LoopStack.IsVisible = true;
|
||||
CameraFixedStack.IsVisible = false;
|
||||
ResolutionStack.IsVisible = false;
|
||||
}
|
||||
else if (preset.Name.Contains("Seedance"))
|
||||
{
|
||||
DurationPicker.ItemsSource = new List<int> { 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
|
||||
LoopStack.IsVisible = false;
|
||||
CameraFixedStack.IsVisible = true;
|
||||
ResolutionStack.IsVisible = true;
|
||||
}
|
||||
else if (preset.Name.Contains("Kling"))
|
||||
{
|
||||
DurationPicker.ItemsSource = new List<int> { 5, 10 };
|
||||
LoopStack.IsVisible = false;
|
||||
CameraFixedStack.IsVisible = false;
|
||||
ResolutionStack.IsVisible = false;
|
||||
}
|
||||
else if (preset.Name.Contains("Wan"))
|
||||
{
|
||||
DurationPicker.ItemsSource = new List<int> { 5 };
|
||||
LoopStack.IsVisible = false;
|
||||
CameraFixedStack.IsVisible = false;
|
||||
ResolutionStack.IsVisible = false;
|
||||
}
|
||||
else if (preset.Name.Contains("Veo"))
|
||||
{
|
||||
// Google Veo 3 Fast only supports 4, 6, or 8 seconds
|
||||
DurationPicker.ItemsSource = new List<int> { 4, 6, 8 };
|
||||
LoopStack.IsVisible = false;
|
||||
CameraFixedStack.IsVisible = false;
|
||||
ResolutionStack.IsVisible = true; // Veo supports 720p/1080p
|
||||
}
|
||||
else
|
||||
{
|
||||
DurationPicker.ItemsSource = new List<int> { 5, 10 };
|
||||
LoopStack.IsVisible = false;
|
||||
CameraFixedStack.IsVisible = false;
|
||||
ResolutionStack.IsVisible = false;
|
||||
}
|
||||
|
||||
DurationPicker.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void ApplySettings()
|
||||
{
|
||||
if (!_appSettings.IsConfigured)
|
||||
return;
|
||||
|
||||
// Configure API token
|
||||
TransformerView.ConfigureSettings(settings =>
|
||||
{
|
||||
settings.ApiToken = _appSettings.ApiToken;
|
||||
});
|
||||
|
||||
// Apply selected preset
|
||||
ApplySelectedPreset();
|
||||
}
|
||||
|
||||
private void ApplySelectedPreset()
|
||||
{
|
||||
if (VideoModelPicker.SelectedIndex < 0 || VideoModelPicker.SelectedIndex >= _videoPresets.Count)
|
||||
return;
|
||||
|
||||
var preset = _videoPresets[VideoModelPicker.SelectedIndex];
|
||||
|
||||
// Build custom parameters based on UI selections
|
||||
var customParams = new Dictionary<string, object>();
|
||||
|
||||
// Duration
|
||||
if (DurationPicker.SelectedItem is int duration)
|
||||
{
|
||||
customParams["duration"] = duration;
|
||||
}
|
||||
|
||||
// Aspect ratio (most models support this)
|
||||
if (AspectRatioPicker.SelectedItem is string aspectRatio)
|
||||
{
|
||||
customParams["aspect_ratio"] = aspectRatio;
|
||||
}
|
||||
|
||||
// Model-specific parameters
|
||||
if (preset.Name.Contains("Luma"))
|
||||
{
|
||||
customParams["loop"] = LoopCheckBox.IsChecked;
|
||||
}
|
||||
else if (preset.Name.Contains("Seedance"))
|
||||
{
|
||||
customParams["camera_fixed"] = CameraFixedCheckBox.IsChecked;
|
||||
if (ResolutionPicker.SelectedItem is string resolution)
|
||||
{
|
||||
customParams["resolution"] = resolution;
|
||||
}
|
||||
}
|
||||
|
||||
TransformerView.SetVideoPreset(preset, customParams);
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
if (!_appSettings.IsConfigured)
|
||||
{
|
||||
StatusLabel.Text = "Configure your API token in Settings to get started";
|
||||
}
|
||||
else if (VideoModelPicker.SelectedIndex >= 0 && VideoModelPicker.SelectedIndex < _videoPresets.Count)
|
||||
{
|
||||
var preset = _videoPresets[VideoModelPicker.SelectedIndex];
|
||||
StatusLabel.Text = $"Ready with {preset.Name} - select an image to animate";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusLabel.Text = "Ready - select an image to generate video";
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnCancelClicked(object? sender, EventArgs e)
|
||||
{
|
||||
StatusLabel.Text = "Canceling...";
|
||||
await TransformerView.CancelTransformationAsync();
|
||||
StatusLabel.Text = "Video generation canceled";
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
private void OnImageSelected(object? sender, ImageSelectedEventArgs e)
|
||||
{
|
||||
if (!_appSettings.IsConfigured)
|
||||
{
|
||||
StatusLabel.Text = "Please configure your API token in Settings first";
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply custom prompt if provided
|
||||
var customPrompt = PromptEditor.Text?.Trim();
|
||||
TransformerView.CustomVideoPrompt = string.IsNullOrEmpty(customPrompt) ? null : customPrompt;
|
||||
|
||||
// Re-apply preset with current settings
|
||||
ApplySelectedPreset();
|
||||
|
||||
var modelName = VideoModelPicker.SelectedIndex >= 0 && VideoModelPicker.SelectedIndex < _videoPresets.Count
|
||||
? _videoPresets[VideoModelPicker.SelectedIndex].Name
|
||||
: "selected model";
|
||||
|
||||
StatusLabel.Text = $"Image selected ({e.ImageBytes.Length / 1024:N0} KB) - tap Generate Video with {modelName}";
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
private void OnTransformationStarted(object? sender, TransformationStartedEventArgs e)
|
||||
{
|
||||
if (e.Type != TransformationType.Video)
|
||||
return;
|
||||
|
||||
// Update prompt right before transformation
|
||||
var customPrompt = PromptEditor.Text?.Trim();
|
||||
TransformerView.CustomVideoPrompt = string.IsNullOrEmpty(customPrompt) ? null : customPrompt;
|
||||
|
||||
var modelName = TransformerView.VideoPreset?.Name ?? "model";
|
||||
StatusLabel.Text = $"Generating video with {modelName} (this may take several minutes)...";
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
private void OnTransformationCompleted(object? sender, TransformationCompletedEventArgs e)
|
||||
{
|
||||
if (e.Type != TransformationType.Video)
|
||||
return;
|
||||
|
||||
var result = e.Result;
|
||||
var modelName = TransformerView.VideoPreset?.Name ?? "generation";
|
||||
|
||||
StatusLabel.Text = $"{modelName} complete!";
|
||||
PredictionIdLabel.Text = $"Prediction ID: {result.Id}";
|
||||
ResultUrlLabel.Text = result.Output ?? "No output URL";
|
||||
|
||||
if (result.Metrics != null)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (result.Metrics.PredictTime.HasValue)
|
||||
parts.Add($"Predict: {result.Metrics.PredictTime:F1}s");
|
||||
if (result.Metrics.TotalTime.HasValue)
|
||||
parts.Add($"Total: {result.Metrics.TotalTime:F1}s");
|
||||
MetricsLabel.Text = string.Join(" | ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTransformationError(object? sender, TransformationErrorEventArgs e)
|
||||
{
|
||||
StatusLabel.Text = $"Error: {e.Error.Message}";
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
private void ClearResults()
|
||||
{
|
||||
PredictionIdLabel.Text = "";
|
||||
ResultUrlLabel.Text = "";
|
||||
MetricsLabel.Text = "";
|
||||
}
|
||||
|
||||
private void OnPredictionTracked(object? sender, PredictionStatusChangedEventArgs e)
|
||||
{
|
||||
_historyService.TrackPrediction(e.Prediction);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
StatusLabel.Text = e.NewStatusEnum switch
|
||||
{
|
||||
PredictionStatus.Starting => "Starting video generation...",
|
||||
PredictionStatus.Processing => "Generating video (this may take several minutes)...",
|
||||
_ => $"Status: {e.NewStatus}"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPredictionCompleted(object? sender, PredictionCompletedEventArgs e)
|
||||
{
|
||||
_historyService.TrackPrediction(e.Prediction);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
if (e.Succeeded)
|
||||
{
|
||||
StatusLabel.Text = "Video generation complete!";
|
||||
}
|
||||
else if (e.Failed)
|
||||
{
|
||||
StatusLabel.Text = $"Failed: {e.Prediction.Error ?? "Unknown error"}";
|
||||
}
|
||||
else if (e.Canceled)
|
||||
{
|
||||
StatusLabel.Text = "Video generation was canceled";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Test.Replicate/Platforms/Android/AndroidManifest.xml
Normal file
6
Test.Replicate/Platforms/Android/AndroidManifest.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
11
Test.Replicate/Platforms/Android/MainActivity.cs
Normal file
11
Test.Replicate/Platforms/Android/MainActivity.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
|
||||
namespace Test.Replicate
|
||||
{
|
||||
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
||||
public class MainActivity : MauiAppCompatActivity
|
||||
{
|
||||
}
|
||||
}
|
||||
16
Test.Replicate/Platforms/Android/MainApplication.cs
Normal file
16
Test.Replicate/Platforms/Android/MainApplication.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Android.App;
|
||||
using Android.Runtime;
|
||||
|
||||
namespace Test.Replicate
|
||||
{
|
||||
[Application]
|
||||
public class MainApplication : MauiApplication
|
||||
{
|
||||
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
|
||||
: base(handle, ownership)
|
||||
{
|
||||
}
|
||||
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#512BD4</color>
|
||||
<color name="colorPrimaryDark">#2B0B98</color>
|
||||
<color name="colorAccent">#2B0B98</color>
|
||||
</resources>
|
||||
10
Test.Replicate/Platforms/MacCatalyst/AppDelegate.cs
Normal file
10
Test.Replicate/Platforms/MacCatalyst/AppDelegate.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using Foundation;
|
||||
|
||||
namespace Test.Replicate
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
}
|
||||
14
Test.Replicate/Platforms/MacCatalyst/Entitlements.plist
Normal file
14
Test.Replicate/Platforms/MacCatalyst/Entitlements.plist
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
|
||||
<dict>
|
||||
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
38
Test.Replicate/Platforms/MacCatalyst/Info.plist
Normal file
38
Test.Replicate/Platforms/MacCatalyst/Info.plist
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- The Mac App Store requires you specify if the app uses encryption. -->
|
||||
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
|
||||
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
|
||||
<!-- Please indicate <true/> or <false/> here. -->
|
||||
|
||||
<!-- Specify the category for your app here. -->
|
||||
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
|
||||
<!-- <key>LSApplicationCategoryType</key> -->
|
||||
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/appicon.appiconset</string>
|
||||
</dict>
|
||||
</plist>
|
||||
16
Test.Replicate/Platforms/MacCatalyst/Program.cs
Normal file
16
Test.Replicate/Platforms/MacCatalyst/Program.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Test.Replicate
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
// This is the main entry point of the application.
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// if you want to use a different Application Delegate class from "AppDelegate"
|
||||
// you can specify it here.
|
||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Test.Replicate/Platforms/Tizen/Main.cs
Normal file
17
Test.Replicate/Platforms/Tizen/Main.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Microsoft.Maui;
|
||||
using Microsoft.Maui.Hosting;
|
||||
using System;
|
||||
|
||||
namespace Test.Replicate
|
||||
{
|
||||
internal class Program : MauiApplication
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var app = new Program();
|
||||
app.Run(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Test.Replicate/Platforms/Tizen/tizen-manifest.xml
Normal file
15
Test.Replicate/Platforms/Tizen/tizen-manifest.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="9" xmlns="http://tizen.org/ns/packages">
|
||||
<profile name="common" />
|
||||
<ui-application appid="maui-application-id-placeholder" exec="Test.Replicate.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
|
||||
<label>maui-application-title-placeholder</label>
|
||||
<icon>maui-appicon-placeholder</icon>
|
||||
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
|
||||
</ui-application>
|
||||
<shortcut-list />
|
||||
<privileges>
|
||||
<privilege>http://tizen.org/privilege/internet</privilege>
|
||||
</privileges>
|
||||
<dependencies />
|
||||
<provides-appdefined-privileges />
|
||||
</manifest>
|
||||
8
Test.Replicate/Platforms/Windows/App.xaml
Normal file
8
Test.Replicate/Platforms/Windows/App.xaml
Normal file
@ -0,0 +1,8 @@
|
||||
<maui:MauiWinUIApplication
|
||||
x:Class="Test.Replicate.WinUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:maui="using:Microsoft.Maui"
|
||||
xmlns:local="using:Test.Replicate.WinUI">
|
||||
|
||||
</maui:MauiWinUIApplication>
|
||||
25
Test.Replicate/Platforms/Windows/App.xaml.cs
Normal file
25
Test.Replicate/Platforms/Windows/App.xaml.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Test.Replicate.WinUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
public partial class App : MauiWinUIApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
|
||||
}
|
||||
46
Test.Replicate/Platforms/Windows/Package.appxmanifest
Normal file
46
Test.Replicate/Platforms/Windows/Package.appxmanifest
Normal file
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="D7ACD08E-EC01-4AFB-AE69-5B4B6BDC7A1E" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>$placeholder$</DisplayName>
|
||||
<PublisherDisplayName>User Name</PublisherDisplayName>
|
||||
<Logo>$placeholder$.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate" />
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="$placeholder$"
|
||||
Description="$placeholder$"
|
||||
Square150x150Logo="$placeholder$.png"
|
||||
Square44x44Logo="$placeholder$.png"
|
||||
BackgroundColor="transparent">
|
||||
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
|
||||
<uap:SplashScreen Image="$placeholder$.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
|
||||
</Package>
|
||||
15
Test.Replicate/Platforms/Windows/app.manifest
Normal file
15
Test.Replicate/Platforms/Windows/app.manifest
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="Test.Replicate.WinUI.app"/>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- The combination of below two tags have the following effect:
|
||||
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||
2) System < Windows 10 Anniversary Update
|
||||
-->
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
10
Test.Replicate/Platforms/iOS/AppDelegate.cs
Normal file
10
Test.Replicate/Platforms/iOS/AppDelegate.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using Foundation;
|
||||
|
||||
namespace Test.Replicate
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
}
|
||||
32
Test.Replicate/Platforms/iOS/Info.plist
Normal file
32
Test.Replicate/Platforms/iOS/Info.plist
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/appicon.appiconset</string>
|
||||
</dict>
|
||||
</plist>
|
||||
16
Test.Replicate/Platforms/iOS/Program.cs
Normal file
16
Test.Replicate/Platforms/iOS/Program.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Test.Replicate
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
// This is the main entry point of the application.
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// if you want to use a different Application Delegate class from "AppDelegate"
|
||||
// you can specify it here.
|
||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Test.Replicate/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
Normal file
51
Test.Replicate/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps.
|
||||
The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK.
|
||||
|
||||
You are responsible for adding extra entries as needed for your application.
|
||||
|
||||
More information: https://aka.ms/maui-privacy-manifest
|
||||
-->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>E174.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!--
|
||||
The entry below is only needed when you're using the Preferences API in your app.
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict> -->
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
8
Test.Replicate/Properties/launchSettings.json
Normal file
8
Test.Replicate/Properties/launchSettings.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Windows Machine": {
|
||||
"commandName": "Project",
|
||||
"nativeDebugging": false
|
||||
}
|
||||
}
|
||||
}
|
||||
4
Test.Replicate/Resources/AppIcon/appicon.svg
Normal file
4
Test.Replicate/Resources/AppIcon/appicon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 228 B |
8
Test.Replicate/Resources/AppIcon/appiconfg.svg
Normal file
8
Test.Replicate/Resources/AppIcon/appiconfg.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
Test.Replicate/Resources/Fonts/OpenSans-Regular.ttf
Normal file
BIN
Test.Replicate/Resources/Fonts/OpenSans-Regular.ttf
Normal file
Binary file not shown.
BIN
Test.Replicate/Resources/Fonts/OpenSans-Semibold.ttf
Normal file
BIN
Test.Replicate/Resources/Fonts/OpenSans-Semibold.ttf
Normal file
Binary file not shown.
BIN
Test.Replicate/Resources/Images/dotnet_bot.png
Normal file
BIN
Test.Replicate/Resources/Images/dotnet_bot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
15
Test.Replicate/Resources/Raw/AboutAssets.txt
Normal file
15
Test.Replicate/Resources/Raw/AboutAssets.txt
Normal file
@ -0,0 +1,15 @@
|
||||
Any raw assets you want to be deployed with your application can be placed in
|
||||
this directory (and child directories). Deployment of the asset to your application
|
||||
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
|
||||
|
||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
|
||||
These files will be deployed with your package and will be accessible using Essentials:
|
||||
|
||||
async Task LoadMauiAsset()
|
||||
{
|
||||
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var contents = reader.ReadToEnd();
|
||||
}
|
||||
8
Test.Replicate/Resources/Splash/splash.svg
Normal file
8
Test.Replicate/Resources/Splash/splash.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
45
Test.Replicate/Resources/Styles/Colors.xaml
Normal file
45
Test.Replicate/Resources/Styles/Colors.xaml
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xaml-comp compile="true" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||
|
||||
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
|
||||
|
||||
<Color x:Key="Primary">#512BD4</Color>
|
||||
<Color x:Key="PrimaryDark">#ac99ea</Color>
|
||||
<Color x:Key="PrimaryDarkText">#242424</Color>
|
||||
<Color x:Key="Secondary">#DFD8F7</Color>
|
||||
<Color x:Key="SecondaryDarkText">#9880e5</Color>
|
||||
<Color x:Key="Tertiary">#2B0B98</Color>
|
||||
|
||||
<Color x:Key="White">White</Color>
|
||||
<Color x:Key="Black">Black</Color>
|
||||
<Color x:Key="Magenta">#D600AA</Color>
|
||||
<Color x:Key="MidnightBlue">#190649</Color>
|
||||
<Color x:Key="OffBlack">#1f1f1f</Color>
|
||||
|
||||
<Color x:Key="Gray100">#E1E1E1</Color>
|
||||
<Color x:Key="Gray200">#C8C8C8</Color>
|
||||
<Color x:Key="Gray300">#ACACAC</Color>
|
||||
<Color x:Key="Gray400">#919191</Color>
|
||||
<Color x:Key="Gray500">#6E6E6E</Color>
|
||||
<Color x:Key="Gray600">#404040</Color>
|
||||
<Color x:Key="Gray900">#212121</Color>
|
||||
<Color x:Key="Gray950">#141414</Color>
|
||||
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
|
||||
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
|
||||
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
|
||||
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
|
||||
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
|
||||
|
||||
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
|
||||
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
|
||||
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
|
||||
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
|
||||
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
|
||||
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
|
||||
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
|
||||
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
|
||||
</ResourceDictionary>
|
||||
440
Test.Replicate/Resources/Styles/Styles.xaml
Normal file
440
Test.Replicate/Resources/Styles/Styles.xaml
Normal file
@ -0,0 +1,440 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xaml-comp compile="true" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||
|
||||
<Style TargetType="ActivityIndicator">
|
||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="IndicatorView">
|
||||
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
|
||||
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="StrokeShape" Value="Rectangle"/>
|
||||
<Setter Property="StrokeThickness" Value="1"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="BoxView">
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="BorderWidth" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="Padding" Value="14,10"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="PointerOver" />
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DatePicker">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Editor">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Entry">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ImageButton">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
<Setter Property="BorderColor" Value="Transparent"/>
|
||||
<Setter Property="BorderWidth" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="0"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="PointerOver" />
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label" x:Key="Headline">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontSize" Value="32" />
|
||||
<Setter Property="HorizontalOptions" Value="Center" />
|
||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label" x:Key="SubHeadline">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontSize" Value="24" />
|
||||
<Setter Property="HorizontalOptions" Value="Center" />
|
||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListView">
|
||||
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Picker">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ProgressBar">
|
||||
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="RadioButton">
|
||||
<Setter Property="BackgroundColor" Value="Transparent"/>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="RefreshView">
|
||||
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="SearchBar">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
||||
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="SearchHandler">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Shadow">
|
||||
<Setter Property="Radius" Value="15" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Offset" Value="10,10" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Slider">
|
||||
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
||||
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="SwipeItem">
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Switch">
|
||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="ThumbColor" Value="{StaticResource White}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="On">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Off">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TimePicker">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent"/>
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
<Style TargetType="TitleBar">
|
||||
<Setter Property="MinimumHeightRequest" Value="32"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="TitleActiveStates">
|
||||
<VisualState x:Name="TitleBarTitleActive">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="TitleBarTitleInactive">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
-->
|
||||
|
||||
<Style TargetType="Page" ApplyToDerivedTypes="True">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Shell" ApplyToDerivedTypes="True">
|
||||
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
||||
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
||||
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="Shell.NavBarHasShadow" Value="False" />
|
||||
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="NavigationPage">
|
||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
||||
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TabbedPage">
|
||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
116
Test.Replicate/Services/AppSettings.cs
Normal file
116
Test.Replicate/Services/AppSettings.cs
Normal file
@ -0,0 +1,116 @@
|
||||
namespace Test.Replicate.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared application settings service for storing API configuration.
|
||||
/// Uses SecureStorage for sensitive data like API tokens.
|
||||
/// </summary>
|
||||
public class AppSettings
|
||||
{
|
||||
private const string ApiTokenKey = "replicate_api_token";
|
||||
private string _apiToken = string.Empty;
|
||||
private bool _isLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// The Replicate API token.
|
||||
/// </summary>
|
||||
public string ApiToken
|
||||
{
|
||||
get => _apiToken;
|
||||
set
|
||||
{
|
||||
if (_apiToken != value)
|
||||
{
|
||||
_apiToken = value;
|
||||
_ = SaveTokenAsync(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the API token has been configured.
|
||||
/// </summary>
|
||||
public bool IsConfigured => !string.IsNullOrEmpty(ApiToken);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when settings change.
|
||||
/// </summary>
|
||||
public event Action? SettingsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Notify listeners that settings have changed.
|
||||
/// </summary>
|
||||
public void NotifySettingsChanged() => SettingsChanged?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// Load the API token from secure storage.
|
||||
/// Call this on app startup.
|
||||
/// </summary>
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
if (_isLoaded)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var token = await SecureStorage.Default.GetAsync(ApiTokenKey);
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
_apiToken = token;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// SecureStorage may not be available on all platforms/emulators
|
||||
// Fall back to preferences
|
||||
_apiToken = Preferences.Default.Get(ApiTokenKey, string.Empty);
|
||||
}
|
||||
|
||||
_isLoaded = true;
|
||||
}
|
||||
|
||||
private async Task SaveTokenAsync(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
SecureStorage.Default.Remove(ApiTokenKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SecureStorage.Default.SetAsync(ApiTokenKey, token);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// SecureStorage may not be available on all platforms/emulators
|
||||
// Fall back to preferences
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
Preferences.Default.Remove(ApiTokenKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
Preferences.Default.Set(ApiTokenKey, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the stored API token.
|
||||
/// </summary>
|
||||
public async Task ClearTokenAsync()
|
||||
{
|
||||
_apiToken = string.Empty;
|
||||
try
|
||||
{
|
||||
SecureStorage.Default.Remove(ApiTokenKey);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Preferences.Default.Remove(ApiTokenKey);
|
||||
}
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Test.Replicate/Services/HistoryService.cs
Normal file
94
Test.Replicate/Services/HistoryService.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using MarketAlly.Replicate.Maui;
|
||||
|
||||
namespace Test.Replicate.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared service for tracking prediction history across pages.
|
||||
/// </summary>
|
||||
public class HistoryService
|
||||
{
|
||||
private readonly List<TrackedPrediction> _history = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Raised when history changes (prediction added or status updated).
|
||||
/// </summary>
|
||||
public event Action? HistoryChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Add or update a prediction in history.
|
||||
/// </summary>
|
||||
public void TrackPrediction(TrackedPrediction prediction)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var existing = _history.FirstOrDefault(p => p.Id == prediction.Id);
|
||||
if (existing != null)
|
||||
{
|
||||
// Update existing
|
||||
var index = _history.IndexOf(existing);
|
||||
_history[index] = prediction;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new
|
||||
_history.Insert(0, prediction);
|
||||
|
||||
// Trim to max 50 items
|
||||
while (_history.Count > 50)
|
||||
{
|
||||
_history.RemoveAt(_history.Count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HistoryChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all tracked predictions ordered by creation time (newest first).
|
||||
/// </summary>
|
||||
public IReadOnlyList<TrackedPrediction> GetHistory()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _history.OrderByDescending(p => p.CreatedAt).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get pending predictions only.
|
||||
/// </summary>
|
||||
public IReadOnlyList<TrackedPrediction> GetPending()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _history.Where(p => p.IsPending).OrderByDescending(p => p.CreatedAt).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear completed predictions.
|
||||
/// </summary>
|
||||
public void ClearCompleted()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_history.RemoveAll(p => p.IsCompleted);
|
||||
}
|
||||
HistoryChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all predictions.
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_history.Clear();
|
||||
}
|
||||
HistoryChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
71
Test.Replicate/Test.Replicate.csproj
Normal file
71
Test.Replicate/Test.Replicate.csproj
Normal file
@ -0,0 +1,71 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
|
||||
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
|
||||
|
||||
<!-- Note for MacCatalyst:
|
||||
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
|
||||
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
|
||||
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
|
||||
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
|
||||
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
|
||||
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>Test.Replicate</RootNamespace>
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<!-- Display name -->
|
||||
<ApplicationTitle>Test.Replicate</ApplicationTitle>
|
||||
|
||||
<!-- App Identifier -->
|
||||
<ApplicationId>com.companyname.test.replicate</ApplicationId>
|
||||
|
||||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
|
||||
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- App Icon -->
|
||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
||||
|
||||
<!-- Splash Screen -->
|
||||
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
|
||||
|
||||
<!-- Images -->
|
||||
<MauiImage Include="Resources\Images\*" />
|
||||
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<MauiFont Include="Resources\Fonts\*" />
|
||||
|
||||
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
|
||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Replicate.Maui\Replicate.Maui.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue
Block a user