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 { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : FluentWindow { private readonly RedmineManager _manager; private readonly RedmineSettingsManager _settings; private readonly RedmineConfig _config; private List _issues = []; private readonly ILoggerFactory _loggerFactory; public ObservableCollection IssuesList { get; set; } = []; public ObservableCollection 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.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 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(); } } }