using System.Diagnostics; using Avalonia; using Avalonia.Controls; using Avalonia.Headless; using Avalonia.Media.Imaging; using Avalonia.Threading; using Avalonia.VisualTree; using MeWeb.App; using MeWeb.App.Services; using MeWeb.App.ViewModels; using MeWeb.App.Views; using MeWeb.Core.Checks; using MeWeb.Core.Knowledge; using MeWeb.Screenshots; using Microsoft.Extensions.DependencyInjection; var store = args.Length > 0 && args[0] == "--store"; var output = store ? args.Length > 1 ? args[1] : Path.Combine("fastlane", "metadata", "android") : args.Length > 0 ? args[0] : Path.Combine("dist", "screenshots"); var codes = !store && args.Length > 1 ? args[1].Split(',', StringSplitOptions.RemoveEmptyEntries) : Languages.All.Select(l => l.Code).ToArray(); Directory.CreateDirectory(output); var data = Directory.CreateTempSubdirectory("meweb-screenshots-"); AppServices.Folder = data.FullName; if (store) App.Platform = new StorePhone(); var session = HeadlessUnitTestSession.StartNew(typeof(ScreenshotEntry)); await session.Dispatch(async () => { if (store) { var invented = StoreRun.Finished(App.Services.GetRequiredService()); var early = StoreRun.Early(invented); foreach (var (code, locale) in new[] { ("en", "en-US"), ("it", "it-IT") }) { Localizer.Instance.SetLanguage(Languages.Find(code)); var folder = Path.Combine(locale, "images", "phoneScreenshots"); Phone(Path.Combine(folder, "2"), model => model.Replay(invented), view => view.GetVisualDescendants().OfType().First(t => t.Text == Localizer.Instance["UsesTitle"])); Phone(Path.Combine(folder, "3"), model => model.Replay(invented)); Phone(Path.Combine(folder, "1"), model => model.Status = Reachability.Online); Phone(Path.Combine(folder, "4"), model => { model.Replay(invented); model.Cards.First(c => c.Id == CheckId.Nat).IsExpanded = true; }, view => view.GetVisualDescendants().OfType().First(b => b.Classes.Contains("card") && b.DataContext is CardViewModel { Id: CheckId.Nat })); Phone(Path.Combine(folder, "5"), model => { model.Replay(early); model.Status = Reachability.Online; model.IsChecking = true; model.AskReach = false; model.OfferShare = false; model.Uses.Clear(); foreach (var id in Enum.GetValues()) model.Uses.Add(new UseViewModel(id, null)); }); Phone(Path.Combine(folder, "6"), model => { model.Replay(invented); model.OpenShareCommand.Execute(null); }, view => view.GetVisualDescendants().OfType().First(t => t.Text == Localizer.Instance["ShareExact"])); } return 0; } var run = await App.Services.GetRequiredService().Run((_, _) => { }, CancellationToken.None); Console.WriteLine($"run: {string.Join(", ", run.Findings.Select(f => $"{f.Id}={f.Outcome}"))}"); foreach (var (width, height, label) in new[] { (1100, 1500, "desktop"), (400, 8000, "phone") }) { foreach (var code in codes) { Localizer.Instance.SetLanguage(Languages.Find(code) ?? Languages.Fallback); Shoot(width, height, $"main-{label}-{code}", model => model.Replay(run)); if (label != "desktop") { Localizer.Instance.SetLanguage(Languages.Fallback); Shoot(width, 860, $"start-{code}", model => model.SelectedLanguage = Languages.Find(code) ?? Languages.Fallback); Localizer.Instance.SetLanguage(Languages.Find(code) ?? Languages.Fallback); Shoot(width, 3200, $"story-{code}", _ => { }); Shoot(width, 860, $"fold-{code}", _ => { }, view => view.ShowCheck()); Shoot(width, 1700, $"checking-{code}", model => model.CheckCommand.Execute(null)); Shoot(width, 860, $"bye-{code}", model => model.Leaving = true); continue; } Shoot(width, 900, $"contacts-{code}", model => model.ShowContacts = true); Shoot(width, 900, $"share-{code}", model => { model.Replay(run); model.OpenShareCommand.Execute(null); }); } } return 0; }, CancellationToken.None); data.Delete(true); Environment.Exit(0); void Phone(string name, Action prepare, Func top = null) => Shoot(405, 720, name, prepare, top is null ? null : view => { var target = top(view); var scroller = target.FindAncestorOfType(); var at = target.TranslatePoint(default, (Visual)scroller.Content); scroller.Offset = new Vector(0, Math.Max(0, (at?.Y ?? 0) - 8)); }, 1080 / 405.0); void Shoot(int width, int height, string name, Action prepare, Action arrange = null, double scale = 1) { var fresh = AppSettings.Read(); fresh.StorySeen = false; AppSettings.Write(fresh); var model = App.Services.GetRequiredService(); var view = new MainView { DataContext = model }; var window = new Window { Width = width, Height = height, Content = view }; window.Show(); window.SetRenderScaling(scale); model.AvailableWidth = width; if (store) { var patience = Stopwatch.StartNew(); do { Dispatcher.UIThread.RunJobs(); Thread.Sleep(20); } while (model.Status == Reachability.Unknown && patience.Elapsed < TimeSpan.FromSeconds(20)); model.LatestVersion = null; } prepare(model); for (var i = 0; i < 6; i++) Dispatcher.UIThread.RunJobs(); if (arrange is not null) { arrange(view); Dispatcher.UIThread.RunJobs(); } foreach (var art in view.GetVisualDescendants().OfType()) art.Hold(); Dispatcher.UIThread.RunJobs(); var frame = window.CaptureRenderedFrame(); var path = Path.Combine(output, $"{name}.png"); Directory.CreateDirectory(Path.GetDirectoryName(path)); if (frame is not null) { using var file = File.Create(path); frame.Save(file, new PngBitmapEncoderOptions()); } Console.WriteLine($"{path} {(frame is null ? "no frame" : "ok")}"); window.Close(); } public static class ScreenshotEntry { public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UseSkia() .UseHarfBuzz() .WithInterFont() .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false }); }