Uno Platform 応用 & Tips

本章では、Uno Platform の応用的な機能や Tips について説明します。

## 共有プロジェクトのルート名前空間の変更

Uno Platform のプロジェクトを新規作成すると Droid, iOS, Shared, UWP, Wasm の 5 つのプロジェクトが出来ます。
この中の Shared プロジェクトに多くのコードを書いていくのですが、Shared プロジェクトにクラスを作ると「プロジェクト名.Shared」がデフォルトの名前空間になります。
この Shared を消すには、ソリューションエクスプローラーで Shared のプロジェクトを選択して右クリックメニューから「プロパティ」を選択します。
プロパティウィンドウが表示されるので、その中のルート名前空間から .Shared を消します。

![root-namespace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/f1c4d4e7-002f-0a1b-5b4d-19ef3388e791.png)

これでクラスの新規作成時に名前空間に Shared が含まれなくなります。

## プラットフォーム固有処理の実装

Uno Platform は、UWP の API を各プラットフォームで動くように実装されていますが、基本的には UI 部分の機能が主になります。
そのため、どうしても UWP の API だけでは出来ないことも出てきます。

そういった時のために Uno Platform にはプラットフォーム固有の処理を実装する方法があります。
Android 固有の処理は Xamarin.Android で、iOS 固有の処理は Xamarin.iOS で、WebAssembly 固有の処理は、Wasm のプロジェクトに実装します。Shared プロジェクトに #if ディレクティブなどで同じファイルに記述することも出来ます。

また、XAML でも UWP, Android, iOS, WASM で表示を分けるように書くことが出来ます。

### C# でのプラットフォーム固有機能の実装と JavaScript 相互運用

Uno Platform は、プラットフォーム毎にソースコードがビルドされます。
例えば、Android 用にビルドしているときは、Xamarin.Android の機能にアクセスできます。

そして、各プロジェクトにはコンパイル定数が定義されています。そのため、`#if` ディレクティブを使うことでプラットフォーム固有の処理を記述できます。
Uno Platform のプロジェクトには以下の定数が定義されています。

- `NETFX_CORE`: UWP 
- `__ANDROID__`: Android
- `__IOS__`: iOS
- `__WASM__`: WebAssembly

そのため各プラットフォーム固有の処理を実装する場合には以下のように書けます。

```csharp:#ifディレクティブでのプラットフォーム固有機能の書きかた
#if NETFX_CORE
  // Windows 固有処理
#elif __ANDROID__
  // Android 固有処理
#elif __IOS__
  // iOS 固有処理
#elif __WASM__
  // WebAssembly 固有処理
#endif
```

また、この中で以下のように型の別名を定義することで、その後のコードで共通の型名を定義できます。

```csharp:型の別名の定義
#if __ANDROID__
  using NativeView = Android.Views.View;
#elif __IOS__
  using NativeView = UIKit.UIView;
#else
  using NativeView = Windows.UI.Xaml.UIElement;
#endif

class ViewHolder
{
  // 共通の型名で書けるようになる
  public NativeView View { get; set; } 
}
```

また、同じシグネチャーのコードが Droid, iOS, Wasm, UWP の各プロジェクトにあれば Shared で使うことが出来ます。
この特性とパーシャル クラスを組み合わせると以下のように共通部分は Shared プロジェクトで定義して、プラットフォーム固有の処理は各プラットフォームのプロジェクトで実装できます。

パーシャル クラスの使用例を以下に示します。

```csharp:partialクラスでのプラットフォーム固有機能の実装
// Shared プロジェクトに定義
using System.Diagnostics;

namespace PlatformSpecificSample
{
    public partial class ToastNotifier
    {
        partial void ShowImpl(string text);
        public void Show(string text)
        {
            Debug.WriteLine($"Show called: {text}");
            ShowImpl(text);
        }
    }
}

// Droid プロジェクトに定義
using Android.App;
using Android.Widget;

namespace PlatformSpecificSample
{
    public partial class ToastNotifier
    {
        partial void ShowImpl(string text) => 
            Toast.MakeText(Application.Context, text, ToastLength.Long).Show();
    }
}

// iOS プロジェクトに定義
using UIKit;

namespace PlatformSpecificSample
{
    public partial class ToastNotifier
    {
        partial void ShowImpl(string text)
        {
            var alert = UIAlertController.Create("Toast", text, UIAlertControllerStyle.Alert);
            alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, _ => { }));
            UIApplication.SharedApplication.KeyWindow.RootViewController.PresentViewController(alert, true, null);
        }
    }
}

// UWP プロジェクトに定義
using System.Linq;
using Windows.UI.Notifications;

namespace PlatformSpecificSample
{
    public partial class ToastNotifier
    {
        partial void ShowImpl(string text)
        {
            var n = ToastNotificationManager.CreateToastNotifier();
            var content = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastText01);
            var textNode = content.GetElementsByTagName("text").First();
            textNode.InnerText = text;
            n.Show(new ToastNotification(content));
        }
    }
}
```

