470 строки
17 KiB
C#
470 строки
17 KiB
C#
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
#nullable enable
|
|
|
|
//#define DebugDragDrop
|
|
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using LottieViewer.ViewModel;
|
|
using Windows.ApplicationModel;
|
|
using Windows.ApplicationModel.DataTransfer;
|
|
using Windows.Foundation.Metadata;
|
|
using Windows.Storage;
|
|
using Windows.Storage.Pickers;
|
|
using Windows.UI;
|
|
using Windows.UI.Core;
|
|
using Windows.UI.Xaml;
|
|
using Windows.UI.Xaml.Controls;
|
|
using Windows.UI.Xaml.Controls.Primitives;
|
|
using Windows.UI.Xaml.Documents;
|
|
using Windows.UI.Xaml.Input;
|
|
|
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
|
|
|
namespace LottieViewer
|
|
{
|
|
/// <summary>
|
|
/// MainPage.
|
|
/// </summary>
|
|
public sealed partial class MainPage : Page, INotifyPropertyChanged
|
|
{
|
|
readonly ToggleButton[] _controlPanelButtons;
|
|
|
|
int _playControlToggleVersion;
|
|
int _playVersion;
|
|
|
|
public MainPage()
|
|
{
|
|
InitializeComponent();
|
|
|
|
// The control panel buttons. We hold onto these in order to ensure that no more
|
|
// than one is checked at the same time.
|
|
_controlPanelButtons = new[] { PaletteButton, InfoButton };
|
|
|
|
// Connect the player's progress to the scrubber's progress.
|
|
_scrubber.SetAnimatedCompositionObject(_stage.Player.ProgressObject);
|
|
|
|
// Add the background to the color picker so that it can be modified by the user.
|
|
_paletteColorPicker.PaletteEntries.Add(BackgroundColor);
|
|
|
|
// Update background visibility after checkbox clicked.
|
|
_paletteColorPicker.ShowSolidBackground.Click +=
|
|
(object sender, RoutedEventArgs e) => _stage.ShowSolidBackground = _paletteColorPicker.ShowSolidBackground.IsChecked ?? false;
|
|
|
|
// Get notified when info about the loaded Lottie changes.
|
|
_stage.DiagnosticsViewModel.PropertyChanged += DiagnosticsViewModel_PropertyChanged;
|
|
|
|
// Remove all of the control panel panes. They will be added back as needed.
|
|
ControlPanel.Children.Clear();
|
|
|
|
// Capture player by PixelView
|
|
_pixelView.SetElementToCapture(_stage.PlayerContainer);
|
|
}
|
|
|
|
public ObservableCollection<object> PropertiesList { get; } = new ObservableCollection<object>();
|
|
|
|
public ObservableCollection<object> MarkersList { get; } = new ObservableCollection<object>();
|
|
|
|
public string AppVersion
|
|
{
|
|
get
|
|
{
|
|
var version = Package.Current.Id.Version;
|
|
|
|
return string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
|
|
}
|
|
}
|
|
|
|
public string UapVersion
|
|
{
|
|
get
|
|
{
|
|
// Start testing on version 2. We know that at least version 1 is supported because
|
|
// we are running in UAP code.
|
|
var versionToTest = 2u;
|
|
|
|
// Keep querying until IsApiContractPresent fails to find the version.
|
|
while (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", (ushort)versionToTest))
|
|
{
|
|
// Keep looking ...
|
|
versionToTest++;
|
|
}
|
|
|
|
// Query failed on versionToTest. Return the previous version.
|
|
return (versionToTest - 1).ToString();
|
|
}
|
|
}
|
|
|
|
void DiagnosticsViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
{
|
|
var list = PropertiesList;
|
|
var viewModel = _stage.DiagnosticsViewModel;
|
|
|
|
if (viewModel is null)
|
|
{
|
|
list.Clear();
|
|
MarkersList.Clear();
|
|
}
|
|
else if (e.PropertyName == nameof(viewModel.FileName))
|
|
{
|
|
list.Clear();
|
|
MarkersList.Clear();
|
|
if (!string.IsNullOrWhiteSpace(viewModel.FileName))
|
|
{
|
|
list.Add(new PairOfStrings("File", viewModel.FileName));
|
|
}
|
|
|
|
// If the Lottie has 0 duration then it isn't valid, so don't show properties
|
|
// the only make sense for valid Lotties.
|
|
if (viewModel.LottieVisualDiagnostics?.Duration.Ticks > 0)
|
|
{
|
|
// Not all Lotties have a name, so only add the name if it exists.
|
|
if (!string.IsNullOrWhiteSpace(viewModel.Name))
|
|
{
|
|
list.Add(new PairOfStrings("Name", viewModel.Name));
|
|
}
|
|
|
|
list.Add(new PairOfStrings("Size", viewModel.SizeText));
|
|
list.Add(new PairOfStrings("Duration", viewModel.DurationText));
|
|
list.Add(new PairOfStrings("Frames", $"{viewModel.FrameCountText} @ {viewModel.FramesPerSecond:0.#}fps"));
|
|
|
|
if (viewModel.Markers.Count > 0)
|
|
{
|
|
foreach (var marker in viewModel.Markers)
|
|
{
|
|
MarkersList.Add(marker);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal ColorPaletteEntry BackgroundColor { get; } = new ColorPaletteEntry(Colors.White, "Background") { IsInitialColorSameAsColor = true };
|
|
|
|
void PickFile_Click(object sender, RoutedEventArgs e)
|
|
=> _ = OnPickFileAsync();
|
|
|
|
async Task OnPickFileAsync()
|
|
{
|
|
try
|
|
{
|
|
var playVersion = ++_playVersion;
|
|
|
|
var filePicker = new FileOpenPicker
|
|
{
|
|
ViewMode = PickerViewMode.List,
|
|
SuggestedStartLocation = PickerLocationId.ComputerFolder,
|
|
};
|
|
filePicker.FileTypeFilter.Add(".json");
|
|
filePicker.FileTypeFilter.Add(".lottie");
|
|
|
|
StorageFile? file = null;
|
|
try
|
|
{
|
|
file = await filePicker.PickSingleFileAsync();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore PickSingleFileAsync exceptions so they don't crash the process.
|
|
}
|
|
|
|
if (file is null)
|
|
{
|
|
// User declined to pick anything.
|
|
return;
|
|
}
|
|
|
|
if (playVersion != _playVersion)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Reset the scrubber to the 0 position.
|
|
_scrubber.Value = 0;
|
|
|
|
if (await _stage.TryLoadFileAsync(file))
|
|
{
|
|
// Loading succeeded, start playing.
|
|
_playStopButton.IsChecked = true;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// Uncheck the button. The button is a ToggleButton so that it indicates
|
|
// visually when the file picker is open. We need to manually reset its state.
|
|
PickFile.IsChecked = false;
|
|
}
|
|
}
|
|
|
|
// Avoid "async void" method. Not valid here because we handle all async exceptions.
|
|
#pragma warning disable VSTHRD100
|
|
async void LottieDragEnterHandler(object sender, DragEventArgs e)
|
|
{
|
|
#pragma warning restore VSTHRD100
|
|
DebugDragDrop("Drag enter");
|
|
|
|
// Only accept files.
|
|
if (e.DataView.Contains(StandardDataFormats.StorageItems))
|
|
{
|
|
// Get a deferral to keep the drag operation alive until the async
|
|
// methods have completed.
|
|
var deferral = e.GetDeferral();
|
|
try
|
|
{
|
|
var items = await e.DataView.GetStorageItemsAsync();
|
|
|
|
var filteredItems = items.Where(IsJsonOrLottieFile);
|
|
|
|
if (!filteredItems.Any() || filteredItems.Skip(1).Any())
|
|
{
|
|
DebugDragDrop("Drag enter - ignoring");
|
|
return;
|
|
}
|
|
|
|
// Exactly one item was selected.
|
|
e.AcceptedOperation = DataPackageOperation.Copy;
|
|
e.DragUIOverride.Caption = "Drop to view Lottie.";
|
|
}
|
|
catch
|
|
{
|
|
// Ignore async exception so they don't crash the process.
|
|
}
|
|
finally
|
|
{
|
|
DebugDragDrop("Completing drag deferral");
|
|
deferral.Complete();
|
|
}
|
|
|
|
DebugDragDrop("Doing drag enter");
|
|
_stage.DoDragEnter();
|
|
}
|
|
}
|
|
|
|
// Avoid "async void" method. Not valid here because we handle all async exceptions.
|
|
#pragma warning disable VSTHRD100
|
|
|
|
// Called when an item is dropped.
|
|
async void LottieDropHandler(object sender, DragEventArgs e)
|
|
{
|
|
#pragma warning restore VSTHRD100
|
|
DebugDragDrop("Dropping");
|
|
var playVersion = ++_playVersion;
|
|
|
|
IStorageItem? item = null;
|
|
try
|
|
{
|
|
item = (await e.DataView.GetStorageItemsAsync()).Single();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore GetStorageItemsAsync exceptions so they don't crash the process.
|
|
}
|
|
|
|
if (playVersion != _playVersion)
|
|
{
|
|
DebugDragDrop("Ignoring drop");
|
|
return;
|
|
}
|
|
|
|
if (item is null)
|
|
{
|
|
DebugDragDrop("Ignoring drop");
|
|
return;
|
|
}
|
|
|
|
// Reset the scrubber to the 0 position.
|
|
_scrubber.Value = 0;
|
|
|
|
DebugDragDrop("Doing drop");
|
|
if (await _stage.TryLoadFileAsync((StorageFile)item))
|
|
{
|
|
// Loading succeeded, start playing.
|
|
_playStopButton.IsChecked = true;
|
|
}
|
|
}
|
|
|
|
void LottieDragLeaveHandler(object sender, DragEventArgs e)
|
|
{
|
|
_stage.DoDragLeave();
|
|
}
|
|
|
|
[Conditional("DebugDragDrop")]
|
|
static void DebugDragDrop(string text) => Debug.WriteLine(text);
|
|
|
|
static bool IsJsonOrLottieFile(IStorageItem item) =>
|
|
item.IsOfType(StorageItemTypes.File) &&
|
|
(item.Name.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase) ||
|
|
item.Name.EndsWith(".lottie", StringComparison.InvariantCultureIgnoreCase));
|
|
|
|
bool _ignoreScrubberValueChanges;
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
void ProgressSliderChanged(object sender, ScrubberValueChangedEventArgs e)
|
|
{
|
|
if (!_ignoreScrubberValueChanges)
|
|
{
|
|
UncheckPlayStopButton();
|
|
_stage.Player.SetProgress(e.NewValue);
|
|
}
|
|
}
|
|
|
|
// Avoid "async void" method. Not valid here because we handle all async exceptions.
|
|
#pragma warning disable VSTHRD100
|
|
#pragma warning disable SA1300 // Element should begin with upper-case letter
|
|
async void _playControl_Toggled(object sender, RoutedEventArgs e)
|
|
#pragma warning restore SA1300 // Element should begin with upper-case letter
|
|
{
|
|
#pragma warning restore VSTHRD100
|
|
// If no Lottie is loaded, do nothing.
|
|
if (!_stage.Player.IsAnimatedVisualLoaded)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Otherwise, if we toggled on, we're stopped in manual mode: set the progress.
|
|
// If we toggled off, we're in auto mode, start playing.
|
|
if (!_playStopButton.IsChecked)
|
|
{
|
|
_stage.Player.SetProgress(_scrubber.Value);
|
|
}
|
|
else
|
|
{
|
|
_ignoreScrubberValueChanges = true;
|
|
_scrubber.Value = 0;
|
|
_ignoreScrubberValueChanges = false;
|
|
|
|
// If we were stopped in manual play control, turn it back to automatic.
|
|
if (!_playStopButton.IsChecked)
|
|
{
|
|
_playStopButton.IsChecked = true;
|
|
}
|
|
|
|
var playControlToggleVersion = ++_playControlToggleVersion;
|
|
|
|
try
|
|
{
|
|
await _stage.Player.PlayAsync(0, 1, looped: true);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore PlayAsync exceptions so they don't crash the process.
|
|
}
|
|
|
|
// Playing has finished. Make sure the PlayStopButton is no longer
|
|
// checked, unless a newer play has started.
|
|
if (playControlToggleVersion == _playControlToggleVersion)
|
|
{
|
|
_playStopButton.IsChecked = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UncheckPlayStopButton()
|
|
{
|
|
_playStopButton.IsChecked = false;
|
|
}
|
|
|
|
void CopyIssuesToClipboard(object sender, RoutedEventArgs e)
|
|
{
|
|
var issues = _stage.DiagnosticsViewModel.Issues;
|
|
var dataPackage = new DataPackage();
|
|
dataPackage.RequestedOperation = DataPackageOperation.Copy;
|
|
dataPackage.SetText(string.Join("\r\n", issues.Select(iss => iss.ToString())));
|
|
Clipboard.SetContent(dataPackage);
|
|
Clipboard.Flush();
|
|
}
|
|
|
|
public bool IsControlPanelVisible => _controlPanelButtons.Any(b => b.IsChecked == true);
|
|
|
|
// Uncheck all the other control panel buttons when one is checked.
|
|
// This allows toggle buttons to act like radio buttons.
|
|
void ControlPanelButtonChecked(object sender, RoutedEventArgs e)
|
|
{
|
|
// Uncheck all the other buttons.
|
|
foreach (var button in _controlPanelButtons)
|
|
{
|
|
if (button != sender)
|
|
{
|
|
if (button.IsChecked == true)
|
|
{
|
|
button.IsChecked = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove all the children from the control pane Grid, then add back the
|
|
// one that is is being shown. This is done to trigger the PaneThemeTransition
|
|
// so that the pane slides in and out.
|
|
ControlPanel.Children.Clear();
|
|
|
|
// Add back the panel corresponding to the button that is checked.
|
|
if (sender == InfoButton)
|
|
{
|
|
ControlPanel.Children.Add(InfoPanel);
|
|
}
|
|
else if (sender == PaletteButton)
|
|
{
|
|
ControlPanel.Children.Add(ColorPanel);
|
|
}
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsControlPanelVisible)));
|
|
}
|
|
|
|
// When one of the control panel buttons is unchecked, if all the buttons
|
|
// are now unpressed, remove the filler from the play/stop control bar so that
|
|
// the scrubber takes up the whole area.
|
|
void ControlPanelButtonUnchecked(object sender, RoutedEventArgs e)
|
|
{
|
|
// Remove all the children from the control pane Grid. This is done to
|
|
// trigger the PaneThemeTransition so that the pane slides in and out.
|
|
ControlPanel.Children.Clear();
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsControlPanelVisible)));
|
|
}
|
|
|
|
// Called when the user clicks on a marker hyperlink.
|
|
void MarkerClick(Hyperlink sender, HyperlinkClickEventArgs args)
|
|
{
|
|
var dataContext = ((FrameworkElement)sender.ElementStart.Parent).DataContext;
|
|
var marker = (Marker)dataContext;
|
|
SeekToProgressValue(marker.ConstrainedInProgress);
|
|
}
|
|
|
|
// Called when the user clicks on a marker-with-duration hyperlink.
|
|
void MarkerEndClick(Hyperlink sender, HyperlinkClickEventArgs args)
|
|
{
|
|
var dataContext = ((FrameworkElement)sender.ElementStart.Parent).DataContext;
|
|
var marker = (MarkerWithDuration)dataContext;
|
|
SeekToProgressValue(marker.ConstrainedOutProgress);
|
|
}
|
|
|
|
// Sets the progress to the given value, and sets the focus to the scrubber
|
|
// so that the arrow keys will control the position of the scrubber.
|
|
void SeekToProgressValue(double progress)
|
|
{
|
|
UncheckPlayStopButton();
|
|
|
|
// Set focus to the scrubber so that the arrow keys will move the
|
|
// position of the scrubber.
|
|
_scrubber.Focus(FocusState.Programmatic);
|
|
_scrubber.Value = progress;
|
|
}
|
|
|
|
void Page_PointerPressed(object sender, PointerRoutedEventArgs e)
|
|
{
|
|
// By default, when the pointer is pressed, focus on the scrubber so
|
|
// that the arrow keys can move the scrubber.
|
|
e.Handled = true;
|
|
_scrubber.Focus(FocusState.Pointer);
|
|
}
|
|
}
|
|
}
|