Files
Blueberry/Blueberry/MainWindow.xaml.cs

574 lines
21 KiB
C#

using Blueberry;
using Blueberry.Redmine;
using Blueberry.Redmine.Dto;
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Input;
using Wpf.Ui.Controls;
namespace BlueMine
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : FluentWindow
{
private readonly RedmineManager _manager;
private readonly RedmineSettingsManager _settings;
private readonly RedmineConfig _config;
private List<IssueList.Issue> _issues = [];
private readonly ILoggerFactory _loggerFactory;
public ObservableCollection<IssueList.Issue> IssuesList { get; set; } = [];
public ObservableCollection<StatusList.IssueStatus> StatusList { get; set; } = [];
public MainWindow(RedmineManager manager, RedmineSettingsManager settings, RedmineConfig config, ILoggerFactory loggerFactory)
{
_settings = settings;
_config = config;
_manager = manager;
InitializeComponent();
DataContext = this;
_loggerFactory = loggerFactory;
}
private async void WindowLoaded(object sender, RoutedEventArgs e)
{
apiUrlTextBox.Text = _config.RedmineUrl;
apiPasswordBox.PlaceholderText = new string('●', _config.ApiKey.Length);
mainCalendar.SelectedDate = DateTime.Today;
versionTextBlock.Text = UpdateManager.CurrentVersion;
if(await TestConnection())
{
Task loadIssuesTask = LoadIssues();
Task getHoursTask = GetHours();
await Task.WhenAll(loadIssuesTask, getHoursTask);
#if !DEBUG
if(await UpdateManager.IsUpdateAvailable())
{
updateButton.Visibility = Visibility.Visible;
UpdateManager.DownloadCompleted += UpdateManager_DownloadCompleted;
await UpdateManager.DownloadUpdateAsync();
}
#endif
}
}
private async void UpdateManager_DownloadCompleted()
{
await Dispatcher.Invoke(async () =>
{
var result = await new Wpf.Ui.Controls.MessageBox
{
Title = "Frissítés elérhető",
Content = "Szeretnél most frissíteni?",
PrimaryButtonText = "Frissítés",
SecondaryButtonText = "Később",
IsCloseButtonEnabled = false,
}.ShowDialogAsync();
if (result == Wpf.Ui.Controls.MessageBoxResult.Primary)
await UpdateManager.PerformUpdate(true);
});
}
private void CalendarButtonClicked(object sender, RoutedEventArgs e)
{
flyoutCalendar.IsOpen = true;
}
private IProgress<(int, int)> UpdateProgress(string message)
{
var progress = new Progress<(int current, int total)>();
progress.ProgressChanged += (s, args) =>
{
progressRing.Visibility = Visibility.Visible;
int current = args.current;
int total = args.total;
statusTextBlock.Text = $"{message}: {current} / {total}";
progressBar.Value = (double)current / total * 100;
};
return progress;
}
private void ApiButtonClicked(object sender, RoutedEventArgs e)
{
apiFlyout.IsOpen = true;
}
private void CalendarSelectedDatesChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if(mainCalendar.SelectedDates.Count == 1)
calendarButton.Content = mainCalendar.SelectedDate?.ToString("yyyy-MM-dd");
else if(mainCalendar.SelectedDates.Count > 1)
calendarButton.Content = $"{mainCalendar.SelectedDates.Count} nap kiválasztva";
else
calendarButton.Content = "Válassz egy napot";
}
private void apiLinkButton_Click(object sender, RoutedEventArgs e)
{
string url = $"{apiUrlTextBox.Text}/my/account";
var psi = new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
};
Process.Start(psi);
}
private async void apiSaveButton_Click(object sender, RoutedEventArgs e)
{
_config.RedmineUrl = apiUrlTextBox.Text;
_config.ApiKey = apiPasswordBox.Password;
apiFlyout.IsOpen = false;
if(await TestConnection())
{
_settings.Save(_config);
statusTextBlock.Text = "Beállítások mentve és kapcsolódva";
await LoadIssues();
await GetHours();
}
else
{
_settings.Save(_config);
statusTextBlock.Text = "Beállítások mentve, de a Redmine nem elérhető";
}
}
private void SearchTextBoxTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
FilterIssues();
}
private async void RefreshButtonClick(object sender, RoutedEventArgs e)
{
Task loadIssuesTask = LoadIssues();
Task getHoursTask = GetHours();
await Task.WhenAll(loadIssuesTask, getHoursTask);
}
private void BrowserButtonClick(object sender, RoutedEventArgs e)
{
var issueNum = IssueNumberTextBox.Text;
if (int.TryParse(issueNum, out var issueId))
{
string url = $"{_config.RedmineUrl}/issues/{issueId}";
var psi = new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
};
Process.Start(psi);
}
}
private async void CloseButtonClick(object sender, RoutedEventArgs e)
{
StatusList.Clear();
var s = await _manager.GetStatusesAsync();
foreach (var status in s)
StatusList.Add(status);
statusFlyout.IsOpen = true;
}
private async void FixButtonClick(object sender, RoutedEventArgs e)
{
var progress = UpdateProgress("Idők javítása:");
progressRing.Visibility = Visibility.Visible;
var i = 0;
foreach (var date in mainCalendar.SelectedDates)
{
var hours = 8 - await _manager.GetCurrentUserTimeAsync(date, date);
if (hours <= 0)
continue;
var message = Constants.GenericMessages[Random.Shared.Next(Constants.GenericMessages.Length)];
var id = 801;
await _manager.LogTimeAsync(id, hours, message, date);
progress.Report((i, mainCalendar.SelectedDates.Count));
i++;
}
progressBar.Value = 0;
progressRing.Visibility = Visibility.Hidden;
await GetHours();
statusTextBlock.Text = "Idők javítva";
}
private async void sendButton_Click(object sender, RoutedEventArgs e)
{
if(mainCalendar.SelectedDates.Count == 0)
{
await new Wpf.Ui.Controls.MessageBox
{
Title = "Nap hiba",
Content = "Nincs kijelölve nap."
}.ShowDialogAsync();
return;
}
if(int.TryParse(IssueNumberTextBox.Text, out var issueId)
&& double.TryParse(HoursTextBox.Text, out var hours))
{
if (hours * 4 != Math.Floor(hours * 4))
{
await new Wpf.Ui.Controls.MessageBox
{
Title = "Idő formátum hiba",
Content = "Az idő csak negyedórákra bontható le."
}.ShowDialogAsync();
return;
}
var total = mainCalendar.SelectedDates.Count;
var progress = UpdateProgress("Idők beküldése:");
progressRing.Visibility = Visibility.Visible;
for (int i = 0; i < total; i++)
{
await _manager.LogTimeAsync(issueId, hours, MessageTextBox.Text, mainCalendar.SelectedDates[i]);
progress.Report((i + 1, total));
}
await GetHours();
progressBar.Value = 0;
progressRing.Visibility = Visibility.Hidden;
statusTextBlock.Text = "Idők beküldve";
} else
{
await new Wpf.Ui.Controls.MessageBox
{
Title = "Szám formátum hiba",
Content = "Az idő/jegyszám nem rendes szám."
}.ShowDialogAsync();
return;
}
}
private void ListView_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
var lv = sender as ListView;
if(lv != null && lv.SelectedItem is IssueList.Issue item)
{
IssueNumberTextBox.Text = item.Id.ToString();
}
}
private async void statusSaveButton_Click(object sender, RoutedEventArgs e)
{
var issueNum = IssueNumberTextBox.Text;
if (int.TryParse(issueNum, out var issueId))
{
try
{
var status = statusComboBox.SelectedItem as StatusList.IssueStatus;
if (status == null)
{
await new Wpf.Ui.Controls.MessageBox
{
Title = "Érvénytelen státusz",
Content = "Státusz kiválasztása sikertelen."
}.ShowDialogAsync();
return;
}
await _manager.SetIssueStatusAsync(issueId, status.Id);
var oldIssue = IssuesList.First(x=>x.Id == issueId);
var newIssue = await _manager.GetSimpleIssueAsync(issueId);
var index = IssuesList.IndexOf(oldIssue);
IssuesList.Insert(index, newIssue);
IssuesList.Remove(oldIssue);
await new Wpf.Ui.Controls.MessageBox
{
Title = "Sikeres művelet",
Content = $"A(z) {issueId} számú jegy új státusza: {status.Name}.",
}.ShowDialogAsync();
}
catch (Exception)
{
await new Wpf.Ui.Controls.MessageBox
{
Title = "Hiba",
Content = $"A(z) {issueId} számú jegy módosítása sikertelen.",
}.ShowDialogAsync();
}
}
else
{
await new Wpf.Ui.Controls.MessageBox
{
Title = "Hiba",
Content = "Érvénytelen jegyszám.",
}.ShowDialogAsync();
}
}
private async void searchTextBox_KeyUp(object sender, KeyEventArgs e)
{
if(e.Key == Key.Enter && searchTextBox.Text.Length > 0)
{
if (int.TryParse(searchTextBox.Text, out var issueId))
{
try
{
statusTextBlock.Text = "Jegy keresése...";
progressRing.Visibility = Visibility.Visible;
var issue = await _manager.GetSimpleIssueAsync(issueId);
IssuesList.Clear();
IssuesList.Add(issue);
statusTextBlock.Text = "Jegy betöltve";
progressRing.Visibility = Visibility.Hidden;
} catch (Exception) {
statusTextBlock.Text = "Jegy nem található";
progressRing.Visibility = Visibility.Hidden;
}
}
}
}
private async void openTicketButton_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement button && button.DataContext is IssueList.Issue item)
{
// 2. Access the property directly from your model
var issueId = item.Id;
try
{
statusTextBlock.Text = "Jegy betöltése...";
progressRing.Visibility = Visibility.Visible;
var issue = await _manager.GetIssueAsync(issueId);
statusTextBlock.Text = "Jegy betöltve";
progressRing.Visibility = Visibility.Hidden;
var issueWindow = new IssueWindow(issue, _manager, _config);
issueWindow.Show();
} catch (Exception)
{
statusTextBlock.Text = "Jegy betöltés sikertelen";
progressRing.Visibility = Visibility.Hidden;
}
}
}
private async void trackerButton_Click(object sender, RoutedEventArgs e)
{
if (int.TryParse(IssueNumberTextBox.Text, out var issueId))
await OpenTimeTracker(issueId);
}
private async void hoursButton_Click(object sender, RoutedEventArgs e)
{
var hoursWindow = new HoursWindow(_manager, _config, _loggerFactory.CreateLogger<HoursWindow>());
hoursWindow.Show();
}
private void updateButton_Click(object sender, RoutedEventArgs e)
{
UpdateManager_DownloadCompleted();
}
}
public partial class MainWindow : FluentWindow
{
private static readonly Regex _regexFractions = new Regex(@"^[0-9]*(?:\.[0-9]*)?$");
private static readonly Regex _regexNumbers = new Regex(@"^[0-9]*$");
private void FracValidation(object sender, TextCompositionEventArgs e)
{
var textBox = sender as TextBox;
// Construct what the text WILL be if we allow this input
string fullText = HoursTextBox.Text.Insert(HoursTextBox.CaretIndex, e.Text);
// If the resulting text is not a match, block the input
e.Handled = !_regexFractions.IsMatch(fullText);
}
private void FracPasting(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(string)))
{
string text = (string)e.DataObject.GetData(typeof(string));
var textBox = sender as TextBox;
string fullText = HoursTextBox.Text.Insert(HoursTextBox.CaretIndex, text);
if (!_regexFractions.IsMatch(fullText))
e.CancelCommand();
}
else
e.CancelCommand();
}
private void NumValidation(object sender, TextCompositionEventArgs e)
{
var textBox = sender as TextBox;
// Construct what the text WILL be if we allow this input
string fullText = HoursTextBox.Text.Insert(HoursTextBox.CaretIndex, e.Text);
// If the resulting text is not a match, block the input
e.Handled = !_regexNumbers.IsMatch(fullText);
}
private void NumPasting(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(string)))
{
string text = (string)e.DataObject.GetData(typeof(string));
var textBox = sender as TextBox;
string fullText = HoursTextBox.Text.Insert(HoursTextBox.CaretIndex, text);
if (!_regexNumbers.IsMatch(fullText))
e.CancelCommand();
}
else
e.CancelCommand();
}
public async Task GetHours()
{
todayProgressRing.Visibility =
yesterdayProgressRing.Visibility =
monthProgressRing.Visibility = Visibility.Visible;
var today = await _manager.GetCurrentUserTimeTodayAsync();
var yesterday = await _manager.GetCurrentUserTimeYesterdayAsync();
var thisMonth = await _manager.GetCurrentUserTimeThisMonthAsync();
int workingDays = 0;
DateTime currentDate = DateTime.Today;
for (int day = 1; day <= currentDate.Day; day++)
{
var dateToCheck = new DateTime(currentDate.Year, currentDate.Month, day);
if (dateToCheck.DayOfWeek != DayOfWeek.Saturday &&
dateToCheck.DayOfWeek != DayOfWeek.Sunday)
{
workingDays++;
}
}
var avgHours = Math.Round(thisMonth/workingDays, 2);
todayTimeLabel.Text = today.ToString();
yesterdayTimeLabel.Text = yesterday.ToString();
monthTimeLabel.Text = thisMonth.ToString();
averageTimeLabel.Text = avgHours.ToString();
todayProgressRing.Visibility =
yesterdayProgressRing.Visibility =
monthProgressRing.Visibility = Visibility.Hidden;
}
public void FilterIssues()
{
var list = string.IsNullOrWhiteSpace(searchTextBox.Text)
? _issues
: _issues.Where(issue => issue.Subject.Contains(searchTextBox.Text, StringComparison.OrdinalIgnoreCase)
|| issue.Id.ToString().Contains(searchTextBox.Text)
|| issue.ProjectName.Contains(searchTextBox.Text, StringComparison.OrdinalIgnoreCase));
IssuesList.Clear();
foreach (var item in list)
{
IssuesList.Add(item);
}
}
public async Task LoadIssues()
{
_issues.Clear();
statusTextBlock.Text = "Jegyek letöltése...";
progressRing.Visibility = Visibility.Visible;
foreach (var issueId in Constants.StaticTickets)
_issues.Add(await _manager.GetSimpleIssueAsync(issueId));
_issues.AddRange(await _manager.GetCurrentUserOpenIssuesAsync(progress: UpdateProgress("Jegyek letöltése:")));
progressBar.Value = 0;
progressRing.Visibility = Visibility.Hidden;
FilterIssues();
statusTextBlock.Text = "Jegyek letöltve";
}
public void DisableUi()
{
searchTextBox.IsEnabled =
//issuesDataGrid.IsEnabled =
IssueNumberTextBox.IsEnabled =
HoursTextBox.IsEnabled =
MessageTextBox.IsEnabled =
calendarButton.IsEnabled =
sendButton.IsEnabled =
closeButton.IsEnabled =
browserButton.IsEnabled =
newButton.IsEnabled =
refreshButton.IsEnabled =
fixButton.IsEnabled = false;
}
public void EnableUi()
{
searchTextBox.IsEnabled =
//issuesDataGrid.IsEnabled =
IssueNumberTextBox.IsEnabled =
HoursTextBox.IsEnabled =
MessageTextBox.IsEnabled =
calendarButton.IsEnabled =
sendButton.IsEnabled =
closeButton.IsEnabled =
browserButton.IsEnabled =
newButton.IsEnabled =
refreshButton.IsEnabled =
fixButton.IsEnabled = true;
}
public async Task<bool> TestConnection()
{
statusTextBlock.Text = $"Kapcsolódás Redminehoz...";
int maxRetries = 3;
int timeoutSeconds = 3; // Force kill after 5s
for (int i = 0; i < maxRetries; i++)
{
try
{
// Creates a token that cancels automatically
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
// Pass the token. If it hangs, this throws OperationCanceledException
if (await _manager.IsRedmineAvailable(cts.Token))
{
EnableUi();
apiButton.Appearance = ControlAppearance.Secondary;
statusTextBlock.Text = "Kapcsolódva";
return true;
}
}
catch (Exception)
{
// Ignore timeout/error and try again unless it's the last attempt
if (i == maxRetries - 1) break;
}
statusTextBlock.Text = $"Kapcsolódási hiba. Újrapróbálkozás: {i + 1}/{maxRetries}";
// Wait 1 second before retrying
await Task.Delay(5000);
}
// All attempts failed
DisableUi();
apiButton.Appearance = ControlAppearance.Primary;
return false;
}
public async Task OpenTimeTracker(int issueId)
{
var i = await _manager.GetSimpleIssueAsync(issueId);
var timer = new TimeTrackerWindow(_config, _manager, i);
timer.Show();
}
}
}