WebAssembly は、JavaScript との相互運用機能を使って JavaScript の関数を実行してトーストを表示したいと思います。
使用する JavaScript のライブラリーは依存関係のないシンプルな iziToast にします。

https://izitoast.marcelodolza.com/

ダウンロードして iziToast.min.js を iziToast.js にリネームして Wasm プロジェクトの WasmScripts フォルダーに、iziToast.min.css を WasmCSS フォルダーに埋め込みリソースとして追加します。

![izi-toast.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/157e1e76-a86e-9baf-7686-86ae3a4d7558.png)


JavaScript の関数を呼び出すには `Uno.Foundation.WebAssemblyRuntime` クラスの `InvokeJS` メソッドに JavaScript の文字列渡して呼び出します。例えばアラートを出したい場合は `WebAssemblyRuntime.InvokeJS("alert('Hello world');");` のようになります。JavaScript のライブラリーを使うときに注意する点は、require.js に対応したモジュールの場合は、require 関数を使って読み込んで使う点です。例えば今回利用する iziToast も、reqire.js に対応したライブラリーの 1 つです。

C# に require 関数の呼び出しを含んだ JavaScript を文字列として書くと複雑になってしまうため、WasmScripts フォルダーに toastnotifier.js というファイルを埋め込みリソースで作成して、以下のような関数を定義して、それを C# から呼ぶ形で実装します。

```javascript:WasmScripts/toastnotifier.js
function showToast(text) {
    require(['iziToast'], function (iziToast) {
        iziToast.show({ title: 'Info', message: text });
    });
}
```

そして、WASM のプロジェクトに　ToastNotifier.cs というファイルを作成して InvokeJS メソッドで上記関数を呼び出します。

```csharp:ToastNotifier.cs
using Uno.Foundation;

namespace PlatformSpecificSample
{
    public partial class ToastNotifier
    {
        partial void ShowImpl(string text) => 
            WebAssemblyRuntime.InvokeJS($"showToast('{text}');");
    }
}
```

これで UWP、Android、iOS、WebAssembly 全てのプラットフォームでの実装が終わりました。あとは、MainPage.xaml にボタンを置いてクリックイベントで以下のような呼び出し処理を書きます。

```csharp
new ToastNotifier().Show("Hello platform specific function!");
```

各プラットフォームで実行すると以下のようにプラットフォーム固有の表示方法で表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/9d4e310f-3c03-a129-e30a-5258e536988e.png" width="400" />

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/663eccb6-83f0-29e0-fe3d-b40ea9051cd9.png" width="400" />

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/be518d7e-efa1-9337-7fc9-168b42f4c4e7.png" width="400" />

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/708cced5-ee77-ea8b-6192-bb081a72d53c.png" width="400" />

### XAML でのプラットフォーム固有機能の実装

C# で書くロジックだけではなく、XAML でもプラットフォーム固有機能を利用できます。XAML 上でプラットフォーム固有機能を使うには、以下のような XML 名前空間を定義します。

|対応プラットフォーム|除外されるプラットフォーム|名前空間|URL|m:Ignorable への追加
|------|------|------|------|------|------|
|Windows}Android, iOS, WebAssembly|win|http://schemas.microsoft.com/winfx/2006/xaml/presentation|不要|
|Android, iOS, WebAssembly|Windows|not_win|http://uno.ui/not_win|必要|
|Android|Windows, iOS, WebAssembly|android|http://uno.ui/android|必要|
|iOS|Windows, Android, WebAssembly|ios|http://uno.ui/ios|必要|
|Android, iOS, WebAssembly|Windows|xamarin|http://uno.ui/xamarin|必要|
|WebAssembly|Windows, Android, iOS|wasm|http://uno.ui/wasm|必要|
|Windows, iOS, WebAssembly|Android|not_android|http://schemas.microsoft.com/winfx/2006/xaml/presentation|不要|
|Windows, Android, WebAssembly|iOS|not_ios|http://schemas.microsoft.com/winfx/2006/xaml/presentation|不要|

