공개: KoTalk 최신 기준선

This commit is contained in:
Ian 2026-04-16 09:24:26 +09:00
commit debf62f76e
572 changed files with 41689 additions and 0 deletions

View file

@ -0,0 +1,142 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PhysOn.Desktop.ViewModels"
x:Class="PhysOn.Desktop.Views.ConversationWindow"
x:DataType="vm:ConversationWindowViewModel"
Width="460"
Height="760"
MinWidth="360"
MinHeight="520"
Background="#F6F7F8"
Title="{Binding ConversationTitle}">
<Window.Styles>
<Style Selector="Border.surface">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#E6E8EB" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Border.soft">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Background" Value="#F7F8F9" />
<Setter Property="BorderBrush" Value="#ECEEF1" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.icon">
<Setter Property="CornerRadius" Value="9" />
<Setter Property="Padding" Value="9,7" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#D9DDE2" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.primary">
<Setter Property="CornerRadius" Value="9" />
<Setter Property="Padding" Value="12,9" />
<Setter Property="Background" Value="#111418" />
<Setter Property="Foreground" Value="#FFFFFF" />
</Style>
<Style Selector="TextBlock.caption">
<Setter Property="FontSize" Value="12" />
<Setter Property="Foreground" Value="#6A7380" />
</Style>
<Style Selector="TextBlock.body">
<Setter Property="FontSize" Value="13" />
<Setter Property="Foreground" Value="#151A20" />
</Style>
<Style Selector="Border.bubble">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="8,7" />
<Setter Property="Margin" Value="0,0,0,5" />
<Setter Property="Background" Value="#F6F7F8" />
<Setter Property="BorderBrush" Value="#EAEDF0" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
<Style Selector="Border.bubble.mine">
<Setter Property="Background" Value="#ECEFF3" />
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
</Window.Styles>
<Grid Margin="16" RowDefinitions="Auto,Auto,*,Auto" RowSpacing="10">
<Border Classes="surface" Padding="12">
<Grid ColumnDefinitions="42,*,Auto" ColumnSpacing="12">
<Border Width="42" Height="42" Classes="soft">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding ConversationGlyph}"
FontWeight="SemiBold"
Foreground="#151A20" />
</Border>
<StackPanel Grid.Column="1" Spacing="2">
<TextBlock Text="{Binding ConversationTitle}"
FontSize="15"
FontWeight="SemiBold"
Foreground="#151A20"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
<Border Classes="soft" Padding="8,5">
<TextBlock Text="{Binding StatusText}" Classes="caption" />
</Border>
<Button Classes="icon"
Command="{Binding ReloadCommand}"
Content="↻" />
</StackPanel>
</Grid>
</Border>
<Border Grid.Row="1"
Classes="surface"
Padding="10"
IsVisible="{Binding HasErrorText}">
<TextBlock Text="{Binding ErrorText}"
Foreground="#C62828"
Classes="caption"
TextWrapping="Wrap" />
</Border>
<Border Grid.Row="2" Classes="surface" Padding="12">
<ScrollViewer Name="MessagesScrollViewer">
<ItemsControl ItemsSource="{Binding Messages}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:MessageRowViewModel">
<Border Classes="bubble"
Classes.mine="{Binding IsMine}">
<StackPanel Spacing="4">
<TextBlock Text="{Binding SenderName}"
Classes="caption"
FontWeight="SemiBold"
IsVisible="{Binding ShowSenderName}" />
<TextBlock Text="{Binding Text}"
Classes="body"
TextWrapping="Wrap" />
<TextBlock Text="{Binding MetaText}" Classes="caption" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
<Border Grid.Row="3" Classes="surface" Padding="10">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="10">
<TextBox Name="ComposerTextBox"
PlaceholderText="메시지"
AcceptsReturn="True"
TextWrapping="Wrap"
MinHeight="46"
Text="{Binding ComposerText}"
KeyDown="ComposerTextBox_OnKeyDown" />
<Button Grid.Column="1"
Classes="primary"
Command="{Binding SendMessageCommand}"
Content="보내기" />
</Grid>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,72 @@
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;
using PhysOn.Desktop.ViewModels;
namespace PhysOn.Desktop.Views;
public partial class ConversationWindow : Window
{
private ConversationWindowViewModel? _boundViewModel;
public ConversationWindow()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}
private async void ComposerTextBox_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && !e.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
if (DataContext is ConversationWindowViewModel viewModel)
{
await viewModel.SendMessageFromShortcutAsync();
e.Handled = true;
}
}
}
private void OnDataContextChanged(object? sender, EventArgs e)
{
if (_boundViewModel is not null)
{
_boundViewModel.Messages.CollectionChanged -= Messages_OnCollectionChanged;
}
_boundViewModel = DataContext as ConversationWindowViewModel;
if (_boundViewModel is not null)
{
_boundViewModel.Messages.CollectionChanged += Messages_OnCollectionChanged;
}
}
private void Messages_OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action is NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Reset or NotifyCollectionChangedAction.Replace)
{
Dispatcher.UIThread.Post(ScrollMessagesToEnd, DispatcherPriority.Background);
}
}
protected override void OnClosed(EventArgs e)
{
if (_boundViewModel is not null)
{
_boundViewModel.Messages.CollectionChanged -= Messages_OnCollectionChanged;
}
base.OnClosed(e);
}
private void ScrollMessagesToEnd()
{
if (this.FindControl<ScrollViewer>("MessagesScrollViewer") is { } scrollViewer)
{
scrollViewer.Offset = new Vector(scrollViewer.Offset.X, scrollViewer.Extent.Height);
}
}
}

