This commit is contained in:
mmrbnjd
2025-08-12 22:38:27 +03:30
parent 85043ecb9e
commit 7a28196cde

View File

@@ -289,11 +289,67 @@
<!-- B2: Message input -->
<div class="message-input-container" id="B2">
<div class="input-wrapper">
<input type="text" @bind-value="MsgInput" class="message-input" placeholder="پیام خود را بنویسید..." />
<Button Color="ButtonColor.Primary" Size=ButtonSize.Small @onclick="OnClickSendMsg" Class="send-btn">
<input type="text" @bind-value="MsgInput" class="message-input" placeholder="پیام خود را بنویسید..." />
<InputFile id="chatImageInput" style="display:none" OnChange="OnImageSelected" accept="image/*" />
<Button Color="ButtonColor.Secondary" Size=ButtonSize.Small Outline="true" @onclick="OpenFileDialog" Class="attach-btn" title="افزودن تصویر">
<Icon Name="IconName.Image" />
</Button>
<!-- Audio Recording Button -->
<Button Color="@(IsRecording ? ButtonColor.Danger : ButtonColor.Secondary)"
Size=ButtonSize.Small
Outline="true"
@onclick="ToggleAudioRecording"
class=@($"audio-btn {(IsRecording ? "recording" : "")}")
title="@(IsRecording ? "توقف ضبط" : "ضبط صدا")">
@if (IsRecording)
{
<Icon Name="IconName.StopCircle" Class="recording-pulse" />
}
else
{
<Icon Name="IconName.Mic" />
}
</Button>
<Button Color="ButtonColor.Primary" Size=ButtonSize.Small @onclick="OnClickSendMsg" Class="send-btn" title="ارسال">
<Icon Name="IconName.Send" />
</Button>
</div>
<!-- Image Preview -->
@if (SelectedImagePreview != null)
{
<div class="mt-2 d-flex align-items-center gap-2">
<img src="@SelectedImagePreview" alt="preview" style="max-height:60px;border-radius:8px;border:1px solid #e9ecef;" />
<Button Color="ButtonColor.Secondary" Size=ButtonSize.ExtraSmall Outline="true" @onclick="ClearSelectedImage">حذف تصویر</Button>
</div>
}
<!-- Audio Preview -->
@if (RecordedAudioBytes != null)
{
<div class="mt-2 d-flex align-items-center gap-2 audio-preview">
<div class="audio-preview-controls">
<audio controls style="max-width: 250px;">
<source src="@RecordedAudioUrl" type="audio/wav">
</audio>
<div class="audio-preview-info">
<small class="text-muted">@RecordedAudioDuration</small>
</div>
</div>
<Button Color="ButtonColor.Danger" Size=ButtonSize.ExtraSmall Outline="true" @onclick="ClearRecordedAudio">حذف صدا</Button>
</div>
}
<!-- Recording Status -->
@if (IsRecording)
{
<div class="mt-2 recording-status">
<div class="recording-indicator">
<span class="recording-dot"></span>
<span class="recording-text">در حال ضبط... @RecordingTime</span>
</div>
</div>
}
</div>
}
@@ -318,6 +374,19 @@
/////////////
ChatItemDto? ChatCurrent { get; set; } = null;
string MsgInput { get; set; }
IBrowserFile? SelectedImageFile = null;
byte[]? SelectedImageBytes = null;
string? SelectedImagePreview = null;
// Audio recording properties
bool IsRecording = false;
string RecordingTime = "00:00";
byte[]? RecordedAudioBytes = null;
string? RecordedAudioUrl = null;
string RecordedAudioDuration = "00:00";
private System.Threading.Timer? recordingTimer;
private DateTime recordingStartTime;
bool chatloading = false;
string SelectedChatUserName = "مهدی ربیع نژاد";
private bool _shouldObserveVisibility = false;
@@ -548,9 +617,145 @@
var seconds = durationSeconds % 60;
return $"{minutes:D2}:{seconds:D2}";
}
// Audio recording methods
private async Task ToggleAudioRecording()
{
if (IsRecording)
{
await StopAudioRecording();
}
else
{
await StartAudioRecording();
}
}
private async Task StartAudioRecording()
{
try
{
var result = await JS.InvokeAsync<bool>("startAudioRecording");
if (result)
{
IsRecording = true;
recordingStartTime = DateTime.Now;
recordingTimer = new System.Threading.Timer(UpdateRecordingTime, null, 0, 1000);
StateHasChanged();
}
else
{
toastService.Notify(new ToastMessage(ToastType.Warning, "خطا در شروع ضبط صدا"));
}
}
catch (Exception ex)
{
toastService.Notify(new ToastMessage(ToastType.Danger, $"خطا در ضبط صدا: {ex.Message}"));
}
}
private async Task StopAudioRecording()
{
try
{
var audioData = await JS.InvokeAsync<string>("stopAudioRecording");
if (!string.IsNullOrEmpty(audioData))
{
// Convert base64 to bytes
var base64Data = audioData.Split(',')[1];
RecordedAudioBytes = Convert.FromBase64String(base64Data);
RecordedAudioUrl = audioData;
RecordedAudioDuration = RecordingTime;
IsRecording = false;
recordingTimer?.Dispose();
recordingTimer = null;
await ClearSelectedImage();
StateHasChanged();
}
}
catch (Exception ex)
{
toastService.Notify(new ToastMessage(ToastType.Danger, $"خطا در توقف ضبط صدا: {ex.Message}"));
}
finally
{
IsRecording = false;
recordingTimer?.Dispose();
recordingTimer = null;
StateHasChanged();
}
}
private void UpdateRecordingTime(object? state)
{
var elapsed = DateTime.Now - recordingStartTime;
RecordingTime = $"{elapsed.Minutes:D2}:{elapsed.Seconds:D2}";
InvokeAsync(StateHasChanged);
}
private Task ClearRecordedAudio()
{
RecordedAudioBytes = null;
RecordedAudioUrl = null;
RecordedAudioDuration = "00:00";
return Task.CompletedTask;
}
private async Task OpenFileDialog()
{
await JS.InvokeVoidAsync("triggerClick", "chatImageInput");
}
private async Task OnImageSelected(InputFileChangeEventArgs e)
{
var file = e.File;
if (file is null)
{
SelectedImageFile = null;
SelectedImageBytes = null;
SelectedImagePreview = null;
return;
}
SelectedImageFile = file;
using var memoryStream = new MemoryStream();
await file.OpenReadStream().CopyToAsync(memoryStream);
SelectedImageBytes = memoryStream.ToArray();
SelectedImagePreview = $"data:{file.ContentType};base64,{Convert.ToBase64String(SelectedImageBytes)}";
await ClearRecordedAudio();
}
private Task ClearSelectedImage()
{
SelectedImageFile = null;
SelectedImageBytes = null;
SelectedImagePreview = null;
return Task.CompletedTask;
}
}
<style>
.attach-btn {
border-radius: 50%;
width: 38px;
height: 38px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 1px solid #e9ecef;
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.2);
transition: all 0.3s ease;
color: #495057;
}
.attach-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 16px rgba(108, 117, 125, 0.3);
background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
}
.attach-btn:active {
transform: translateY(0) scale(0.95);
box-shadow: 0 2px 8px rgba(108, 117, 125, 0.2);
}
/* Enhanced Sidebar Styling */
.sidebar-container {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
@@ -1441,9 +1646,273 @@
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
color: white;
}
/* Audio recording button styling */
.audio-btn {
border-radius: 50%;
width: 38px;
height: 38px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 1px solid #e9ecef;
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.2);
transition: all 0.3s ease;
color: #495057;
position: relative;
overflow: hidden;
}
.audio-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 16px rgba(108, 117, 125, 0.3);
background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
}
.audio-btn:active {
transform: translateY(0) scale(0.95);
box-shadow: 0 2px 8px rgba(108, 117, 125, 0.2);
}
.audio-btn.recording {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
border-color: #dc3545;
color: white;
animation: recordingPulse 1.5s ease-in-out infinite;
}
.audio-btn.recording:hover {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%);
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 16px rgba(220, 53, 69, 0.4);
}
.recording-pulse {
animation: iconPulse 1s ease-in-out infinite;
}
@@keyframes recordingPulse {
0%, 100% {
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
50% {
box-shadow: 0 4px 20px rgba(220, 53, 69, 0.6), 0 0 30px rgba(220, 53, 69, 0.3);
}
}
@@keyframes iconPulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* Audio preview styling */
.audio-preview {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 0.75rem;
margin-top: 0.5rem;
}
.audio-preview-controls {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.audio-preview-controls audio {
border-radius: 8px;
background: #f8f9fa;
}
.audio-preview-info {
text-align: center;
color: #6c757d;
font-size: 0.875rem;
}
/* Recording status styling */
.recording-status {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border: 1px solid #ffc107;
border-radius: 12px;
padding: 0.75rem;
margin-top: 0.5rem;
animation: recordingStatusPulse 2s ease-in-out infinite;
}
@@keyframes recordingStatusPulse {
0%, 100% {
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.2);
}
50% {
box-shadow: 0 4px 16px rgba(255, 193, 7, 0.4);
}
}
.recording-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.recording-dot {
width: 12px;
height: 12px;
background: #dc3545;
border-radius: 50%;
animation: recordingDotPulse 1s ease-in-out infinite;
}
@@keyframes recordingDotPulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
.recording-text {
color: #856404;
font-weight: 600;
font-size: 0.875rem;
}
/* Audio message styling in chat */
.audio-message {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
}
.audio-message audio {
border-radius: 8px;
background: #f8f9fa;
border: 1px solid #e9ecef;
}
.audio-info {
text-align: center;
color: #6c757d;
font-size: 0.75rem;
}
/* Responsive design for audio elements */
@@media (max-width: 768px) {
.audio-preview-controls audio {
max-width: 200px;
}
.audio-message audio {
max-width: 200px;
}
}
@@media (max-width: 480px) {
.audio-preview-controls audio {
max-width: 180px;
}
.audio-message audio {
max-width: 180px;
}
.recording-text {
font-size: 0.8rem;
}
}
</style>
<script>
// Trigger click on hidden input by id
window.triggerClick = (elementId) => {
const el = document.getElementById(elementId);
if (el) {
el.click();
}
};
// Audio recording variables
let mediaRecorder = null;
let audioChunks = [];
let audioStream = null;
window.startAudioRecording = async () => {
try {
// Request microphone access
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Create MediaRecorder
mediaRecorder = new MediaRecorder(audioStream, {
mimeType: 'audio/webm;codecs=opus'
});
audioChunks = [];
// Collect audio data
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
// Start recording
mediaRecorder.start();
return true;
} catch (error) {
console.error('Error starting audio recording:', error);
return false;
}
};
window.stopAudioRecording = () => {
return new Promise((resolve) => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.onstop = async () => {
try {
// Create audio blob
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
// Convert to base64
const reader = new FileReader();
reader.onloadend = () => {
// Convert webm to wav format (simplified)
const base64Data = reader.result;
resolve(base64Data);
};
reader.readAsDataURL(audioBlob);
// Stop all tracks
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
audioStream = null;
}
} catch (error) {
console.error('Error processing audio:', error);
resolve('');
}
};
mediaRecorder.stop();
} else {
resolve('');
}
});
};
window.getWindowSize = () => {
return {
width: window.innerWidth,