この XML 名前空間のプレフィックスをつけると、その対象プラットフォームでだけ評価されます。例えば Android と、それ以外のプラットフォームで表示テキストを変える場合は以下のようになります。m:Ignorable に追加しないといけない名前空間は追加するのを忘れないように注意してください。

```xml
<Page
    x:Class="NativeStyleApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:NativeStyleApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:android="http://uno.ui/android"
    xmlns:not_android="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d android">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" VerticalAlignment="Center"
                HorizontalAlignment="Center">
        <TextBlock not_android:Text="Not Android"
                   android:Text="Android"
                   FontSize="32" />
    </StackPanel>
</Page>
```

WebAssembly(左) と Android(右) で実行しました。

![platform-specific-xaml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/e7d62373-ed00-f6dd-857b-623bdb8bc617.png)

この例では属性に指定しましたが、タグにも適用可能です。以下のようにタグを書くと、iOS のみで表示されるコントロールが定義できます。

```xml
<ios:Button Content="iOS Button" />
```

## アプリケーションのタイトルバー

CommandBar を使うと、iOS や Android でタイトルバーを出すことが出来ます。ここまでは小さなプログラムで動作を見てみましたが、ここでは、もう少し本格的なサンプルで見ていきます。
UADO という Uno Platform のサンプルを見ていきます。

https://github.com/unoplatform/uado

このアプリケーションを Android などのモバイル OS で実行すると以下のようにネイティブの見た目のタイトルバーがある画面が表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/8f240ca9-45af-5dce-a337-da353171c228.png" width="400" />

Android と iOS では、CommandBar コントロールを置くとタイトルバーが表示されます。UWP や WebAssembly でも CommandBar は表示されますが Android や iOS の持っている戻るボタンなどへの対応はありません。
UADO のサンプルでは、UWP と WebAssembly でも CommandBar の動きを Android と iOS に合わせるために、そこそこの量のコードが追加されています。
自分で対応する場合でも、同じようなコードを書くことになるので UADO の src/Uno.AzureDevOps/Uno.AzureDevOps.Views/Styles/Controls/CommandBar.xaml によるスタイルのカスタマイズと src/Uno.AzureDevOps/Uno.AzureDevOps.Views/Controls/PageHeader.xaml と src/Uno.AzureDevOps/Uno.AzureDevOps.Views/Controls/PageHeader.xaml.cs のソースコードは参考になるので興味のある方は是非確認してみてください。

## 複数件のデータの表示方法（ListView）

ほぼ全てのアプリケーションで複数レコードのデータをリスト形式で表示します。
Uno Platform では、ListView を使って実現します。

ListView は、ItemsSource プロパティに設定された配列やリストを ItemTemplate に設定されたテンプレートで表示します。例えば以下のような Person クラスのリストを ListView に表示してみましょう。

Person クラスの定義は以下のようになります。

```csharp:Person.cs
using Windows.UI;

namespace NavigationViewApp
{
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public Color FaboriteColor { get; set; }
    }
}
```

そして、XAML で ItemTemplate を設定した ListView を定義します。

```xml:MainPage.xaml
<Page
    x:Class="NavigationViewApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:NavigationViewApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Page.Resources>
        <local:FromColorToSolidColorBrushConverter x:Key="FromColorToSolidColorBrushConverter" />
    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <ListView x:Name="listView">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <RelativePanel>
                        <Rectangle x:Name="faboriteColor" 
                                   Fill="{Binding FaboriteColor, Converter={StaticResource FromColorToSolidColorBrushConverter}}"
                                   Width="50"
                                   Height="50"
                                   Margin="10" 
                                   VerticalAlignment="Center"
                                   RelativePanel.AlignTopWithPanel="True"
                                   RelativePanel.AlignBottomWithPanel="True" />
                        <TextBlock x:Name="textBlockName" 
                                   Text="{Binding Name}"
                                   Style="{StaticResource CaptionTextBlockStyle}" 
                                   RelativePanel.RightOf="faboriteColor" 
                                   Margin="0,10,10,10" />
                        <TextBlock x:Name="textBlockAge" 
                                   Text="{Binding Age}"
                                   Style="{StaticResource BodyTextBlockStyle}"
                                   RelativePanel.Below="textBlockName" 
                                   RelativePanel.AlignLeftWith="textBlockName"
                                   Margin="0, 0, 10, 10" />
                    </RelativePanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>
```

MainPage.xaml.cs では、コンストラクターで ListView の ItemsSource にデータを設定します。

