diff --git a/Blacklight.sln.DotSettings.user b/Blacklight.sln.DotSettings.user index 1ddb57c2e2bc370f3df0f20a3083f46987bcde99..25b17d78d07ae681d6cb60e803133ac7d7bc4313 100644 --- a/Blacklight.sln.DotSettings.user +++ b/Blacklight.sln.DotSettings.user @@ -2,6 +2,7 @@ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AContentControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa0b45743773945a3bb67ca6440ae9eadf9c00_003F50_003F9d315227_003FContentControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADockFluentTheme_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc13735b26c914624b4ee4a88a575ae253ae00_003Feb_003F23efc6b0_003FDockFluentTheme_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe8b9e6b427dceb6f436414441b760d4dc52661df17f386ce4caa48aab354989_003FFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHtmlControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F58931fa041a846b53462c4c4c5f52620bf9de09eff582aa4b1ed83c83e4e10ea_003FHtmlControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F52dbc4b2fb874f79fb4fec0b07cf7f6ca6ac95616e1d164ef210a0645a1aa9_003FIFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANetworkInformation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b1b048ea0e94b28ad7cbac8ef6bc4c26e00_003Fb4_003Fff405095_003FNetworkInformation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AProportionalDock_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F91c655b1795150937d9264ac1431c463ed5e5e8ad0286ad17e16f16175d94_003FProportionalDock_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> diff --git a/Blacklight/App.axaml.cs b/Blacklight/App.axaml.cs index 64ffdd31ea0210db44705a029b94650b4cda2fcf..5d273706f10f7ba267b457bbf7814cab4b258a6b 100644 --- a/Blacklight/App.axaml.cs +++ b/Blacklight/App.axaml.cs @@ -1,3 +1,4 @@ +using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -13,6 +14,9 @@ namespace Blacklight; public partial class App : Application { + + public static IServiceProvider? Services { get; set; } + public override void Initialize() { AvaloniaXamlLoader.Load(this); @@ -30,6 +34,7 @@ public partial class App : Application services.AddTransient<LoginViewModel>(); var serviceProvider = services.BuildServiceProvider(); + Services = serviceProvider; serviceProvider.GetRequiredService<ViewNavigationService>(); var mainWindowViewModel = serviceProvider.GetRequiredService<MainWindowViewModel>(); diff --git a/Blacklight/Assets/netcat.jpg b/Blacklight/Assets/netcat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f32608faaebd42a0cbce4d780a282616acc02e7a Binary files /dev/null and b/Blacklight/Assets/netcat.jpg differ diff --git a/Blacklight/Models/TestData.cs b/Blacklight/Models/TestData.cs deleted file mode 100644 index 4e1551c879bb7b0b4920a9f0a149ede94808ec16..0000000000000000000000000000000000000000 --- a/Blacklight/Models/TestData.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Blacklight.Models; - -public class TestData -{ - -} \ No newline at end of file diff --git a/Blacklight/ViewModels/ClientViewModel.cs b/Blacklight/ViewModels/ClientViewModel.cs index 33ac5c4c1f97eaba5701d9da0f2c82673670d49f..8f03eace9e895469cc96a0005ab6fbdf840601da 100644 --- a/Blacklight/ViewModels/ClientViewModel.cs +++ b/Blacklight/ViewModels/ClientViewModel.cs @@ -32,7 +32,7 @@ public class ClientViewModel : ViewModelBase public ClientViewModel() { - _factory = new DockFactory(new TestData()); + _factory = new DockFactory(); DebugFactoryEvents(_factory); diff --git a/Blacklight/ViewModels/DockFactory.cs b/Blacklight/ViewModels/DockFactory.cs index a3a72a0b5bac493016a62ece2d9c63aa346bcda9..14e43fe7f7f86548306e478bd6149e7c236c4ba2 100644 --- a/Blacklight/ViewModels/DockFactory.cs +++ b/Blacklight/ViewModels/DockFactory.cs @@ -13,7 +13,7 @@ using Dock.Model.Mvvm.Controls; namespace Blacklight.ViewModels; -public class DockFactory(object context) : Factory +public class DockFactory : Factory { private IRootDock? _rootDock; private IDocumentDock? _documentDock; @@ -104,11 +104,9 @@ public class DockFactory(object context) : Factory ["Document1"] = () => new Document1(), ["Document2"] = () => new Document1(), ["Resource Explorer"] = () => new ResourceExplorer(), - // ["Settings"] = () => layout, - // ["Client"] = () => context }; - DockableLocator = new Dictionary<string, Func<IDockable?>>() + DockableLocator = new Dictionary<string, Func<IDockable?>> { ["Root"] = () => _rootDock, ["Documents"] = () => _documentDock diff --git a/Blacklight/ViewModels/LoginViewModel.cs b/Blacklight/ViewModels/LoginViewModel.cs index f0bb227b7c64eeab6aa73257891251255b50b47a..024f4bc8f0870330895c29edbeba2a0b0128624e 100644 --- a/Blacklight/ViewModels/LoginViewModel.cs +++ b/Blacklight/ViewModels/LoginViewModel.cs @@ -12,7 +12,17 @@ public class LoginViewModel : ViewModelBase { private object _currentControl = new MainLoginPrompt(); private readonly ViewNavigationService _nav; + public string Network { get; set; } = "lightquark.network"; + + public bool EnableButtons + { + get => _enableButtons; + set => SetProperty(ref _enableButtons, value); + } + private string _status = "Begin"; + private string? _networkError; + private bool _enableButtons = true; public string Status { @@ -20,6 +30,12 @@ public class LoginViewModel : ViewModelBase set => SetProperty(ref _status, value); } + public string? NetworkError + { + get => _networkError; + set => SetProperty(ref _networkError, value); + } + public LqClient Client { get; } public LoginViewModel(ViewNavigationService nav, LqClient lqClient) @@ -27,18 +43,7 @@ public class LoginViewModel : ViewModelBase _nav = nav; Client = lqClient; Status = "Get network"; - RetrieveInitialNetwork(); - } - - private async void RetrieveInitialNetwork() - { - var net = await Client.GetNetworkAsync(); - Status = "Network getted"; - if (net.success) - { - Client.NetworkInformation = net.networkInformation; - Status = "netinfo updated"; - } + SwitchNetwork(); } public object CurrentControl @@ -48,21 +53,70 @@ public class LoginViewModel : ViewModelBase } public ICommand LoginCommand => new RelayCommand(Login); - public ICommand FakeLoginCommand => new RelayCommand(FakeLogin); + public ICommand EnterNetworkSwitcherCommand => new RelayCommand(EnterNetworkSwitcher); + public ICommand UseSolsticeCommand => new RelayCommand(UseSolstice); + + public ICommand UseEquinoxCommand => new RelayCommand(UseEquinox); + public ICommand UseDevCommand => new RelayCommand(UseDev); + public ICommand SwitchNetworkCommand => new RelayCommand(SwitchNetwork); public ICommand WaitNvmCommand => new RelayCommand(WaitNvm); + private void Login() { _nav.ShowView<ClientViewModel>(); } - private void FakeLogin() + private void EnterNetworkSwitcher() { - throw new Exception("oopsie :3"); - //CurrentControl = new NetworkSelectionPrompt(); + CurrentControl = new NetworkSelectionPrompt(); } + private void WaitNvm() { CurrentControl = new MainLoginPrompt(); } + + private void UseSolstice() + { + Network = "lightquark.network"; + SwitchNetwork(); + } + private void UseEquinox() + { + Network = "equinox.lightquark.network"; + SwitchNetwork(); + } + private void UseDev() + { + Network = "dev.lightquark.network"; + SwitchNetwork(); + } + + private async void SwitchNetwork() + { + Client.NetworkInformation = null; + EnableButtons = false; + if (!Network.StartsWith("http://") && !Network.StartsWith("https://")) + { + Network = $"https://{Network}"; + } + + var net = await Client.GetNetworkAsync(new Uri(Network)); + Status = "Network getted"; + if (net.success) + { + NetworkError = null; + Client.NetworkInformation = net.networkInformation; + Status = "netinfo updated"; + CurrentControl = new MainLoginPrompt(); + } + else + { + NetworkError = net.error; + } + EnableButtons = true; + } + + } \ No newline at end of file diff --git a/Blacklight/ViewModels/MainWindowViewModel.cs b/Blacklight/ViewModels/MainWindowViewModel.cs index 99da520c25c9ce8b849e3f96c6cce4346ef8dc2f..212a2d44a95cc5cd71d1304817a183f58b10b2fe 100644 --- a/Blacklight/ViewModels/MainWindowViewModel.cs +++ b/Blacklight/ViewModels/MainWindowViewModel.cs @@ -34,6 +34,7 @@ public class MainWindowViewModel : ViewModelBase services.AddTransient<LoginViewModel>(); var serviceProvider = services.BuildServiceProvider(); + App.Services = serviceProvider; var nav = serviceProvider.GetRequiredService<ViewNavigationService>(); nav.SetMainWindowViewModel(this); _currentViewModel = serviceProvider.GetRequiredService<LoginViewModel>(); diff --git a/Blacklight/Views/Login/MainLoginPrompt.axaml b/Blacklight/Views/Login/MainLoginPrompt.axaml index 03de7528a110e5922c96418c86cac30f42305294..e1508e7d6bbfc6889063edb6a5d5a414011ef01f 100644 --- a/Blacklight/Views/Login/MainLoginPrompt.axaml +++ b/Blacklight/Views/Login/MainLoginPrompt.axaml @@ -7,24 +7,19 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Blacklight.Views.Login.MainLoginPrompt" x:DataType="viewModels:LoginViewModel"> - <Grid ColumnDefinitions="*,*" x:Name="ExpandableContent" RowDefinitions="Auto,*"> - <Image Grid.Column="1" Grid.ColumnSpan="1" Grid.Row="1" Margin="0,5,0,0" Source="avares://Blacklight/Assets/cat.jpg" /> - <Border Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" BorderBrush="Black" BorderThickness="0,0,1,0"> - <StackPanel Margin="0,0,5,0"> - <login:NetworkDisplay /> - </StackPanel> - </Border> + <Grid ColumnDefinitions="*,*" x:Name="ExpandableContent" RowDefinitions="Auto,*,Auto"> + <Border Grid.Column="0" Grid.Row="0" Grid.RowSpan="3" BorderBrush="Black" BorderThickness="0,0,1,0" /> + <StackPanel Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Margin="0,0,5,0"> + <login:NetworkDisplay /> + </StackPanel> + <Button Command="{Binding EnterNetworkSwitcherCommand}" Grid.Column="0" Grid.Row="2" HorizontalAlignment="Center" VerticalAlignment="Bottom">Switch network</Button> <StackPanel Grid.Column="1" Grid.Row="0" Margin="5,5,0,0"> - <StackPanel.Transitions> - <Transitions> - - <!-- <SizeTransition Property="DesiredSize" Duration="0:0:0.3"/> --> - </Transitions> - </StackPanel.Transitions> <TextBox Watermark="emilia@015.sh" Margin="15, 5" /> - <TextBox Watermark="hunter3" Margin="15, 5" /> - <Button HorizontalAlignment="Center" Padding="50,5" Command="{Binding FakeLoginCommand}">Log in</Button> + <TextBox PasswordChar="*" Watermark="hunter3" Margin="15, 5" /> + <Button HorizontalAlignment="Center" Padding="50,5" Command="{Binding LoginCommand}">Log in</Button> </StackPanel> + + <Image Grid.Column="1" Grid.RowSpan="2" Grid.Row="1" Margin="0,5,0,0" Source="avares://Blacklight/Assets/cat.jpg" /> </Grid> </UserControl> \ No newline at end of file diff --git a/Blacklight/Views/Login/NetworkDisplay.axaml b/Blacklight/Views/Login/NetworkDisplay.axaml index c3282afb5019bfa291d65c4b45ddc991922f93da..ce67645fd83dbbcfdda50f82fba80ef47732b8db 100644 --- a/Blacklight/Views/Login/NetworkDisplay.axaml +++ b/Blacklight/Views/Login/NetworkDisplay.axaml @@ -31,6 +31,7 @@ </utility:MarkdownText> </StackPanel> - <utility:MarkdownText Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" Text="{Binding Client.NetworkInformation.Description }" /> + <utility:MarkdownText Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" LinksAreNetworks="True" Text="{Binding Client.NetworkInformation.Description }" /> + <TextBlock FontWeight="Bold" Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" VerticalAlignment="Bottom" TextWrapping="Wrap" Foreground="Red" Text="{Binding NetworkError}" /> </Grid> </UserControl> diff --git a/Blacklight/Views/Login/NetworkSelectionPrompt.axaml b/Blacklight/Views/Login/NetworkSelectionPrompt.axaml index 000594b423ddb10633377159e10d29e12a3df41d..081d0ca3c6d1b0fe9d245b6fe6c830a8c1623e88 100644 --- a/Blacklight/Views/Login/NetworkSelectionPrompt.axaml +++ b/Blacklight/Views/Login/NetworkSelectionPrompt.axaml @@ -3,11 +3,27 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModels="clr-namespace:Blacklight.ViewModels" + xmlns:login="clr-namespace:Blacklight.Views.Login" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Blacklight.Views.Login.NetworkSelectionPrompt" x:DataType="viewModels:LoginViewModel"> - <StackPanel> - <Button Command="{Binding LoginCommand}">Actually log in</Button> - <Button Command="{Binding WaitNvmCommand}">wait nvm</Button> - </StackPanel> + <Grid ColumnDefinitions="*,*" x:Name="ExpandableContent" RowDefinitions="Auto,*,Auto"> + <Border Grid.Column="0" Grid.Row="0" Grid.RowSpan="3" BorderBrush="Black" BorderThickness="0,0,1,0" /> + <StackPanel Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Margin="0,0,5,0"> + <login:NetworkDisplay /> + </StackPanel> + <StackPanel Grid.Column="0" Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"> + <Button IsEnabled="{Binding EnableButtons}" Margin="1,2,4,1" HorizontalAlignment="Stretch" Command="{Binding UseSolsticeCommand}">Solstice (Official)</Button> + <Button IsEnabled="{Binding EnableButtons}" Margin="1,2,4,1" HorizontalAlignment="Stretch" Command="{Binding UseEquinoxCommand}">Equinox (015 Test Server)</Button> + <Button IsEnabled="{Binding EnableButtons}" Margin="1,2,4,1" HorizontalAlignment="Stretch" Command="{Binding UseDevCommand}">Dev (015 Dev Server)</Button> + </StackPanel> + + <StackPanel Grid.Column="1" Grid.Row="0" Margin="5,5,0,0"> + <TextBox Text="{Binding Network}" Watermark="(https://)lightquark.network" Margin="15, 5" /> + <Button IsEnabled="{Binding EnableButtons}" HorizontalAlignment="Center" Padding="50,5" Command="{Binding SwitchNetworkCommand}">Switch</Button> + <Button IsEnabled="{Binding EnableButtons}" HorizontalAlignment="Center" Padding="50,5" Margin="0,5,0,0" Command="{Binding WaitNvmCommand}">Back</Button> + </StackPanel> + + <Image Grid.Column="1" Grid.RowSpan="2" Grid.Row="1" Margin="0,5,0,0" Source="avares://Blacklight/Assets/netcat.jpg" /> + </Grid> </UserControl> diff --git a/Blacklight/Views/Utility/MarkdownText.axaml.cs b/Blacklight/Views/Utility/MarkdownText.axaml.cs index 58062fc79c2328843353874394e08f4d9c9d0135..dcb5290f3120549b0f1ee2de43315f25f025d1ac 100644 --- a/Blacklight/Views/Utility/MarkdownText.axaml.cs +++ b/Blacklight/Views/Utility/MarkdownText.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Globalization; using Avalonia; using Avalonia.Controls; @@ -6,13 +7,22 @@ using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Threading; +using Lightquark.NET; using Markdig; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using Microsoft.Extensions.DependencyInjection; using TheArtOfDev.HtmlRenderer.Avalonia; +using TheArtOfDev.HtmlRenderer.Core.Entities; namespace Blacklight.Views.Utility; public partial class MarkdownText : UserControl { + public static readonly StyledProperty<bool> LinksAreNetworksProperty = + AvaloniaProperty.Register<MarkdownText, bool>(nameof(LinksAreNetworks)); + public static readonly StyledProperty<string?> TextProperty = AvaloniaProperty.Register<MarkdownText, string?>(nameof(Text)); @@ -22,10 +32,55 @@ public partial class MarkdownText : UserControl set => SetValue(TextProperty, value); } + public bool LinksAreNetworks + { + get => GetValue(LinksAreNetworksProperty); + set => SetValue(LinksAreNetworksProperty, value); + } + public MarkdownText() { InitializeComponent(); + HtmlPanel.LinkClicked += OnLinkClicked; + } + + private async void OnLinkClicked(object? sender, HtmlRendererRoutedEventArgs<HtmlLinkClickedEventArgs> e) + { + if (LinksAreNetworks) + { + e.Event.Handled = true; + var lqClient = App.Services!.GetRequiredService<LqClient>(); + var res = await lqClient.GetNetworkAsync(new Uri(e.Event.Link)); + if (res.success) + { + lqClient.NetworkInformation = res.networkInformation; + } + else + { + Console.WriteLine("Not a network! Opening link..."); + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel == null) + { + Console.WriteLine("Top level null"); + return; + } + await Dispatcher.UIThread.InvokeAsync(async () => + { + var success = await topLevel.Launcher.LaunchUriAsync(new Uri(e.Event.Link)); + Console.WriteLine($"Opened? {success}"); + if (!success) + { + Console.WriteLine($"Fallback method..."); + Process.Start(new ProcessStartInfo + { + FileName = e.Event.Link, + UseShellExecute = true + }); + } + }); + } + } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -48,7 +103,9 @@ public partial class MarkdownText : UserControl if (Text != null) { - var htmlContent = Markdown.ToHtml(Text); + var markdown = Markdown.Parse(Text); + + var htmlContent = markdown.ToHtml(); if (htmlContent.StartsWith("<p>")) { @@ -62,6 +119,10 @@ public partial class MarkdownText : UserControl HtmlPanel.Text = htmlContent; } + else + { + HtmlPanel.Text = ""; + } } base.OnPropertyChanged(change); diff --git a/Lightquark.NET/LqClient.cs b/Lightquark.NET/LqClient.cs index 7c2abfac9db63b3e163ee360ba2782c695adaf1d..039eeb4c16fb94065d8a1d227212d625f21a0b87 100644 --- a/Lightquark.NET/LqClient.cs +++ b/Lightquark.NET/LqClient.cs @@ -46,18 +46,26 @@ public class LqClient : ObservableObject try { var finalUri = new Uri(networkUri, $"{Version}/network"); + Console.WriteLine($"{finalUri}"); var res = await _httpClient.GetAsync(finalUri); - if (!res.IsSuccessStatusCode) return (false, $"Could not retrieve network information (code {res.StatusCode})", null); + if (!res.IsSuccessStatusCode) + return (false, $"Could not retrieve network information (code {res.StatusCode})", null); var json = await res.Content.ReadAsStringAsync(); var networkInformation = JsonConvert.DeserializeObject<EnrichedNetworkInformation>(json); - if (networkInformation == null) return (false, - "Malformed network information response. Are you sure this is a Lightquark network?", - null); + if (networkInformation == null) + return (false, + "Malformed network information response. Are you sure this is a Lightquark network?", + null); Console.WriteLine(networkInformation.IconUrl); var iconRes = await _httpClient.GetAsync(networkInformation.IconUrl); networkInformation.Icon = new Bitmap(await iconRes.Content.ReadAsStreamAsync()); return (true, string.Empty, networkInformation); } + catch (JsonReaderException ex) + { + Console.WriteLine(ex); + return (false, "Malformed network information response. Are you sure this is a Lightquark network?", null); + } catch (Exception ex) { Console.Error.WriteLine(ex);