View file

@ -0,0 +1,712 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PhysOn.Desktop.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Name="RootWindow"
x:Class="PhysOn.Desktop.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
d:DesignWidth="1440"
d:DesignHeight="900"
Title="KoTalk"
Width="1440"
Height="900"
MinWidth="1040"
MinHeight="680"
Background="#F6F7F8">
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<Window.Styles>
<Style Selector="Border.surface">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#E4E7EB" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Border.surface-muted">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Background" Value="#F5F6F7" />
<Setter Property="BorderBrush" Value="#E8EBEF" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Border.rail-surface">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#E4E7EB" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Border.dashboard-card">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Background" Value="#F7F8F9" />
<Setter Property="BorderBrush" Value="#E8EBEF" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="12,10" />
</Style>
<Style Selector="Border.empty-card">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#E4E7EB" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="18" />
</Style>
<Style Selector="TextBlock.display-title">
<Setter Property="FontSize" Value="28" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="#111418" />
</Style>
<Style Selector="TextBlock.section-title">
<Setter Property="FontSize" Value="15" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="#111418" />
</Style>
<Style Selector="TextBlock.body">
<Setter Property="FontSize" Value="12.5" />
<Setter Property="Foreground" Value="#1D232B" />
</Style>
<Style Selector="TextBlock.caption">
<Setter Property="FontSize" Value="11.5" />
<Setter Property="Foreground" Value="#6A7480" />
</Style>
<Style Selector="TextBlock.eyebrow">
<Setter Property="FontSize" Value="11" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="#6A7480" />
</Style>
<Style Selector="TextBox.input">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="11,8" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#D9DEE4" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="TextBox.search-input">
<Setter Property="CornerRadius" Value="9" />
<Setter Property="Padding" Value="10,7" />
<Setter Property="Background" Value="#F6F7F8" />
<Setter Property="BorderBrush" Value="#E4E7EB" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="Button.primary-button">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="14,10" />
<Setter Property="Background" Value="#111418" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.secondary-button">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="13,10" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="Foreground" Value="#1E252C" />
<Setter Property="BorderBrush" Value="#D9DEE4" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.icon-button">
<Setter Property="Height" Value="34" />
<Setter Property="MinWidth" Value="34" />
<Setter Property="CornerRadius" Value="9" />
<Setter Property="Padding" Value="10,0" />
<Setter Property="Background" Value="#F7F8F9" />
<Setter Property="Foreground" Value="#1E252C" />
<Setter Property="BorderBrush" Value="#E4E7EB" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="Button.icon-button.compact">
<Setter Property="MinWidth" Value="28" />
<Setter Property="Height" Value="28" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="8,0" />
<Setter Property="FontSize" Value="11" />
</Style>
<Style Selector="Button.filter-button">
<Setter Property="CornerRadius" Value="999" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="Background" Value="#F7F8F9" />
<Setter Property="Foreground" Value="#69727D" />
<Setter Property="BorderBrush" Value="#E4E7EB" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="Button.filter-button.selected">
<Setter Property="Background" Value="#111418" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#111418" />
</Style>
<Style Selector="Button.rail-button">
<Setter Property="MinWidth" Value="48" />
<Setter Property="Height" Value="52" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="Foreground" Value="#1E252C" />
<Setter Property="BorderBrush" Value="#E4E7EB" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="Button.rail-button.active">
<Setter Property="Background" Value="#111418" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#111418" />
</Style>
<Style Selector="Button.row-button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
<Style Selector="Border.row-card">
<Setter Property="CornerRadius" Value="9" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#EEF1F4" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Border.row-card.active">
<Setter Property="Background" Value="#F3F5F7" />
<Setter Property="BorderBrush" Value="#D8DDE4" />
</Style>
<Style Selector="Border.status-chip">
<Setter Property="CornerRadius" Value="999" />
<Setter Property="Padding" Value="8,4" />
<Setter Property="Background" Value="#F7F8F9" />
<Setter Property="BorderBrush" Value="#E4E7EB" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Border.inline-alert">
<Setter Property="CornerRadius" Value="9" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#C9392C" />
<Setter Property="BorderThickness" Value="1,0,0,0" />
<Setter Property="Padding" Value="12,10" />
</Style>
<Style Selector="Border.avatar-badge">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="#F3F4F6" />
</Style>
<Style Selector="Border.unread-badge">
<Setter Property="CornerRadius" Value="999" />
<Setter Property="Background" Value="#111418" />
<Setter Property="Padding" Value="7,2" />
</Style>
<Style Selector="Border.message-bubble">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="#F7F8F9" />
<Setter Property="BorderBrush" Value="#E4E7EB" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="MaxWidth" Value="680" />
<Setter Property="Margin" Value="0,0,0,6" />
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
<Style Selector="Border.message-bubble.mine">
<Setter Property="Background" Value="#EEF1F4" />
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
<Style Selector="Border.message-bubble.pending">
<Setter Property="Opacity" Value="0.72" />
</Style>
<Style Selector="Border.message-bubble.failed">
<Setter Property="BorderBrush" Value="#C9392C" />
</Style>
<Style Selector="TextBlock.message-text">
<Setter Property="FontSize" Value="13" />
<Setter Property="Foreground" Value="#111418" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style Selector="TextBlock.message-meta">
<Setter Property="FontSize" Value="11" />
<Setter Property="Foreground" Value="#69727D" />
</Style>
</Window.Styles>
<Grid Margin="20">
<Grid IsVisible="{Binding ShowOnboarding}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MaxWidth="1120"
ColumnDefinitions="1.2*,420"
ColumnSpacing="18">
<Border Grid.Column="0" Classes="surface" Padding="28">
<Grid RowDefinitions="Auto,Auto,*" RowSpacing="18">
<StackPanel Spacing="10">
<TextBlock Text="KTOP" Classes="eyebrow" />
<StackPanel Spacing="6">
<Border Width="60" Height="60" Classes="surface-muted" HorizontalAlignment="Left">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="KO"
FontSize="20"
FontWeight="Bold"
Foreground="#111418" />
</Border>
<TextBlock Text="KoTalk" Classes="display-title" />
<TextBlock Text="열면 바로 이어지는 대화 워크스페이스" Classes="body" />
</StackPanel>
</StackPanel>
<Grid Grid.Row="1" ColumnDefinitions="*,*,*" ColumnSpacing="10">
<Border Grid.Column="0" Classes="dashboard-card">
<StackPanel Spacing="4">
<TextBlock Text="빠른 입장" Classes="eyebrow" />
<TextBlock Text="표시 이름 + 참여 키" Classes="section-title" />
<TextBlock Text="필수 입력만 바로 보입니다." Classes="caption" TextWrapping="Wrap" />
</StackPanel>
</Border>
<Border Grid.Column="1" Classes="dashboard-card">
<StackPanel Spacing="4">
<TextBlock Text="대화 집중" Classes="eyebrow" />
<TextBlock Text="받은함과 대화만 남김" Classes="section-title" />
<TextBlock Text="불필요한 빈 패널을 줄였습니다." Classes="caption" TextWrapping="Wrap" />
</StackPanel>
</Border>
<Border Grid.Column="2" Classes="dashboard-card">
<StackPanel Spacing="4">
<TextBlock Text="멀티 윈도우" Classes="eyebrow" />
<TextBlock Text="대화별 분리 창" Classes="section-title" />
<TextBlock Text="작업 흐름을 끊지 않고 분리합니다." Classes="caption" TextWrapping="Wrap" />
</StackPanel>
</Border>
</Grid>
<Border Grid.Row="2" Classes="surface-muted" Padding="16">
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12">
<Border Width="36" Height="36" Classes="surface">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="·"
FontWeight="Bold"
Foreground="#111418" />
</Border>
<StackPanel Grid.Column="1" Spacing="4">
<TextBlock Text="세션은 이 기기에만 남깁니다." Classes="section-title" />
<TextBlock Text="서버 주소는 기본으로 숨겨 두고, 변경이 필요할 때만 고급 설정에서 엽니다." Classes="caption" TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
<Border Grid.Column="1" Classes="surface" Padding="22">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*" RowSpacing="10">
<StackPanel Spacing="4">
<TextBlock Text="입장" Classes="section-title" />
<TextBlock Text="필수 입력만 먼저 받습니다." Classes="caption" />
</StackPanel>
<TextBox Grid.Row="1"
Classes="input"
PlaceholderText="표시 이름"
Text="{Binding DisplayName}" />
<TextBox Grid.Row="2"
Classes="input"
PlaceholderText="참여 키"
Text="{Binding InviteCode}" />
<Grid Grid.Row="3" ColumnDefinitions="Auto,*" ColumnSpacing="10">
<Button Classes="secondary-button"
HorizontalAlignment="Left"
Command="{Binding ToggleAdvancedSettingsCommand}"
Content="{Binding AdvancedSettingsButtonText}" />
<TextBox Grid.Column="1"
Classes="input"
IsVisible="{Binding ShowAdvancedSettings}"
PlaceholderText="기본 서버를 바꾸는 경우만 입력"
Text="{Binding ApiBaseUrl}" />
</Grid>
<StackPanel Grid.Row="4" VerticalAlignment="Bottom" Spacing="10">
<CheckBox Content="이 기기에서 이어서 열기" IsChecked="{Binding RememberSession}" />
<Border Classes="inline-alert" IsVisible="{Binding HasErrorText}">
<TextBlock Text="{Binding ErrorText}"
Classes="caption"
Foreground="#C9392C"
TextWrapping="Wrap" />
</Border>
<Button Classes="primary-button"
HorizontalAlignment="Stretch"
Command="{Binding SignInCommand}"
Content="대화 열기" />
</StackPanel>
</Grid>
</Border>
</Grid>
<Grid IsVisible="{Binding ShowShell}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="72" />
<ColumnDefinition Width="{Binding ConversationPaneWidth}" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Classes="rail-surface" Padding="10">
<Grid RowDefinitions="Auto,*,Auto">
<StackPanel Spacing="12">
<Border Width="48" Height="48" Classes="surface-muted">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="KO"
FontWeight="Bold"
Foreground="#111418" />
</Border>
<Button Classes="rail-button active" ToolTip.Tip="받은함">
<StackPanel Spacing="2" HorizontalAlignment="Center">
<TextBlock Text="⌂" HorizontalAlignment="Center" />
<TextBlock Text="대화" Classes="caption" HorizontalAlignment="Center" />
</StackPanel>
</Button>
<Button Classes="rail-button"
ToolTip.Tip="대화를 분리 창으로 열기"
Command="{Binding DetachConversationCommand}">
<StackPanel Spacing="2" HorizontalAlignment="Center">
<TextBlock Text="{Binding DetachedWindowActionGlyph}" HorizontalAlignment="Center" />
<TextBlock Text="분리" Classes="caption" HorizontalAlignment="Center" />
</StackPanel>
</Button>
<Button Classes="rail-button"
ToolTip.Tip="대화 목록 접기"
Command="{Binding ToggleConversationPaneCommand}">
<StackPanel Spacing="2" HorizontalAlignment="Center">
<TextBlock Text="{Binding PaneActionGlyph}" HorizontalAlignment="Center" />
<TextBlock Text="목록" Classes="caption" HorizontalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<StackPanel Grid.Row="2" Spacing="10">
<Border Width="48" Height="48" Classes="surface-muted">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding CurrentUserMonogram}"
FontWeight="SemiBold"
Foreground="#1E252C" />
</Border>
<Button Classes="rail-button"
ToolTip.Tip="로그아웃"
Command="{Binding SignOutCommand}">
<StackPanel Spacing="2" HorizontalAlignment="Center">
<TextBlock Text="⎋" HorizontalAlignment="Center" />
<TextBlock Text="종료" Classes="caption" HorizontalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Border>
<Border Grid.Column="1"
Classes="surface"
Padding="14"
IsVisible="{Binding IsConversationPaneExpanded}">
<Grid RowDefinitions="Auto,Auto,*,Auto" RowSpacing="12">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="10">
<StackPanel Spacing="3">
<TextBlock Text="받은함" Classes="section-title" />
<TextBlock Text="{Binding CurrentUserDisplayName}" Classes="caption" />
</StackPanel>
<Border Grid.Column="1" Classes="status-chip" VerticalAlignment="Center">
<TextBlock Text="{Binding StatusSummaryText}" Classes="caption" />
</Border>
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
<TextBox Grid.Column="0"
Classes="search-input"
PlaceholderText="{Binding SearchWatermark}"
Text="{Binding ConversationSearchText}" />
<Button Grid.Column="1"
Classes="icon-button"
ToolTip.Tip="밀도 전환"
Command="{Binding ToggleCompactModeCommand}"
Content="{Binding DensityGlyph}" />
<Button Grid.Column="2"
Classes="icon-button"
ToolTip.Tip="새로고침"
Command="{Binding ReloadCommand}"
Content="↻" />
</Grid>
<StackPanel Grid.Row="2" Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Classes="filter-button"
Classes.selected="{Binding IsAllFilterSelected}"
Command="{Binding ShowAllConversationsCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="전체" />
<TextBlock Text="{Binding TotalConversationCount}" />
</StackPanel>
</Button>
<Button Classes="filter-button"
Classes.selected="{Binding IsUnreadFilterSelected}"
Command="{Binding ShowUnreadConversationsCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="안읽음" />
<TextBlock Text="{Binding UnreadConversationCount}" />
</StackPanel>
</Button>
<Button Classes="filter-button"
Classes.selected="{Binding IsPinnedFilterSelected}"
Command="{Binding ShowPinnedConversationsCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="고정" />
<TextBlock Text="{Binding PinnedConversationCount}" />
</StackPanel>
</Button>
</StackPanel>
<Grid>
<Border Classes="empty-card"
IsVisible="{Binding ShowConversationEmptyState}">
<StackPanel Spacing="10">
<TextBlock Text="{Binding ConversationEmptyStateText}" Classes="section-title" />
<TextBlock Text="필터를 바꾸거나 새로고침으로 다시 맞춰 보세요." Classes="caption" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="secondary-button"
Command="{Binding ShowAllConversationsCommand}"
Content="전체 보기" />
<Button Classes="secondary-button"
Command="{Binding ReloadCommand}"
Content="다시 확인" />
</StackPanel>
</StackPanel>
</Border>
<ScrollViewer IsVisible="{Binding HasFilteredConversations}">
<ItemsControl ItemsSource="{Binding FilteredConversations}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ConversationRowViewModel">
<Grid Margin="0,0,0,6" ColumnDefinitions="*,Auto" ColumnSpacing="6">
<Button Classes="row-button"
Command="{Binding $parent[Window].DataContext.SelectConversationCommand}"
CommandParameter="{Binding .}">
<Border Classes="row-card"
Classes.active="{Binding IsSelected}"
Padding="{Binding $parent[Window].DataContext.ConversationRowPadding}">
<Grid ColumnDefinitions="34,*,Auto" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="2">
<Border Width="34"
Height="34"
Classes="avatar-badge">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding AvatarText}"
FontWeight="SemiBold"
Foreground="#111418" />
</Border>
<TextBlock Grid.Column="1"
Text="{Binding Title}"
Classes="body"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2"
Text="{Binding MetaText}"
Classes="caption" />
<TextBlock Grid.Row="1"
Grid.Column="1"
Text="{Binding LastMessageText}"
Classes="caption"
TextTrimming="CharacterEllipsis" />
<StackPanel Grid.Row="1"
Grid.Column="2"
Orientation="Horizontal"
Spacing="6"
HorizontalAlignment="Right">
<TextBlock Text="★"
Classes="caption"
IsVisible="{Binding IsPinned}" />
<Border Classes="unread-badge"
IsVisible="{Binding HasUnread}">
<TextBlock Text="{Binding UnreadBadgeText}"
FontSize="11"
Foreground="#FFFFFF" />
</Border>
</StackPanel>
</Grid>
</Border>
</Button>
<Button Grid.Column="1"
Classes="icon-button compact"
ToolTip.Tip="분리 창으로 열기"
Command="{Binding $parent[Window].DataContext.DetachConversationRowCommand}"
CommandParameter="{Binding .}"
Content="↗" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</StackPanel>
<Grid Grid.Row="3" ColumnDefinitions="Auto,Auto" ColumnSpacing="8">
<Border Classes="status-chip">
<TextBlock Text="{Binding RealtimeStatusText}" Classes="caption" />
</Border>
<Border Grid.Column="1" Classes="status-chip">
<TextBlock Text="{Binding WorkspaceModeText}" Classes="caption" />
</Border>
</Grid>
</Grid>
</Border>
<GridSplitter Grid.Column="2"
Width="6"
IsVisible="{Binding IsConversationPaneExpanded}"
Background="#E4E7EB"
ResizeDirection="Columns"
ShowsPreview="True" />
<Border Grid.Column="3" Classes="surface" Padding="14">
<Grid RowDefinitions="Auto,Auto,*,Auto" RowSpacing="12">
<Grid ColumnDefinitions="*,Auto,Auto,Auto" ColumnSpacing="8">
<Grid Grid.Column="0" ColumnDefinitions="Auto,*" ColumnSpacing="10">
<Border Width="38" Height="38" Classes="surface-muted">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding SelectedConversationGlyph}"
FontWeight="SemiBold"
Foreground="#111418" />
</Border>
<StackPanel Grid.Column="1" Spacing="2">
<TextBlock Text="{Binding SelectedConversationTitle}"
Classes="section-title"
TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding SelectedConversationSubtitle}"
Classes="caption"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</Grid>
<Border Grid.Column="1" Classes="status-chip" VerticalAlignment="Center">
<TextBlock Text="{Binding RealtimeStatusText}" Classes="caption" />
</Border>
<Button Grid.Column="2"
Classes="icon-button"
ToolTip.Tip="분리 창으로 열기"
Command="{Binding DetachConversationCommand}"
Content="↗" />
<Button Grid.Column="3"
Classes="icon-button"
ToolTip.Tip="새로고침"
Command="{Binding ReloadCommand}"
Content="↻" />
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,*,*" ColumnSpacing="8">
<Border Grid.Column="0" Classes="dashboard-card">
<StackPanel Spacing="3">
<TextBlock Text="안읽음" Classes="eyebrow" />
<TextBlock Text="{Binding UnreadConversationCount}" Classes="section-title" />
<TextBlock Text="지금 바로 볼 대화" Classes="caption" />
</StackPanel>
</Border>
<Border Grid.Column="1" Classes="dashboard-card">
<StackPanel Spacing="3">
<TextBlock Text="고정" Classes="eyebrow" />
<TextBlock Text="{Binding PinnedConversationCount}" Classes="section-title" />
<TextBlock Text="자주 여는 대화" Classes="caption" />
</StackPanel>
</Border>
<Border Grid.Column="2" Classes="dashboard-card">
<StackPanel Spacing="3">
<TextBlock Text="창 상태" Classes="eyebrow" />
<TextBlock Text="{Binding WorkspaceModeText}" Classes="section-title" />
<TextBlock Text="분리 창을 바로 열 수 있습니다." Classes="caption" />
</StackPanel>
</Border>
</Grid>
<Grid Grid.Row="2" RowDefinitions="Auto,*" RowSpacing="10">
<Border Classes="inline-alert"
IsVisible="{Binding HasErrorText}">
<TextBlock Text="{Binding ErrorText}"
Classes="caption"
Foreground="#C9392C"
TextWrapping="Wrap" />
</Border>
<Grid Grid.Row="1">
<ScrollViewer Name="MessagesScrollViewer">
<ItemsControl ItemsSource="{Binding Messages}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:MessageRowViewModel">
<Border Classes="message-bubble"
Classes.mine="{Binding IsMine}"
Classes.pending="{Binding IsPending}"
Classes.failed="{Binding IsFailed}"
Padding="{Binding $parent[Window].DataContext.MessageBubblePadding}">
<StackPanel Spacing="5">
<TextBlock Text="{Binding SenderName}"
Classes="caption"
FontWeight="SemiBold"
IsVisible="{Binding ShowSenderName}" />
<TextBlock Text="{Binding Text}" Classes="message-text" />
<TextBlock Text="{Binding MetaText}" Classes="message-meta" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Border Classes="empty-card"
Width="340"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsVisible="{Binding ShowMessageEmptyState}">
<StackPanel Spacing="10">
<TextBlock Text="{Binding MessageEmptyStateTitle}" Classes="section-title" />
<TextBlock Text="{Binding MessageEmptyStateText}" Classes="caption" TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="secondary-button"
Command="{Binding DetachConversationCommand}"
Content="분리 창" />
<Button Classes="secondary-button"
Command="{Binding ReloadCommand}"
Content="새로고침" />
</StackPanel>
</StackPanel>
</Border>
</Grid>
</Grid>
<Border Grid.Row="3" Classes="surface-muted" Padding="10">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto" ColumnSpacing="10" RowSpacing="8">
<TextBox Name="ComposerTextBox"
Grid.RowSpan="2"
Classes="input"
PlaceholderText="{Binding ComposerPlaceholderText}"
AcceptsReturn="True"
TextWrapping="Wrap"
MinHeight="{Binding ComposerMinHeight}"
Text="{Binding ComposerText}"
KeyDown="ComposerTextBox_OnKeyDown" />
<Button Grid.Column="1"
Classes="primary-button"
VerticalAlignment="Stretch"
Command="{Binding SendMessageCommand}"
Content="{Binding ComposerActionText}" />
<Border Grid.Row="1" Grid.Column="1" Classes="status-chip" HorizontalAlignment="Right">
<TextBlock Text="{Binding ComposerCounterText}" Classes="caption" />
</Border>
</Grid>
</Border>
</Grid>
</Border>
</Grid>
</Grid>
</Window>