```csharp:MainPage.xaml.cs
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.UI;
using Windows.UI.Xaml.Controls;

namespace NavigationViewApp
{
    public sealed partial class MainPage : Page
    {
        private static Random Random { get; } = new Random();
        public MainPage()
        {
            this.InitializeComponent();
            listView.ItemsSource = Enumerable.Range(1, 100)
                .Select(x => new Person
                {
                    Name = $"Tanaka {x}",
                    Age = x,
                    FaboriteColor = Color
                        .FromArgb(
                            255, 
                            (byte)Random.Next(0, 255), 
                            (byte)Random.Next(0, 255), 
                            (byte)Random.Next(0, 255)),
                })
                .ToArray();
        }
    }
}
```

MainPage.xaml で使用している FromColorToSolidColorBrushConverter は以下のような Color から SolidColorBrush に変換するコンバーターです。

```csharp:FromColorToSolidColorBrushConverter.cs
using System;
using Windows.UI;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;

namespace NavigationViewApp
{
    public class FromColorToSolidColorBrushConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            return new SolidColorBrush((Color)value);
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotSupportedException();
        }
    }
}
```

実行すると、リスト状にデータが表示されます。以下に WebAssembly(左) と Android(右) に表示した例を示します。

![listview-basic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/eb215006-1ff2-86ee-73f2-90fc6fbad9bf.png)


ListView の選択項目は SelectedItem プロパティで取得できます。アイテムの変更が変わった時に処理がしたいときは SelectionChanged イベントを処理します。
SelectionChanged イベントを処理するには ListView タグに以下のようにイベントハンドラーを追加します。

```xml
<ListView x:Name="listView"
          SelectionChanged="ListView_SelectionChanged">
```

イベントハンドラーの中では、イベント引数の AddedItems に選択項目が入っています。選択された項目の名前をメッセージダイアログで出す場合は以下のようになります。

```csharp
private async void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var selectedPerson = (Person)e.AddedItems.FirstOrDefault();
    if (selectedPerson == null)
    {
        return;
    }

    var dialog = new MessageDialog($"{selectedPerson.Name} was selected.");
    await dialog.ShowAsync();
}
```

このコードを実行してリストを選択すると以下のようになります。

![listview-selection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/807e5afd-07bc-74d8-954f-55780793a0b3.png)

##  JSON.NET を使うときの注意点

JSON.NET のようなリフレクションを使うようなプログラムの場合、コード内で明示していない型は最終的な出力ファイルのサイズを小さくするためにビルド時に削除されることがあります。Uno Platform の WebAssembly のプロジェクトも、そのような動きをします。
そのため、WASM プロジェクトから別のライブラリを参照していて明示的にコード内から使っていないメソッドやコンストラクターをリフレクションで呼び出すと実行時エラーになります。

例えば ClassLibrary1 というクラス ライブラリー プロジェクトを作成して、以下のようにクラスを 2 つ定義します。

```csharp
using System;

namespace ClassLibrary1
{
    public class Class1 {}
    public class Class2 {}
}
```

そして、MainPage クラスなどでリフレクションを使って Class1 をインスタンス化するコードを書きます。

```csharp
var assem = typeof(Class2).Assembly;
System.Diagnostics.Debug.WriteLine(assem);
var type = assem.GetType("ClassLibrary1.Class1");
System.Diagnostics.Debug.WriteLine(type);
var ctor = type.GetConstructor(Array.Empty<Type>());
System.Diagnostics.Debug.WriteLine(ctor);
var instance = ctor.Invoke(null);
System.Diagnostics.Debug.WriteLine(instance);
```

この状態で WASM プロジェクトを実行するとブラウザーの開発者ツールのコンソールに NullReferenceExcpeion が出たというメッセージが表示されます。
これは、Class1 がコード内には出てこないため、削除されてしまい型情報を取得するところで null が返ってきて後続の処理で NullReferenceException が発生します。
これを解消するには、WASM プロジェクトにある LinkerConfig.xml に以下のように追記します。

```xml:LinkerConfig.xml
<linker>
  <assembly fullname="App1.Wasm" />
  <assembly fullname="Uno.UI" />

  <assembly fullname="System.Core">
    <!-- This is required by JSon.NET and any expression.Compile caller -->
    <type fullname="System.Linq.Expressions*" />
  </assembly>
  <!-- 以下のタグを追加 -->
  <assembly fullname="ClassLibrary1">
    <type fullname="ClassLibrary1.Class1*" />
  </assembly>
</linker>
```

type タグを使用せずに `<assembly fullname="ClassLibrary1" />` のように書いてアセンブリー内の全ての型を削除しないように設定することが出来ます。

