diff --git a/cevio-casts.sln b/cevio-casts.sln
index 16afb65..91c82bd 100644
--- a/cevio-casts.sln
+++ b/cevio-casts.sln
@@ -17,6 +17,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpdateCheck", "tests\Update
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CevioCasts.UpdateChecker", "languages\csharp\CevioCasts.UpdateChecker\CevioCasts.UpdateChecker.csproj", "{4B44E997-89E9-4E0C-8FD3-A059926CE007}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "viewer", "viewer", "{63AE9FF6-FFC9-4D2A-9C36-D0BDB80BF3E9}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CastViewer", "CastViewer", "{DABA4146-2600-4C2C-974F-934F9D89B643}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CastViewer.Core", "viewer\CastViewer\CastViewer.Core\CastViewer.Core.csproj", "{6401CAD1-98BB-44AD-9A25-ABB76D53BFB7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CastViewer", "viewer\CastViewer\CastViewer\CastViewer.csproj", "{4ACE5C06-354C-45F9-A57F-B74D6AC515D1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CastViewer.Browser", "viewer\CastViewer\CastViewer.Browser\CastViewer.Browser.csproj", "{DDD2BDDD-7DC8-48D8-9D53-13CD9DE5496B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CastViewer.Desktop", "viewer\CastViewer\CastViewer.Desktop\CastViewer.Desktop.csproj", "{BB3045B4-358F-4814-97D7-F431E26F196C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -42,11 +54,32 @@ Global
{4B44E997-89E9-4E0C-8FD3-A059926CE007}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4B44E997-89E9-4E0C-8FD3-A059926CE007}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4B44E997-89E9-4E0C-8FD3-A059926CE007}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6401CAD1-98BB-44AD-9A25-ABB76D53BFB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6401CAD1-98BB-44AD-9A25-ABB76D53BFB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6401CAD1-98BB-44AD-9A25-ABB76D53BFB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6401CAD1-98BB-44AD-9A25-ABB76D53BFB7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4ACE5C06-354C-45F9-A57F-B74D6AC515D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4ACE5C06-354C-45F9-A57F-B74D6AC515D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4ACE5C06-354C-45F9-A57F-B74D6AC515D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4ACE5C06-354C-45F9-A57F-B74D6AC515D1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DDD2BDDD-7DC8-48D8-9D53-13CD9DE5496B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DDD2BDDD-7DC8-48D8-9D53-13CD9DE5496B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DDD2BDDD-7DC8-48D8-9D53-13CD9DE5496B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DDD2BDDD-7DC8-48D8-9D53-13CD9DE5496B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BB3045B4-358F-4814-97D7-F431E26F196C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BB3045B4-358F-4814-97D7-F431E26F196C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BB3045B4-358F-4814-97D7-F431E26F196C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BB3045B4-358F-4814-97D7-F431E26F196C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0DADEAF5-284F-403D-9614-76B50AF98559} = {79FD13AA-8212-4495-A58D-CBF517EB00C7}
{52BF462D-66A4-4B5B-9945-B7EDC41524CD} = {0DADEAF5-284F-403D-9614-76B50AF98559}
{F7E7758D-E28B-459D-ACBE-DE78AF594AB2} = {FF0FEE4A-4334-4D98-8873-CC62A1A37C7D}
{4B44E997-89E9-4E0C-8FD3-A059926CE007} = {0DADEAF5-284F-403D-9614-76B50AF98559}
+ {DABA4146-2600-4C2C-974F-934F9D89B643} = {63AE9FF6-FFC9-4D2A-9C36-D0BDB80BF3E9}
+ {6401CAD1-98BB-44AD-9A25-ABB76D53BFB7} = {DABA4146-2600-4C2C-974F-934F9D89B643}
+ {4ACE5C06-354C-45F9-A57F-B74D6AC515D1} = {DABA4146-2600-4C2C-974F-934F9D89B643}
+ {DDD2BDDD-7DC8-48D8-9D53-13CD9DE5496B} = {DABA4146-2600-4C2C-974F-934F9D89B643}
+ {BB3045B4-358F-4814-97D7-F431E26F196C} = {DABA4146-2600-4C2C-974F-934F9D89B643}
EndGlobalSection
EndGlobal
diff --git a/viewer/CastViewer/CastViewer.Android/CastViewer.Android.csproj b/viewer/CastViewer/CastViewer.Android/CastViewer.Android.csproj
new file mode 100644
index 0000000..42608b8
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/CastViewer.Android.csproj
@@ -0,0 +1,28 @@
+
+
+ Exe
+ net8.0-android
+ 21
+ enable
+ com.CompanyName.CastViewer
+ 1
+ 1.0
+ apk
+ false
+
+
+
+
+ Resources\drawable\Icon.png
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.Android/Icon.png b/viewer/CastViewer/CastViewer.Android/Icon.png
new file mode 100644
index 0000000..41a2a61
Binary files /dev/null and b/viewer/CastViewer/CastViewer.Android/Icon.png differ
diff --git a/viewer/CastViewer/CastViewer.Android/MainActivity.cs b/viewer/CastViewer/CastViewer.Android/MainActivity.cs
new file mode 100644
index 0000000..b8a2acb
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/MainActivity.cs
@@ -0,0 +1,23 @@
+using Android.App;
+using Android.Content.PM;
+using Avalonia;
+using Avalonia.Android;
+using Avalonia.ReactiveUI;
+
+namespace CastViewer.Android;
+
+[Activity(
+ Label = "CastViewer.Android",
+ Theme = "@style/MyTheme.NoActionBar",
+ Icon = "@drawable/icon",
+ MainLauncher = true,
+ ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
+public class MainActivity : AvaloniaMainActivity
+{
+ protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
+ {
+ return base.CustomizeAppBuilder(builder)
+ .WithInterFont()
+ .UseReactiveUI();
+ }
+}
diff --git a/viewer/CastViewer/CastViewer.Android/Properties/AndroidManifest.xml b/viewer/CastViewer/CastViewer.Android/Properties/AndroidManifest.xml
new file mode 100644
index 0000000..9f44e64
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/Properties/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.Android/Resources/AboutResources.txt b/viewer/CastViewer/CastViewer.Android/Resources/AboutResources.txt
new file mode 100644
index 0000000..4cedede
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/Resources/AboutResources.txt
@@ -0,0 +1,44 @@
+Images, layout descriptions, binary blobs and string dictionaries can be included
+in your application as resource files. Various Android APIs are designed to
+operate on the resource IDs instead of dealing with images, strings or binary blobs
+directly.
+
+For example, a sample Android app that contains a user interface layout (main.axml),
+an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
+would keep its resources in the "Resources" directory of the application:
+
+Resources/
+ drawable/
+ icon.png
+
+ layout/
+ main.axml
+
+ values/
+ strings.xml
+
+In order to get the build system to recognize Android resources, set the build action to
+"AndroidResource". The native Android APIs do not operate directly with filenames, but
+instead operate on resource IDs. When you compile an Android application that uses resources,
+the build system will package the resources for distribution and generate a class called "R"
+(this is an Android convention) that contains the tokens for each one of the resources
+included. For example, for the above Resources layout, this is what the R class would expose:
+
+public class R {
+ public class drawable {
+ public const int icon = 0x123;
+ }
+
+ public class layout {
+ public const int main = 0x456;
+ }
+
+ public class strings {
+ public const int first_string = 0xabc;
+ public const int second_string = 0xbcd;
+ }
+}
+
+You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main
+to reference the layout/main.axml file, or R.strings.first_string to reference the first
+string in the dictionary file values/strings.xml.
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer.Android/Resources/drawable-night-v31/avalonia_anim.xml b/viewer/CastViewer/CastViewer.Android/Resources/drawable-night-v31/avalonia_anim.xml
new file mode 100644
index 0000000..1fef3ac
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/Resources/drawable-night-v31/avalonia_anim.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.Android/Resources/drawable-v31/avalonia_anim.xml b/viewer/CastViewer/CastViewer.Android/Resources/drawable-v31/avalonia_anim.xml
new file mode 100644
index 0000000..4784f80
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/Resources/drawable-v31/avalonia_anim.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.Android/Resources/drawable/splash_screen.xml b/viewer/CastViewer/CastViewer.Android/Resources/drawable/splash_screen.xml
new file mode 100644
index 0000000..4cebfe2
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/Resources/drawable/splash_screen.xml
@@ -0,0 +1,13 @@
+
+
+
+ -
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.Android/Resources/values-night/colors.xml b/viewer/CastViewer/CastViewer.Android/Resources/values-night/colors.xml
new file mode 100644
index 0000000..0f6f6c8
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/Resources/values-night/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #212121
+
diff --git a/viewer/CastViewer/CastViewer.Android/Resources/values-v31/styles.xml b/viewer/CastViewer/CastViewer.Android/Resources/values-v31/styles.xml
new file mode 100644
index 0000000..8075ffa
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/Resources/values-v31/styles.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.Android/Resources/values/colors.xml b/viewer/CastViewer/CastViewer.Android/Resources/values/colors.xml
new file mode 100644
index 0000000..6fbae25
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/Resources/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
diff --git a/viewer/CastViewer/CastViewer.Android/Resources/values/styles.xml b/viewer/CastViewer/CastViewer.Android/Resources/values/styles.xml
new file mode 100644
index 0000000..77ed2d7
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Android/Resources/values/styles.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.Browser/CastViewer.Browser.csproj b/viewer/CastViewer/CastViewer.Browser/CastViewer.Browser.csproj
new file mode 100644
index 0000000..5affca2
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Browser/CastViewer.Browser.csproj
@@ -0,0 +1,31 @@
+
+
+ Exe
+ net8.0-browser
+ browser-wasm
+ wwwroot\main.js
+ ./_framework
+ true
+ true
+
+ false
+ full
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.Browser/Program.cs b/viewer/CastViewer/CastViewer.Browser/Program.cs
new file mode 100644
index 0000000..777bacb
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Browser/Program.cs
@@ -0,0 +1,28 @@
+using System.Runtime.Versioning;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Browser;
+using Avalonia.Media;
+using Avalonia.ReactiveUI;
+using CastViewer;
+
+[assembly: SupportedOSPlatform("browser")]
+
+internal sealed partial class Program
+{
+ private static Task Main(string[] args) => BuildAvaloniaApp()
+ //.WithInterFont()
+ .UseReactiveUI()
+ .With(new FontManagerOptions
+ {
+ FontFallbacks = [
+ new(){
+ FontFamily = new("avares://CastViewer/Assets/NotoSansCJKjp-Regular.otf#Noto Sans CJK JP")
+ }
+ ],
+ })
+ .StartBrowserAppAsync("out");
+
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure();
+}
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer.Browser/Properties/launchSettings.json b/viewer/CastViewer/CastViewer.Browser/Properties/launchSettings.json
new file mode 100644
index 0000000..fe67f29
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Browser/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "CastViewer.Browser": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/debug?browser={browserInspectUri}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer.Browser/runtimeconfig.template.json b/viewer/CastViewer/CastViewer.Browser/runtimeconfig.template.json
new file mode 100644
index 0000000..cc3f2ae
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Browser/runtimeconfig.template.json
@@ -0,0 +1,11 @@
+{
+ "wasmHostProperties": {
+ "perHostConfig": [
+ {
+ "name": "browser",
+ "html-path": "index.html",
+ "Host": "browser"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer.Browser/wwwroot/Logo.svg b/viewer/CastViewer/CastViewer.Browser/wwwroot/Logo.svg
new file mode 100644
index 0000000..7d28229
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Browser/wwwroot/Logo.svg
@@ -0,0 +1,5 @@
+
diff --git a/viewer/CastViewer/CastViewer.Browser/wwwroot/app.css b/viewer/CastViewer/CastViewer.Browser/wwwroot/app.css
new file mode 100644
index 0000000..c34bee5
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Browser/wwwroot/app.css
@@ -0,0 +1,74 @@
+:root {
+ --sat: env(safe-area-inset-top);
+ --sar: env(safe-area-inset-right);
+ --sab: env(safe-area-inset-bottom);
+ --sal: env(safe-area-inset-left);
+}
+
+/* HTML styles for the splash screen */
+
+.highlight {
+ color: white;
+ font-size: 2.5rem;
+ display: block;
+}
+
+.purple {
+ color: #8b44ac;
+}
+
+.icon {
+ opacity: 0.05;
+ height: 35%;
+ width: 35%;
+ position: absolute;
+ background-repeat: no-repeat;
+ right: 0px;
+ bottom: 0px;
+ margin-right: 3%;
+ margin-bottom: 5%;
+ z-index: 5000;
+ background-position: right bottom;
+ pointer-events: none;
+}
+
+#avalonia-splash a {
+ color: whitesmoke;
+ text-decoration: none;
+}
+
+.center {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+}
+
+#avalonia-splash {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ color: whitesmoke;
+ background: #1b2a4e;
+ font-family: 'Nunito', sans-serif;
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+ justify-content: center;
+ align-items: center;
+}
+
+.splash-close {
+ animation: fadeout 0.25s linear forwards;
+}
+
+@keyframes fadeout {
+ 0% {
+ opacity: 100%;
+ }
+
+ 100% {
+ opacity: 0;
+ visibility: collapse;
+ }
+}
diff --git a/viewer/CastViewer/CastViewer.Browser/wwwroot/favicon.ico b/viewer/CastViewer/CastViewer.Browser/wwwroot/favicon.ico
new file mode 100644
index 0000000..da8d49f
Binary files /dev/null and b/viewer/CastViewer/CastViewer.Browser/wwwroot/favicon.ico differ
diff --git a/viewer/CastViewer/CastViewer.Browser/wwwroot/index.html b/viewer/CastViewer/CastViewer.Browser/wwwroot/index.html
new file mode 100644
index 0000000..a00f52c
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Browser/wwwroot/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+ CastViewer.Browser
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer.Browser/wwwroot/main.js b/viewer/CastViewer/CastViewer.Browser/wwwroot/main.js
new file mode 100644
index 0000000..3604add
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Browser/wwwroot/main.js
@@ -0,0 +1,13 @@
+import { dotnet } from './_framework/dotnet.js'
+
+const is_browser = typeof window != "undefined";
+if (!is_browser) throw new Error(`Expected to be running in a browser`);
+
+const dotnetRuntime = await dotnet
+ .withDiagnosticTracing(false)
+ .withApplicationArgumentsFromQuery()
+ .create();
+
+const config = dotnetRuntime.getConfig();
+
+await dotnetRuntime.runMain(config.mainAssemblyName, [window.location.search]);
diff --git a/viewer/CastViewer/CastViewer.Core/CastViewer.Core.csproj b/viewer/CastViewer/CastViewer.Core/CastViewer.Core.csproj
new file mode 100644
index 0000000..050a30c
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Core/CastViewer.Core.csproj
@@ -0,0 +1,15 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer.Core/Model/DisplayCast.cs b/viewer/CastViewer/CastViewer.Core/Model/DisplayCast.cs
new file mode 100644
index 0000000..2a5ed9e
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Core/Model/DisplayCast.cs
@@ -0,0 +1,150 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using CevioCasts;
+using PropertyModels.ComponentModel.DataAnnotations;
+using PropertyModels.Extensions;
+
+namespace CastViewer.Core.Model;
+
+public sealed class DisplayCast : Cast
+{
+ [Category("Basic")]
+ new public string? Id { get; set; }
+ [Category("Basic")]
+ new public string? Cname { get; set; }
+ [Category("Basic")]
+ [ConditionTarget]
+ new public Category Category { get; set; }
+
+ [Category("Basic")]
+ new public Product Product { get; set; }
+ [Category("Basic")]
+ new public Gender Gender { get; set; }
+ [Category("Basic")]
+ new public Lang Lang { get; set; }
+
+ [Category("Basic")]
+ [Editable(false)]
+ new public BindingList? Names { get; set; }
+
+ [Category("Basic")]
+ [Editable(false)]
+ new public BindingList? Versions { get; set; }
+
+ [Category("Emotion")]
+ [Editable(false)]
+ [ReadOnly(true)]
+ [ConditionTarget]
+ new public bool HasEmotions { get; set; }
+
+ [Category("Emotion")]
+ [Editable(false)]
+ [VisibilityPropertyCondition(nameof(HasEmotions), true)]
+ new public BindingList? Emotions { get; set; }
+
+ [Category("Song")]
+ [Editable(false)]
+ [VisibilityPropertyCondition(nameof(Category), CevioCasts.Category.SingerSong)]
+ new public BindingList? SpSymbols { get; set; }
+
+ [Category("Emotion")]
+ [Editable(false)]
+ [VisibilityPropertyCondition(nameof(HasEmotions), true)]
+ new public BindingList? EmotionOrder { get; set; }
+
+ public DisplayCast(Cast? instance)
+ {
+ if(instance is null){ return; }
+
+ // リフレクションを使用してプロパティをコピー
+ foreach (PropertyInfo property in typeof(Cast).GetProperties())
+ {
+ var value = property?.GetValue(instance);
+ property?.SetValue(this, value);
+ }
+
+ Id = instance.Id ?? "ID NOT FOUND";
+ Cname = instance.Cname ?? "CNAME NOT FOUND";
+ Product = instance.Product;
+ Gender = instance.Gender;
+ Lang = instance.Lang;
+ Names = new(instance.Names.Select(v => v.Display).ToList());
+ Versions = new(instance.Versions);
+ Category = instance.Category;
+ HasEmotions = instance.HasEmotions;
+
+ var emotions = (instance?.Emotions ?? [])
+ .Select(v => new(v));
+ Emotions = [ .. emotions ];
+
+ var sbs = instance?.SpSymbols ?? [];
+ var symbols = sbs
+ .Select(v => new(v));
+ SpSymbols = [.. symbols];
+
+ var orders = instance?.EmotionOrder is null
+ ? []
+ : instance.EmotionOrder
+ .Where(item => item is not null)
+ .ToList()
+ .Select(v => new(v))
+ ;
+ EmotionOrder = orders is null
+ ? []
+ : [.. orders];
+ }
+}
+
+[Category("Song")]
+[TypeConverter(typeof(ExpandableObjectConverter))]
+public class DisplaySpSymbol
+{
+ [Category("Song")]
+ [Editable(false)]
+ public string? Id { get; set; }
+
+ [Category("Song")]
+ [Editable(false)]
+ public BindingList? Names { get; set; }
+
+ [Category("Song")]
+ [Editable(false)]
+ public BindingList? Symbols { get; set; }
+
+ public DisplaySpSymbol(SpSymbol instance)
+ {
+ if (instance is null) { return; }
+
+ Id = instance.Id;
+ Names =
+ new(instance.Names.Select(v => v.Display).ToList());
+ Symbols = [.. instance.Symbols];
+ }
+
+ public override string ToString()
+ {
+ return $"{Id}: {string.Join(", ", Names ?? [])}, {string.Join("|", Symbols ?? [])}";
+ }
+}
+
+[Category("Emotion")]
+[TypeConverter(typeof(ExpandableObjectConverter))]
+public class DisplayEmotionOrder
+{
+ [Category("Emotion")]
+ [Editable(false)]
+ public string? Version { get; set; }
+
+ [Category("Emotion")]
+ [Editable(false)]
+ public BindingList? Order { get; set; }
+
+ public DisplayEmotionOrder(EmotionOrder instance)
+ {
+ if (instance is null) { return; }
+
+ Version = instance.Version;
+ Order = [..instance.Order];
+ }
+}
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer.Core/Model/DisplayEmotion.cs b/viewer/CastViewer/CastViewer.Core/Model/DisplayEmotion.cs
new file mode 100644
index 0000000..75c27d7
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Core/Model/DisplayEmotion.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using CevioCasts;
+
+namespace CastViewer.Core.Model;
+
+[Category("Emotion")]
+[TypeConverter(typeof(ExpandableObjectConverter))]
+public class DisplayEmotion(Emotion instance) : Emotion
+{
+ [Category("Emotion")]
+ [Editable(false)]
+ new public string Id { get; set; } = instance.Id;
+
+ [Category("Emotion")]
+ [Editable(false)]
+ new public BindingList Names { get; set; } =
+ new(instance.Names.Select(v => v.Display).ToList());
+}
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer.Desktop/CastViewer.Desktop.csproj b/viewer/CastViewer/CastViewer.Desktop/CastViewer.Desktop.csproj
new file mode 100644
index 0000000..17fe289
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Desktop/CastViewer.Desktop.csproj
@@ -0,0 +1,39 @@
+
+
+ WinExe
+
+ net8.0
+ enable
+ true
+
+
+
+ app.manifest
+
+
+
+ true
+ /_/
+ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\'))
+ $(RepoRoot)=$(DeterministicSourceRoot)
+ false
+ true
+ copyused
+ true
+ true
+ true
+ false
+ en-US;ja-JP
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.Desktop/Program.cs b/viewer/CastViewer/CastViewer.Desktop/Program.cs
new file mode 100644
index 0000000..49d415c
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Desktop/Program.cs
@@ -0,0 +1,23 @@
+using System;
+using Avalonia;
+using Avalonia.ReactiveUI;
+
+namespace CastViewer.Desktop;
+
+sealed class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace()
+ .UseReactiveUI();
+}
diff --git a/viewer/CastViewer/CastViewer.Desktop/app.manifest b/viewer/CastViewer/CastViewer.Desktop/app.manifest
new file mode 100644
index 0000000..b068dfc
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.Desktop/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.iOS/AppDelegate.cs b/viewer/CastViewer/CastViewer.iOS/AppDelegate.cs
new file mode 100644
index 0000000..a78a651
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.iOS/AppDelegate.cs
@@ -0,0 +1,25 @@
+using Foundation;
+using UIKit;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.iOS;
+using Avalonia.Media;
+using Avalonia.ReactiveUI;
+
+namespace CastViewer.iOS;
+
+// The UIApplicationDelegate for the application. This class is responsible for launching the
+// User Interface of the application, as well as listening (and optionally responding) to
+// application events from iOS.
+[Register("AppDelegate")]
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+public partial class AppDelegate : AvaloniaAppDelegate
+#pragma warning restore CA1711 // Identifiers should not have incorrect suffix
+{
+ protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
+ {
+ return base.CustomizeAppBuilder(builder)
+ .WithInterFont()
+ .UseReactiveUI();
+ }
+}
diff --git a/viewer/CastViewer/CastViewer.iOS/CastViewer.iOS.csproj b/viewer/CastViewer/CastViewer.iOS/CastViewer.iOS.csproj
new file mode 100644
index 0000000..29fab51
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.iOS/CastViewer.iOS.csproj
@@ -0,0 +1,16 @@
+
+
+ Exe
+ net8.0-ios
+ 13.0
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.iOS/Entitlements.plist b/viewer/CastViewer/CastViewer.iOS/Entitlements.plist
new file mode 100644
index 0000000..c2cba09
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.iOS/Entitlements.plist
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer.iOS/Info.plist b/viewer/CastViewer/CastViewer.iOS/Info.plist
new file mode 100644
index 0000000..f3ed229
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.iOS/Info.plist
@@ -0,0 +1,43 @@
+
+
+
+
+ CFBundleDisplayName
+ CastViewer
+ CFBundleIdentifier
+ companyName.CastViewer
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1.0
+ LSRequiresIPhoneOS
+
+ MinimumOSVersion
+ 13.0
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/viewer/CastViewer/CastViewer.iOS/Main.cs b/viewer/CastViewer/CastViewer.iOS/Main.cs
new file mode 100644
index 0000000..18744cf
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.iOS/Main.cs
@@ -0,0 +1,14 @@
+using UIKit;
+
+namespace CastViewer.iOS;
+
+public class Application
+{
+ // 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));
+ }
+}
diff --git a/viewer/CastViewer/CastViewer.iOS/Resources/LaunchScreen.xib b/viewer/CastViewer/CastViewer.iOS/Resources/LaunchScreen.xib
new file mode 100644
index 0000000..eb51ef4
--- /dev/null
+++ b/viewer/CastViewer/CastViewer.iOS/Resources/LaunchScreen.xib
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer/App.axaml b/viewer/CastViewer/CastViewer/App.axaml
new file mode 100644
index 0000000..e18bead
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/App.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ avares://CastViewer/Assets/NotoSansCJKjp-Regular.otf
+
+
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer/App.axaml.cs b/viewer/CastViewer/CastViewer/App.axaml.cs
new file mode 100644
index 0000000..38c914c
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/App.axaml.cs
@@ -0,0 +1,49 @@
+using System.Runtime.InteropServices;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using CastViewer.ViewModels;
+using CastViewer.Views;
+using HotAvalonia;
+
+namespace CastViewer;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ switch (RuntimeInformation.OSArchitecture)
+ {
+ case Architecture.Wasm:
+ break;
+
+ default:
+ {
+ this.EnableHotReload();
+ break;
+ }
+ }
+
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow
+ {
+ DataContext = new MainViewModel()
+ };
+ }
+ else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
+ {
+ singleViewPlatform.MainView = new MainView
+ {
+ DataContext = new MainViewModel()
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer/Assets/NotoSansCJKjp-Regular.otf b/viewer/CastViewer/CastViewer/Assets/NotoSansCJKjp-Regular.otf
new file mode 100644
index 0000000..f562249
Binary files /dev/null and b/viewer/CastViewer/CastViewer/Assets/NotoSansCJKjp-Regular.otf differ
diff --git a/viewer/CastViewer/CastViewer/Assets/avalonia-logo.ico b/viewer/CastViewer/CastViewer/Assets/avalonia-logo.ico
new file mode 100644
index 0000000..da8d49f
Binary files /dev/null and b/viewer/CastViewer/CastViewer/Assets/avalonia-logo.ico differ
diff --git a/viewer/CastViewer/CastViewer/CastViewer.csproj b/viewer/CastViewer/CastViewer/CastViewer.csproj
new file mode 100644
index 0000000..d2a442b
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/CastViewer.csproj
@@ -0,0 +1,53 @@
+
+
+ net8.0
+ enable
+ latest
+ true
+
+
+ $(DefineConstants);ENABLE_XAML_HOT_RELOAD
+
+
+ true
+ /_/
+ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\'))
+ $(RepoRoot)=$(DeterministicSourceRoot)
+ false
+
+ en-US;ja-JP
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer/ViewLocator.cs b/viewer/CastViewer/CastViewer/ViewLocator.cs
new file mode 100644
index 0000000..edd7680
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/ViewLocator.cs
@@ -0,0 +1,32 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using CastViewer.ViewModels;
+
+namespace CastViewer;
+
+public class ViewLocator : IDataTemplate
+{
+ public Control? Build(object? param)
+ {
+ if (param is null)
+ {
+ return null;
+ }
+
+ var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
+ var type = Type.GetType(name);
+
+ if (type != null)
+ {
+ return (Control)Activator.CreateInstance(type)!;
+ }
+
+ return new TextBlock { Text = "Not Found: " + name };
+ }
+
+ public bool Match(object? data)
+ {
+ return data is ViewModelBase;
+ }
+}
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer/ViewModels/MainViewModel.cs b/viewer/CastViewer/CastViewer/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..7b07d11
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/ViewModels/MainViewModel.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
+using System.Linq;
+using CastViewer.Core.Model;
+using CevioCasts;
+using Epoxy;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Platform;
+using System.IO;
+using Avalonia.PropertyGrid.Controls;
+using System.Diagnostics.CodeAnalysis;
+
+namespace CastViewer.ViewModels;
+
+[ViewModel]
+public class MainViewModel : ViewModelBase
+{
+ private ImmutableList? loadedList;
+
+ public ObservableCollection CastList { get; set; }
+
+ public DisplayCast? SelectedCast { get; set; }
+
+ public int SelectedCastIndex { get; set; }
+ public Task AsyncSelectedCast => GetCastDataAsync();
+
+ public bool IsLoading { get; set; }
+ public bool IsPgEnabled { get; set; }
+
+ public Command? CastDataChanged { get; }
+
+ public Pile PgPile { get; }
+ = Pile.Factory.Create();
+
+ public ImmutableList TestList { get; } =
+ [
+ new()
+ {
+ Id = "thisisid1",
+ Cname = "thisiscname1",
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語名前1", },
+ new() { Lang = Lang.English, Display = "English name 1", },
+ ],
+ Category = Category.SingerSong,
+ Product = Product.CeVIO_AI,
+ Gender = Gender.Male,
+ Lang = Lang.Japanese,
+ Versions = ["2.0.0", "1.0.1"],
+ HasEmotions = true,
+ Emotions =
+ [
+ new()
+ {
+ Id = "id1",
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語感情名前1", },
+ new() { Lang = Lang.English, Display = "English emo name 1", },
+ ]
+ },
+ new()
+ {
+ Id = "id2",
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語感情名前2", },
+ new() { Lang = Lang.English, Display = "English emo name 2", },
+ ]
+ },
+ ],
+ SpSymbols =
+ [
+ new()
+ {
+ Id = "sp-id1",
+ Symbols = ["@", "@"],
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語 sp 名前1", },
+ new() { Lang = Lang.English, Display = "English sp name 1", },
+ ],
+ },
+ new()
+ {
+ Id = "sp-id2",
+ Symbols = ["$", "$"],
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語 sp 名前2", },
+ new() { Lang = Lang.English, Display = "English sp name 2", },
+ ],
+ }
+ ],
+ },
+ new()
+ {
+ Id = "thisisid2",
+ Cname = "thisiscname2",
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語名前2", },
+ new() { Lang = Lang.English, Display = "English name 2", },
+ ],
+ Category = Category.TextVocal,
+ Product = Product.VoiSona,
+ Gender = Gender.Female,
+ Lang = Lang.English,
+ Versions = ["2.0.0", "1.1.0", "1.0.1 ported from CeVIO AI"],
+ HasEmotions = true,
+ Emotions =
+ [
+ new()
+ {
+ Id = "id1",
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語感情名前1", },
+ new() { Lang = Lang.English, Display = "English emo name 1", },
+ ]
+ },
+ new()
+ {
+ Id = "id2",
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語感情名前2", },
+ new() { Lang = Lang.English, Display = "English emo name 2", },
+ ]
+ },
+ new()
+ {
+ Id = "id3",
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語感情名前3", },
+ new() { Lang = Lang.English, Display = "English emo name 3", },
+ ]
+ },
+ new()
+ {
+ Id = "id4",
+ Names =
+ [
+ new() { Lang = Lang.Japanese, Display = "日本語感情名前4", },
+ new() { Lang = Lang.English, Display = "English emo name 4", },
+ ]
+ },
+ ],
+ EmotionOrder =
+ [
+ new() { Version = "2.0.0", Order = ["id2", "id1", "id3", "id4"], },
+ new() { Version = "1.1.0", Order = ["id1", "id2", "id3", "id4"], }
+ ],
+ },
+ ];
+
+ public MainViewModel()
+ {
+ var list = TestList
+ .Select(v => new(v))
+ .ToImmutableList();
+ CastList = new(list);
+
+ using var fs = new StreamReader(AssetLoader.Open(new Uri("avares://CastViewer/Assets/data.json")));
+ string jsonString = fs.ReadToEnd();
+ var definitions = Definitions.FromJson(jsonString, true);
+ loadedList = definitions
+ .Casts
+ .Select(v => new(v))
+ .ToImmutableList();
+ CastList = new(loadedList);
+
+ IsPgEnabled = true;
+ }
+
+ private Task GetCastDataAsync()
+ {
+ IsLoading = true;
+
+ return Task.Run(() => {
+ var cast = CastList[SelectedCastIndex];
+ if(cast is null)
+ {
+ IsLoading = false;
+ return new DisplayCast(null);
+ }
+
+ IsLoading = false;
+ return cast;
+ });
+ }
+
+ [PropertyChanged(nameof(SelectedCast))]
+ [SuppressMessage("","IDE0051")]
+ private async ValueTask SelectedCastChangedAsync(DisplayCast value)
+ {
+ if(PgPile is null){return;}
+
+ IsLoading = true;
+ await PgPile.RentAsync(async pg =>
+ {
+ await Task.Delay(300);
+ pg.DataContext = value;
+ await Task.Delay(500);
+ });
+ IsLoading = false;
+
+ //return default;
+ }
+}
diff --git a/viewer/CastViewer/CastViewer/ViewModels/ViewModelBase.cs b/viewer/CastViewer/CastViewer/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..0beda65
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/ViewModels/ViewModelBase.cs
@@ -0,0 +1,7 @@
+using ReactiveUI;
+
+namespace CastViewer.ViewModels;
+
+public class ViewModelBase : ReactiveObject
+{
+}
diff --git a/viewer/CastViewer/CastViewer/Views/MainView.axaml b/viewer/CastViewer/CastViewer/Views/MainView.axaml
new file mode 100644
index 0000000..d96bf4b
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/Views/MainView.axaml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/viewer/CastViewer/CastViewer/Views/MainView.axaml.cs b/viewer/CastViewer/CastViewer/Views/MainView.axaml.cs
new file mode 100644
index 0000000..8b17da4
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/Views/MainView.axaml.cs
@@ -0,0 +1,50 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.PropertyGrid.Controls;
+using CastViewer.ViewModels;
+
+namespace CastViewer.Views;
+
+public partial class MainView : UserControl
+{
+ public MainView()
+ {
+ InitializeComponent();
+ }
+
+ private void Listbox_SelectionChanged(
+ object? sender,
+ SelectionChangedEventArgs e)
+ {
+ if (sender is not ListBox listBox || listBox.SelectedItem is null)
+ {
+ return;
+ }
+
+ if (DataContext is not MainViewModel vm)
+ {
+ return;
+ }
+
+ //vm.IsLoading = true;
+ }
+
+ private void PropertyGrid_Loaded(
+ object? sender,
+ EventArgs e
+ )
+ {
+ if (sender is not PropertyGrid pg || !pg.IsLoaded)
+ {
+ return;
+ }
+
+ if (DataContext is not MainViewModel vm)
+ {
+ return;
+ }
+
+ vm.IsLoading = false;
+ }
+}
\ No newline at end of file
diff --git a/viewer/CastViewer/CastViewer/Views/MainWindow.axaml b/viewer/CastViewer/CastViewer/Views/MainWindow.axaml
new file mode 100644
index 0000000..d2f7573
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/Views/MainWindow.axaml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/viewer/CastViewer/CastViewer/Views/MainWindow.axaml.cs b/viewer/CastViewer/CastViewer/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..7f0405e
--- /dev/null
+++ b/viewer/CastViewer/CastViewer/Views/MainWindow.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace CastViewer.Views;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/viewer/CastViewer/Directory.Build.props b/viewer/CastViewer/Directory.Build.props
new file mode 100644
index 0000000..ed5fd9c
--- /dev/null
+++ b/viewer/CastViewer/Directory.Build.props
@@ -0,0 +1,21 @@
+
+
+ enable
+ 11.0.10
+ 12.0
+ 8.0-Recommended
+ true
+ true
+
+
+
+
+ true
+ /_/
+ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\'))
+ $(RepoRoot)=$(DeterministicSourceRoot)
+ false
+ false
+ en-US;ja-JP
+
+