View file

@ -0,0 +1,87 @@
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;
using PhysOn.Desktop.ViewModels;
namespace PhysOn.Desktop.Views;
public partial class MainWindow : Window
{
private MainWindowViewModel? _boundViewModel;
public MainWindow()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}
private async void ComposerTextBox_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && !e.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
if (DataContext is MainWindowViewModel viewModel)
{
await viewModel.SendMessageFromShortcutAsync();
e.Handled = true;
}
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Key == Key.O &&
e.KeyModifiers.HasFlag(KeyModifiers.Control) &&
e.KeyModifiers.HasFlag(KeyModifiers.Shift) &&
DataContext is MainWindowViewModel viewModel)
{
_ = viewModel.OpenDetachedConversationFromShortcutAsync();
e.Handled = true;
}
}
private void OnDataContextChanged(object? sender, EventArgs e)
{
if (_boundViewModel is not null)
{
_boundViewModel.Messages.CollectionChanged -= Messages_OnCollectionChanged;
}
_boundViewModel = DataContext as MainWindowViewModel;
if (_boundViewModel is not null)
{
_boundViewModel.Messages.CollectionChanged += Messages_OnCollectionChanged;
}
}
private void Messages_OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action is NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Reset or NotifyCollectionChangedAction.Replace)
{
Dispatcher.UIThread.Post(ScrollMessagesToEnd, DispatcherPriority.Background);
}
}
protected override void OnClosed(EventArgs e)
{
if (_boundViewModel is not null)
{
_boundViewModel.Messages.CollectionChanged -= Messages_OnCollectionChanged;
_ = _boundViewModel.DisposeAsync();
}
base.OnClosed(e);
}
private void ScrollMessagesToEnd()
{
if (this.FindControl<ScrollViewer>("MessagesScrollViewer") is { } scrollViewer)
{
scrollViewer.Offset = new Vector(scrollViewer.Offset.X, scrollViewer.Extent.Height);
}
}
}