Web API などから取ってきた JSON をシリアライズ・デシリアライズする場所などでエラーが出たときは、型情報などが消されてるためにエラーが起きている可能性があるので LinkerConfig.xml にアセンブリ―の定義を追加して確認してみてください。

## サンプルプログラム UADO

Uno Platform がベースとしている UWP は、見た目とロジックを分離して作る方法として MVVM パターンがよく採用されています。
XAML とコードビハインドが、View レイヤーで、その裏に View 用のモデルとして ViewModel がいて、アプリケーションのロジックの Model がいるという 3 つのレイヤーにわけてアプリケーションを作成する方法です。

ViewModel クラスは、データバインディング向けに前章で紹介した INotifyPropertyChanged インターフェースを実装します。
Model と ViewModel 間はメソッド呼び出しやプロパティ変更通知などのイベントを使って互いにやり取りを行います。

実際に MVVM パターンで作られた Uno Platform のアプリケーションとして本章でも取り上げた UADO があります。

https://github.com/unoplatform/uado

このリポジトリーの src/Uno.AzureDevOps/Uno.AzureDevOps.Views に View レイヤーのコードがあります。そして、src/Uno.AzureDevOps/Uno.AzureDevOps.Shared には ViewModel と Model のコードがあります。
このプロジェクトは DI コンテナーも使っていて、コンテナーの初期化処理は、先ほど紹介したフォルダーの中に、それぞれある Module.cs ファイル内で行われています。

このサンプルでは、ViewModel 内で SimpleIoc というコンテナーの static なインスタンスから依存関係を取得しています。
このようにして、ViewModel から Model を取得して実際の処理をしています。

本格的なアプリケーションを Uno Platform で実装するときには UADO は参考になるコードが沢山あるので確認してみてください。

## 各種ストア提出用のパッケージの作成

アプリが完成したらストア向けパッケージを作成します。各種パッケージの作成方法はここでは詳細には解説しませんが、UWP は UWP の方法で、Android は Xamarin.Android の方法で、iOS は Xamarin.iOS の方法でパッケージを作成できます。

以下にドキュメントへのリンクの URL を書いておくのでストアに出す前に確認してください。

- UWP: [Visual Studio でデスクトップ アプリや UWP アプリをパッケージ化する](https://docs.microsoft.com/ja-jp/windows/msix/package/packaging-uwp-apps)
- Android: [リリースに向けてアプリケーションを準備する](https://docs.microsoft.com/ja-jp/xamarin/android/deploy-test/release-prep/?tabs=windows&WT.mc_id=docs-book-machiy)
- iOS: [App Store への Xamarin.iOS アプリの公開](https://docs.microsoft.com/ja-jp/xamarin/ios/deploy-test/app-distribution/app-store-distribution/publishing-to-the-app-store?tabs=windows&WT.mc_id=docs-book-machiy)

## Android やブラウザーの戻るボタン対応

前章で、画面遷移について説明しました。Frame クラスの Navigate メソッドを使うことで簡単に画面遷移が出来ることを説明しましたが、この状態では Android やブラウザーの戻るボタンには対応していません。
戻るボタンに対応するには `Windows.UI.Core.SystemNavigationManager` クラスの `BackRequested` イベントを使います。このイベントはシステムの戻るボタンをハンドリングするための UWP の API で Android の戻るボタンやブラウザーの戻るボタンにも対応しています。

戻るボタンの動きをページ単位で細かく制御したい場合は各ページで上記イベントをハンドリングすることも出来ますが、戻るボタンが押されたら素直に戻る動作でよい場合は App.xaml.cs の OnLanuched メソッドの `if(rootFrame == null)` の if 文の中に以下のコードを追加することで対応できます。

```csharp
Windows.UI.Core.SystemNavigationManager.GetForCurrentView().BackRequested += (s, args) =>
{
    // 戻れない場合は何もしない
    if (!rootFrame.CanGoBack)
    {
        return;
    }

    // 戻れる場合はフレームの GoBack メソッドを呼んで
    rootFrame.GoBack();
    // 戻るボタンを処理したことを通知する
    args.Handled = true;
};
```

## まとめ

まとめ

ここでは、Uno Platform の Tips や応用的なトピックを取り上げました。
ここで紹介した UADO プロジェクトは、参考になるコードがあるため是非 GitHub からクローンして実際に動かして確認してみてください。

また、Uno Platform の開発はかなり活発に行われているので、以下の Uno Platform の Twitter アカウントをフォローしてウォッチしてみてください。

https://twitter.com/UnoPlatform

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/97475967-b4bb-b2b1-e7ab-f04282b024d9.png)