/
aleksandrazimut
/
TimerCountdownDesktop
Обзор
Документация
Войти
/
aleksandrazimut
/
TimerCountdownDesktop
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/main/java/com/timerapp/controller/MainController.java
1 320 строк
58 KB
Aleksandr Azimut
init commit
17 дек 2025, 21:59
17 дек 2025, 21:59
3c602f1
Код
Авторство
О чём код?
package com.timerapp.controller; import com.timerapp.model.Theme; import com.timerapp.model.ThemeColors; import com.timerapp.model.TimerConfig; import com.timerapp.service.AudioService; import com.timerapp.service.ConfigService; import com.timerapp.service.TimerService; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.layout.HBox; import javafx.stage.Modality; import javafx.stage.Stage; import java.io.File; import java.io.IOException; import java.time.Duration; public class MainController { @FXML private VBox rootPane; @FXML private Label timeLabel; @FXML private ImageView themeIcon; @FXML private Region processBar; @FXML private Region processBackground; @FXML private Label cancelButton; @FXML private HBox presetsBox; // Контейнер для динамических пресетов @FXML private Label themeSimpleLabel; @FXML private Label themeFalloutLabel; @FXML private Label themeLondonLabel; @FXML private Label themePokemonLabel; @FXML private Label themeVaderLabel; @FXML private Label themeR2D2Label; @FXML private HBox rateUsBox; @FXML private ImageView star1; @FXML private ImageView star2; @FXML private ImageView star3; @FXML private ImageView star4; @FXML private ImageView star5; @FXML private Label otherExtensionsBtn; private TimerService timerService; private AudioService audioService; private ConfigService configService; private TimerConfig currentConfig; private Theme currentTheme; // Картинки для звездочек private Image starRegular; private Image starSolid; private boolean ratingSet = false; // Флаг: был ли установлен рейтинг /** * Инициализация контроллера */ @FXML public void initialize() { System.out.println("[MainController] Инициализация..."); initializeStars(); // Инициализация звезд рейтинга // Инициализация сервисов this.timerService = new TimerService(); this.audioService = new AudioService(); this.configService = new ConfigService(); timerService.setOnTick(() -> updateTimeDisplay(Duration.ofSeconds(timerService.getRemainingSeconds()))); timerService.setOnComplete(this::showAlarmWindow); cancelButton.managedProperty().bind(cancelButton.visibleProperty()); cancelButton.setVisible(false); loadConfig(); setupPresets(); // Создает пресеты после загрузки конфига initializeStars(); // Инициализируем звездочки рейтинга } @FXML public void onAdd30s() { addTime(Duration.ofSeconds(30)); } @FXML public void onAdd1m() { addTime(Duration.ofMinutes(1)); } @FXML public void onAdd5m() { addTime(Duration.ofMinutes(5)); } @FXML public void onAdd10m() { addTime(Duration.ofMinutes(10)); } @FXML public void onAdd15m() { addTime(Duration.ofMinutes(15)); } @FXML public void onAdd30m() { addTime(Duration.ofMinutes(30)); } @FXML public void onAdd1h() { addTime(Duration.ofHours(1)); } private void addTime(Duration duration) { timerService.addTime((int) duration.toSeconds()); if (!timerService.isRunning()) { timerService.start(); } cancelButton.setVisible(true); playClickSound(); } private void updateTimeDisplay(Duration remaining) { Platform.runLater(() -> { long totalSeconds = remaining.getSeconds(); if (totalSeconds <= 0 && !timerService.isRunning()) { timeLabel.setText("--:--:--"); processBar.setMaxWidth(0); // Сброс ширины прогресс-бара return; } long hours = totalSeconds / 3600; long minutes = (totalSeconds % 3600) / 60; long seconds = totalSeconds % 60; String timeText = String.format("%02d:%02d:%02d", hours, minutes, seconds); timeLabel.setText(timeText); // Прогресс бар (ширина 326px с учетом border фона 2px) // ИНВЕРСИЯ: начинается справа (335px) и уменьшается до 0 double progress = 0; if (timerService.getTotalSeconds() > 0) { progress = (double) remaining.toMillis() / (timerService.getTotalSeconds() * 1000.0); } if (progress < 0) progress = 0; if (progress > 1) progress = 1; // Устанавливает ширину заполнителя через maxWidth (инвертировано) double width = 335 * progress; processBar.setMaxWidth(width); }); } @FXML private void onResetClick() { timerService.stop(); timerService.reset(); cancelButton.setVisible(false); updateTimeDisplay(Duration.ZERO); playClickSound(); } private void playClickSound() { if (audioService != null) { try { audioService.playSound("/sounds/general.wav"); } catch (Exception e) { e.printStackTrace(); } } } private void showAlarmView() { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/alarm-view.fxml")); Parent root = loader.load(); rootPane.set } catch (IOException e) { e.printStackTrace(); } } private void showAlarmWindow() { System.out.println("[MainController] showAlarmWindow вызван!"); Platform.runLater(() -> { try { System.out.println("[MainController] Загрузка alarm-view.fxml..."); FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/alarm-view.fxml")); Parent root = loader.load(); System.out.println("[MainController] FXML загружен успешно"); AlarmController ac = loader.getController(); // Применяет настройки к alarm окну (из конфига, так как он содержит актуальные // данные) if (currentConfig != null) { // Фон String bgPath = currentConfig.getBackgroundPath(); if (bgPath == null || bgPath.isEmpty()) { if (currentTheme != null) bgPath = currentTheme.getBackgroundPath(); } if (bgPath != null) ac.setBackgroundPath(bgPath); // Иконка String iconPath = currentConfig.getIconPath(); if (iconPath == null || iconPath.isEmpty()) { if (currentTheme != null) iconPath = currentTheme.getIconPath(); } if (iconPath != null) ac.setIconPath(iconPath); // Цвета (создаем объект цветов из конфига) String timerColor = currentConfig.getTimerTextColor() != null ? currentConfig.getTimerTextColor() : "#ffffff"; String progressColor = currentConfig.getProgressBarColor() != null ? currentConfig.getProgressBarColor() : "#4CAF50"; String btnColor = currentConfig.getButtonColor() != null ? currentConfig.getButtonColor() : "#ffffff"; String btnText = currentConfig.getButtonTextColor() != null ? currentConfig.getButtonTextColor() : "#000000"; com.timerapp.model.ThemeColors colors = new com.timerapp.model.ThemeColors( timerColor, // timerColor progressColor, // progressColor "white", // progressBgColor - для пользовательских настроек используем белый btnColor, // presetsBtnBg btnText, // presetsBtnText btnText, // presetsBtnHoverText progressColor, // accentColor btnText, // themeLinkColor btnText, // themeLinkHoverColor "transparent", // footerBg "#000000" // footerTextColor ); // Проверяет какая тема выбрана boolean isSimple = currentTheme != null && currentTheme.getName().equals("Simple"); boolean isFallout = currentTheme != null && currentTheme.getName().equals("Fallout"); boolean isLondon = currentTheme != null && currentTheme.getName().equals("London"); boolean isPokemon = currentTheme != null && currentTheme.getName().equals("Pokemon"); boolean isVader = currentTheme != null && currentTheme.getName().equals("Vader"); boolean isR2D2 = currentTheme != null && currentTheme.getName().equals("R2D2"); ac.applyThemeColors(colors, isSimple, isFallout, isLondon, isPokemon, isVader, isR2D2); // Применяет стили кнопок из конфига ac.applyButtonStyles(currentConfig); } else if (currentTheme != null) { // Fallback если конфига нет (маловероятно) ac.setBackgroundPath(currentTheme.getAlarmBackgroundPath()); ac.setIconPath(currentTheme.getIconPath()); boolean isSimple = currentTheme.getName().equals("Simple"); boolean isFallout = currentTheme.getName().equals("Fallout"); boolean isLondon = currentTheme.getName().equals("London"); boolean isPokemon = currentTheme.getName().equals("Pokemon"); boolean isVader = currentTheme.getName().equals("Vader"); boolean isR2D2 = currentTheme.getName().equals("R2D2"); ac.applyThemeColors(currentTheme.getColors(), isSimple, isFallout, isLondon, isPokemon, isVader, isR2D2); } ac.setAudioService(audioService); ac.setOnRestart(() -> { timerService.restart(); cancelButton.setVisible(true); }); ac.setOnClose(() -> { // Сбрасывает таймер и скрываем Cancel timerService.reset(); cancelButton.setVisible(false); // updateTimeDisplay покажет --:--:-- автоматически при следующем тике или через // вызов вручную updateTimeDisplay(Duration.ZERO); }); Stage stage = new Stage(); stage.initModality(Modality.APPLICATION_MODAL); // Создаем сцену без фиксированных размеров Scene scene = new Scene(root); stage.setScene(scene); // Устанавливает fullscreen для alarm окна stage.setFullScreen(true); stage.setFullScreenExitHint(""); // Убираем подсказку выхода из fullscreen if (themeIcon.getImage() != null) stage.getIcons().add(themeIcon.getImage()); if (currentConfig.isSoundEnabled()) { System.out.println("[MainController] Воспроизводим звук: " + currentConfig.getSoundPath()); audioService.playSound(currentConfig.getSoundPath()); // Запускает анимацию ПОСЛЕ того как начали воспроизводить звук ac.startAnimation(); } else { // Если звук отключен, запускаем fallback анимацию ac.startAnimation(); } stage.setOnCloseRequest(e -> audioService.stop()); System.out.println("[MainController] Показываем alarm окно..."); stage.show(); System.out.println("[MainController] Alarm окно отображено!"); } catch (Exception e) { System.err.println("[MainController] ОШИБКА при показе alarm окна:"); e.printStackTrace(); } }); } public void cleanup() { timerService.stop(); audioService.cleanup(); } private void loadConfig() { currentConfig = configService.loadConfig(); if (currentConfig == null) { currentConfig = new TimerConfig.Builder() .setThemeName("London") .setSoundPath("/sounds/bigban.mp3") .setSoundEnabled(true) .build(); } // Проверяет: если есть пользовательский фон - применяет пользовательские // настройки // Иначе применяет тему if (currentConfig.getBackgroundPath() != null && !currentConfig.getBackgroundPath().isEmpty()) { // Пользовательские настройки applyCustomSettings(currentConfig); } else if (currentConfig.getThemeName() != null && !currentConfig.getThemeName().isEmpty()) { // Тема по умолчанию switchTheme(currentConfig.getThemeName(), false); // Загрузка - сохраняем цвета } else { // Fallback на Simple тему switchTheme("Simple", false); // Fallback - сохраняем цвета } } private void switchTheme(String themeName, boolean clearCustomColors) { Theme theme = null; for (Theme t : Theme.getDefaultThemes()) { if (t.getName().equalsIgnoreCase(themeName)) { theme = t; break; } } if (theme != null) { // При выборе темы - ОЧИЩАЕМ пользовательский фон и настройки // НО сохраняет пресеты ифи были TimerConfig.Builder builder = new TimerConfig.Builder() .setThemeName(theme.getName()) .setSoundPath(theme.getSoundPath()) .setSoundEnabled(true) .setBackgroundPath("") // Очищает пользовательский фон .setIconPath(""); // Очищает пользовательскую иконку // Очищает цвета ТОЛЬКО при явном выборе темы if (clearCustomColors) { builder.setTimerTextColor(""); // Очищает пользовательский цвет таймера builder.setProgressBarColor(""); // Очищает пользовательский цвет прогресс-бара } else { // Сохраняет существующие цвета (если есть) if (currentConfig != null) { if (currentConfig.getTimerTextColor() != null) { builder.setTimerTextColor(currentConfig.getTimerTextColor()); } if (currentConfig.getProgressBarColor() != null) { builder.setProgressBarColor(currentConfig.getProgressBarColor()); } } } // DEBUG: Почему теряется градиент? System.out.println("[MainController] switchTheme: clearCustomColors=" + clearCustomColors); if (currentConfig != null) { System.out.println("[MainController] switchTheme: currentConfig.getButtonGradient()=" + currentConfig.getButtonGradient()); } else { System.out.println("[MainController] switchTheme: currentConfig is NULL"); } // Сохраняет пользовательские стили кнопок ТОЛЬКО если это не сброс темы if (!clearCustomColors && currentConfig != null) { if (currentConfig.getButtonGradient() != null) { builder.setButtonGradient(currentConfig.getButtonGradient()); System.out.println("[MainController] switchTheme: Перенос градиента в builder: " + currentConfig.getButtonGradient()); } else { System.out.println("[MainController] switchTheme: Градиент в конфиге NULL"); } if (currentConfig.getButtonColor() != null) { builder.setButtonColor(currentConfig.getButtonColor()); } if (currentConfig.getButtonTextColor() != null) { builder.setButtonTextColor(currentConfig.getButtonTextColor()); } builder.setButtonOpacity(currentConfig.getButtonOpacity()); } // Сохраняет пользовательские пресеты (они независимы от темы) if (currentConfig != null && currentConfig.getCustomPresets() != null && currentConfig.getCustomPresets().length > 0) { builder.setCustomPresets(currentConfig.getCustomPresets()); System.out.println("[MainController] Сохраняем пресеты при смене темы: " + String.join(", ", currentConfig.getCustomPresets())); } // ВАЖНО: сначала обновляем currentConfig, ПОТОМ применяем тему! currentConfig = builder.build(); configService.saveConfig(currentConfig); // Теперь применяет тему с УЖЕ обновленным currentConfig applyTheme(theme); playClickSound(); } } private void applyTheme(Theme theme) { this.currentTheme = theme; applyThemeStyles(theme); applyIcon(theme.getIconPath()); updateActiveTheme(theme.getName()); // Обновляет активную тему } /** * Обновляет выделение активной темы */ private void updateActiveTheme(String themeName) { // Убирает класс theme-link-active у всех ссылок themeSimpleLabel.getStyleClass().remove("theme-link-active"); themeFalloutLabel.getStyleClass().remove("theme-link-active"); themeLondonLabel.getStyleClass().remove("theme-link-active"); themePokemonLabel.getStyleClass().remove("theme-link-active"); themeVaderLabel.getStyleClass().remove("theme-link-active"); themeR2D2Label.getStyleClass().remove("theme-link-active"); // Добавляет класс к активной теме switch (themeName) { case "Simple": themeSimpleLabel.getStyleClass().add("theme-link-active"); break; case "Fallout": themeFalloutLabel.getStyleClass().add("theme-link-active"); break; case "London": themeLondonLabel.getStyleClass().add("theme-link-active"); break; case "Pokemon": themePokemonLabel.getStyleClass().add("theme-link-active"); break; case "Vader": themeVaderLabel.getStyleClass().add("theme-link-active"); break; case "R2D2": themeR2D2Label.getStyleClass().add("theme-link-active"); break; } } private void applyIcon(String iconPath) { if (iconPath != null) { try { java.net.URL url = getClass().getResource(iconPath); if (url != null) { themeIcon.setImage(new Image(url.toExternalForm())); } } catch (Exception e) { System.err.println("Error loading icon: " + iconPath); } } } /** * Применяет пользовательскую иконку (из файла или ресурса) */ private void applyCustomIcon(String iconPath) { if (iconPath != null && !iconPath.isEmpty()) { try { java.net.URL url = null; File file = new File(iconPath); if (file.exists()) { url = file.toURI().toURL(); System.out.println("[MainController] Загружаем пользовательскую иконку из файла: " + iconPath); } else { // Пробует как ресурс url = getClass().getResource(iconPath); if (url != null) { System.out.println("[MainController] Загружаем иконку из ресурса: " + iconPath); } } if (url != null) { themeIcon.setImage(new Image(url.toExternalForm())); System.out.println("[MainController] Пользовательская иконка установлена: " + url.toExternalForm()); } else { System.err.println("[MainController] Не удалось загрузить иконку: " + iconPath); } } catch (Exception e) { System.err.println("[MainController] Ошибка загрузки иконки: " + e.getMessage()); e.printStackTrace(); } } } /** * Применяет стили темы: фоновую картинку + CSS-переменные цветов */ private void applyThemeStyles(Theme theme) { // Управление классами тем для CSS (удаляем старые, добавляем новую) rootPane.getStyleClass().removeIf(style -> style.startsWith("theme-")); rootPane.getStyleClass().add("theme-" + theme.getName().toLowerCase()); ThemeColors colors = theme.getColors(); // Формирует стиль фона String bgStyle = ""; try { if (theme.getBackgroundPath() != null) { java.net.URL url = getClass().getResource(theme.getBackgroundPath()); if (url != null) { bgStyle = String.format( "-fx-background-image: url('%s'); -fx-background-size: cover; -fx-background-position: center;", url.toExternalForm()); } } } catch (Exception e) { e.printStackTrace(); } // Формирует полный стиль с переменными (включая футер) String style = String.format( "%s " + "-fx-theme-timer-text: %s; " + "-fx-theme-btn-bg: %s; " + "-fx-theme-btn-text: %s; " + "-fx-theme-btn-hover-text: %s; " + "-fx-theme-accent: %s; " + "-fx-theme-progress-bg: %s; " + "-fx-theme-link-text: %s; " + "-fx-theme-link-hover-text: %s; " + "-fx-theme-footer-bg: %s; " + "-fx-theme-footer-text: %s;", bgStyle, colors.getTimerColor(), colors.getPresetsBtnBg(), colors.getPresetsBtnText(), colors.getPresetsBtnHoverText(), colors.getAccentColor(), colors.getProgressBgColor(), colors.getThemeLinkColor(), colors.getThemeLinkHoverColor(), colors.getFooterBg(), colors.getFooterTextColor()); rootPane.setStyle(style); // ВАЖНО: Очищает inline-стили у кнопок пресетов, чтобы применились CSS-стили // темы if (presetsBox != null) { for (javafx.scene.Node node : presetsBox.getChildren()) { if (node instanceof javafx.scene.control.Label) { node.setStyle(""); // Сброс инлайн стилей } } } // Применяет пользов ательские цвета ПОВЕРХ темы (если заданы в конфиге) // Сначала сбрасывает стиль таймера, чтобы применился CSS из файла timeLabel.setStyle(""); if (currentConfig != null) { if (currentConfig.getTimerTextColor() != null && !currentConfig.getTimerTextColor().isEmpty()) { timeLabel.setStyle("-fx-text-fill: " + currentConfig.getTimerTextColor() + ";"); System.out.println("[MainController] Применен пользовательский цвет таймера: " + currentConfig.getTimerTextColor()); } else { timeLabel.setStyle(""); // Сброс на стиль из темы } if (currentConfig.getProgressBarColor() != null && !currentConfig.getProgressBarColor().isEmpty()) { // Убирает styleClass чтобы CSS не перекрывал inline стиль processBar.getStyleClass().remove("progress-bar-fill"); // Использует inline стиль для установки пользовательского цвета String customStyle = "-fx-background-color: " + currentConfig.getProgressBarColor() + "; -fx-opacity: 0.9; -fx-background-radius: 2; -fx-translate-x: 1;"; processBar.setStyle(customStyle); System.out.println("[MainController] Применен пользовательский цвет прогресс-бара: " + currentConfig.getProgressBarColor()); } else { // Возвращает styleClass для использования цвета из темы if (!processBar.getStyleClass().contains("progress-bar-fill")) { processBar.getStyleClass().add("progress-bar-fill"); } processBar.setStyle(""); // Сброс inline стилей } } // Применяет пользовательские стили кнопок (градиент, прозрачность) поверх темы if (currentConfig != null) { applyButtonStylesToPresets(currentConfig); } // Разблокирует стили футера, чтобы он использовал стили темы unlockFooterStyles(); } @FXML public void onThemeSimple() { switchTheme("Simple", true); // Клик - очищает цвета } @FXML public void onThemeFallout() { switchTheme("Fallout", true); } @FXML public void onThemeLondon() { switchTheme("London", true); } @FXML public void onThemePokemon() { switchTheme("Pokemon", true); } @FXML public void onThemeVader() { switchTheme("Vader", true); } @FXML public void onThemeR2D2() { switchTheme("R2D2", true); } @FXML public void onSettingsClick() { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/settings-view.fxml")); Parent root = loader.load(); SettingsController settingsController = loader.getController(); // Сначала устанавливает callback для сохранения settingsController.setOnConfigSaved(config -> { currentConfig = config; configService.saveConfig(config); // Применяет изменения: // Если есть пользовательский фон - применяет пользовательские настройки // Иначе если есть тема - применяет тему if (config.getBackgroundPath() != null && !config.getBackgroundPath().isEmpty()) { // Пользовательские настройки applyCustomSettings(config); } else if (config.getThemeName() != null && !config.getThemeName().isEmpty()) { // Тема switchTheme(config.getThemeName(), false); // Загрузка alarm - сохраняем цвета } else { // Применяет пользовательские настройки без фона applyCustomSettings(config); } // Обновляет пресеты после применения настроек setupPresets(); }); // ПОТОМ передает текущую конфигурацию (это вызовет loadConfigToUI) settingsController.setCurrentConfig(currentConfig); Stage stage = new Stage(); stage.initModality(Modality.APPLICATION_MODAL); stage.setTitle("Settings"); stage.setScene(new Scene(root)); stage.showAndWait(); // НЕ вызывает loadConfig() - callback уже применил настройки! // loadConfig(); // УДАЛЕНО - это перезагружало конфиг и сбрасывало изменения } catch (Exception e) { e.printStackTrace(); } } /** * Применяет пользовательские настройки (цвета, кнопки и т.д.) */ private void applyCustomSettings(TimerConfig config) { System.out.println("[MainController] applyCustomSettings вызван. Фон: " + config.getBackgroundPath()); // НЕ сбрасывает currentTheme полностью - может понадобиться для иконки // this.currentTheme = null; // УДАЛЕНО // 1. Применяет фон через setBackground (надежнее чем CSS) if (config.getBackgroundPath() != null && !config.getBackgroundPath().isEmpty()) { try { java.net.URL url = null; File file = new File(config.getBackgroundPath()); if (file.exists()) { url = file.toURI().toURL(); } else if (config.getBackgroundPath().startsWith("http") || config.getBackgroundPath().startsWith("file:")) { url = new java.net.URI(config.getBackgroundPath()).toURL(); } else { url = getClass().getResource(config.getBackgroundPath()); } if (url != null) { javafx.scene.image.Image bgImage = new javafx.scene.image.Image(url.toExternalForm()); javafx.scene.layout.BackgroundImage backgroundImage = new javafx.scene.layout.BackgroundImage( bgImage, javafx.scene.layout.BackgroundRepeat.NO_REPEAT, javafx.scene.layout.BackgroundRepeat.NO_REPEAT, javafx.scene.layout.BackgroundPosition.CENTER, new javafx.scene.layout.BackgroundSize( javafx.scene.layout.BackgroundSize.AUTO, javafx.scene.layout.BackgroundSize.AUTO, false, false, false, true)); rootPane.setBackground(new javafx.scene.layout.Background(backgroundImage)); System.out.println("[MainController] Пользовательский фон установлен через setBackground: " + url.toExternalForm()); } else { System.err.println("[MainController] Не удалось загрузить фон: " + config.getBackgroundPath()); } } catch (Exception e) { System.err.println("[MainController] Ошибка загрузки фона: " + e.getMessage()); e.printStackTrace(); } } else { // Если фона нет - очищаем rootPane.setBackground(null); } // 2. Определяет цвета String btnText = config.getButtonTextColor() != null ? config.getButtonTextColor() : "#122242"; String accent = config.getProgressBarColor() != null ? config.getProgressBarColor() : "#0066FF"; String varsStyle = String.format( "-fx-theme-timer-text: %s; " + "-fx-theme-accent: %s; " + "-fx-theme-btn-bg: %s; " + "-fx-theme-btn-text: %s; " + "-fx-theme-btn-hover-text: %s; " + "-fx-theme-link-text: %s; " + "-fx-theme-link-hover-text: %s; " + "-fx-theme-footer-bg: rgba(255,255,255,0.5); " + "-fx-theme-footer-text: #122242;", config.getTimerTextColor() != null ? config.getTimerTextColor() : "#0066FF", accent, config.getButtonColor() != null ? config.getButtonColor() : "#ffffff", btnText, btnText, btnText, accent); // 3. Применяет CSS стили (без фона - только цвета) rootPane.setStyle(varsStyle); System.out.println("[MainController] applyCustomSettings завершено"); // 4. Применяет пользовательские цвета напрямую к элементам if (config.getTimerTextColor() != null && !config.getTimerTextColor().isEmpty()) { timeLabel.setStyle("-fx-text-fill: " + config.getTimerTextColor() + ";"); System.out.println("[MainController] Применен цвет таймера: " + config.getTimerTextColor()); } if (config.getProgressBarColor() != null && !config.getProgressBarColor().isEmpty()) { // Убирает styleClass чтобы CSS не перекрывал inline стиль processBar.getStyleClass().remove("progress-bar-fill"); // Использует inline стиль вместо setBackground для приоритета над CSS классом processBar.setStyle("-fx-background-color: " + config.getProgressBarColor() + "; -fx-opacity: 0.9; -fx-background-radius: 0; -fx-translate-x: 0;"); System.out.println("[MainController] Применен цвет прогресс-бара: " + config.getProgressBarColor()); } // Применяет градиент и прозрачность к кнопкам пресетов applyButtonStylesToPresets(config); // Применяет иконку: если пользователь указал свою - использует её // Иначе оставляет иконку от последней темы if (config.getIconPath() != null && !config.getIconPath().isEmpty()) { applyCustomIcon(config.getIconPath()); System.out.println("[MainController] Применена пользовательская иконка: " + config.getIconPath()); } else if (currentTheme != null) { // Оставляет иконку от темы applyIcon(currentTheme.getIconPath()); System.out.println("[MainController] Оставлена иконка от темы: " + currentTheme.getName()); } // Фиксирует стили футера в пользовательском режиме lockFooterStyles(); } /** * Настраивает кнопки пресетов (дефолтные или пользовательские) */ private void setupPresets() { if (presetsBox == null) { System.err.println("[MainController] presetsBox is null"); return; } // Очищает старые кнопки presetsBox.getChildren().clear(); // Проверяет наличие пользовательских пресетов String[] customPresets = (currentConfig != null) ? currentConfig.getCustomPresets() : null; if (customPresets != null && customPresets.length > 0) { // Создает пользовательские пресеты System.out .println("[MainController] Создаем пользовательские пресеты: " + String.join(", ", customPresets)); for (String preset : customPresets) { createPresetButton(preset); } } else { // Создает дефолтные пресеты System.out.println("[MainController] Создаем дефолтные пресеты"); createDefaultPresetButton("+30s", 30, false); createDefaultPresetButton("+1m", 1, true); createDefaultPresetButton("+5m", 5, true); createDefaultPresetButton("+10m", 10, true); createDefaultPresetButton("+15m", 15, true); createDefaultPresetButton("+30m", 30, true); createDefaultPresetButton("+1h", 60, true); } // ВАЖНО: Применяет пользовательские стили ПОСЛЕ создания кнопок! if (currentConfig != null) { applyButtonStylesToPresets(currentConfig); } } /** * Создает кнопку пользовательского пресета * Поддерживает форматы: "30с", "5м", "10" (по умолчанию минуты) */ private void createPresetButton(String preset) { Label btn = new Label(); btn.getStyleClass().add("time-btn"); btn.setCursor(javafx.scene.Cursor.HAND); // Парсит значение int seconds = parsePresetToSeconds(preset.trim()); // Форматирует текст кнопки String buttonText = "+" + preset.trim(); btn.setText(buttonText); // Обработчик клика btn.setOnMouseClicked(e -> { addTime(Duration.ofSeconds(seconds)); }); presetsBox.getChildren().add(btn); } /** * Создает дефолтную кнопку пресета */ private void createDefaultPresetButton(String text, int value, boolean isMinutes) { Label btn = new Label(); btn.setText(text); btn.getStyleClass().add("time-btn"); btn.setCursor(javafx.scene.Cursor.HAND); int seconds = isMinutes ? value * 60 : value; btn.setOnMouseClicked(e -> { addTime(Duration.ofSeconds(seconds)); }); presetsBox.getChildren().add(btn); } /** * Парсит пресет в секунды * Поддерживает: "30с", "5м", "10" (по умолчанию минуты), "1h" */ private int parsePresetToSeconds(String preset) { preset = preset.toLowerCase().trim(); try { // Секунды: 30с, 30сек, 30s if (preset.endsWith("с") || preset.endsWith("сек") || preset.endsWith("s") || preset.endsWith("sec")) { String num = preset.replaceAll("[^0-9]", ""); return Integer.parseInt(num); } // Минуты: 5м, 5мин, 5m, 5min else if (preset.endsWith("м") || preset.endsWith("мин") || preset.endsWith("m") || preset.endsWith("min")) { String num = preset.replaceAll("[^0-9]", ""); return Integer.parseInt(num) * 60; } // Часы: 1h, 1ч else if (preset.endsWith("h") || preset.endsWith("ч")) { String num = preset.replaceAll("[^0-9]", ""); return Integer.parseInt(num) * 3600; } // По умолчанию - минуты else { return Integer.parseInt(preset) * 60; } } catch (NumberFormatException e) { System.err.println("[MainController] Ошибка парсинга пресета: " + preset); return 60; // Дефолт 1 минута } } /** * Применяет пользовательские стили к кнопкам (градиент, прозрачность) */ private void applyButtonStylesToPresets(TimerConfig config) { if (presetsBox == null || config == null) return; String gradient = config.getButtonGradient(); String color = config.getButtonColor(); double opacity = config.getButtonOpacity(); String textColor = config.getButtonTextColor(); System.out.println("[MainController] Применение стилей кнопок: gradient=" + gradient + ", color=" + color + ", opacity=" + opacity + ", textColor=" + textColor); // Проверка: есть ли пользовательские стили? boolean hasCustomStyle = (gradient != null && !gradient.isEmpty() && !gradient.equalsIgnoreCase("none") && !gradient.equalsIgnoreCase("нет")); // Если цвета отличаются от дефолтных if (!hasCustomStyle && color != null && !color.isEmpty() && !color.equals("#4a5568")) { hasCustomStyle = true; } if (!hasCustomStyle) { System.out.println("[MainController] Сброс стилей кнопок (возврат к теме)"); for (var node : presetsBox.getChildren()) { if (node instanceof Label) { Label btn = (Label) node; btn.setStyle(""); // Сброс inline стилей btn.setOnMouseEntered(null); // Удаляет слушатели btn.setOnMouseExited(null); } } return; } // Формирует inline стиль StringBuilder styleBuilder = new StringBuilder(); // Фон: градиент или цвет if (gradient != null && !gradient.isEmpty() && !gradient.equalsIgnoreCase("none") && !gradient.equalsIgnoreCase("нет")) { // Конвертирует в CSS gradient String cssGradient = convertGradientToCss(gradient); styleBuilder.append("-fx-background-color: ").append(cssGradient).append("; "); } else if (color != null && !color.isEmpty()) { styleBuilder.append("-fx-background-color: ").append(color).append("; "); } // Добавляет radius чтобы переопределить CSS класс styleBuilder.append("-fx-background-radius: 5; "); // Цвет текста if (textColor != null && !textColor.isEmpty()) { styleBuilder.append("-fx-text-fill: ").append(textColor).append("; "); } // Прозрачность styleBuilder.append("-fx-opacity: ").append(opacity).append("; "); // Добавляет остальные свойства из .time-btn styleBuilder.append("-fx-font-family: 'Verdana'; "); styleBuilder.append("-fx-font-size: 11px; "); styleBuilder.append("-fx-padding: 2 3 2 3; "); styleBuilder.append("-fx-cursor: hand;"); String finalStyle = styleBuilder.toString(); // Стиль при наведении (full opacity) String hoverStyle = finalStyle.replaceAll("-fx-opacity: [0-9.]+", "-fx-opacity: 1.0"); System.out.println("[MainController] Итоговый стиль кнопок: " + finalStyle); // Применяет ко всем кнопкам пресетов for (var node : presetsBox.getChildren()) { if (node instanceof Label) { Label btn = (Label) node; btn.setStyle(finalStyle); btn.setOnMouseEntered(e -> btn.setStyle(hoverStyle)); btn.setOnMouseExited(e -> btn.setStyle(finalStyle)); } } } /** * Фиксирует стили футера (Rate Us, Other extensions), чтобы они не менялись */ private void lockFooterStyles() { // Фиксирует стиль для rateUsBox if (rateUsBox != null) { String rateBoxStyle = "-fx-text-fill: #122242; " + "-fx-font-family: 'Verdana'; " + "-fx-font-size: 11.5px; " + "-fx-cursor: default; " + "-fx-background-color: rgba(255,255,255,0.5); " + "-fx-background-radius: 7; " + "-fx-padding: 5 10 5 10; " + "-fx-min-width: 145; " + "-fx-max-width: 145; " + "-fx-alignment: CENTER_LEFT; " + "-fx-opacity: 0.6;"; rateUsBox.setStyle(rateBoxStyle); // Добавляет hover эффект rateUsBox.setOnMouseEntered( e -> rateUsBox.setStyle(rateBoxStyle.replace("-fx-opacity: 0.6", "-fx-opacity: 1.0"))); rateUsBox.setOnMouseExited(e -> rateUsBox.setStyle(rateBoxStyle)); } // Фиксирует стиль для otherExtensionsBtn if (otherExtensionsBtn != null) { String btnStyle = "-fx-text-fill: #122242; " + "-fx-font-family: 'Verdana'; " + "-fx-font-size: 11.9px; " + "-fx-cursor: hand; " + "-fx-background-color: rgba(255,255,255,0.5); " + "-fx-background-radius: 7; " + "-fx-padding: 5 10 5 10; " + "-fx-min-width: 120; " + "-fx-max-width: 130; " + "-fx-alignment: CENTER_RIGHT; " + "-fx-opacity: 0.6;"; otherExtensionsBtn.setStyle(btnStyle); // Добавляет hover эффект otherExtensionsBtn.setOnMouseEntered( e -> otherExtensionsBtn.setStyle(btnStyle.replace("-fx-opacity: 0.6", "-fx-opacity: 1.0"))); otherExtensionsBtn.setOnMouseExited(e -> otherExtensionsBtn.setStyle(btnStyle)); } System.out.println("[MainController] Стили футера зафиксированы"); } /** * Снимает фиксацию стилей футера, возвращая использование CSS переменных */ private void unlockFooterStyles() { // Сбрасывает inline стили для rateUsBox if (rateUsBox != null) { rateUsBox.setStyle(""); rateUsBox.setOnMouseEntered(null); rateUsBox.setOnMouseExited(null); } // Сбрасывает inline стили для otherExtensionsBtn if (otherExtensionsBtn != null) { otherExtensionsBtn.setStyle(""); otherExtensionsBtn.setOnMouseEntered(null); otherExtensionsBtn.setOnMouseExited(null); } System.out.println("[MainController] Стили футера разблокированы (используют тему)"); } /** * Конвертирует внутреннее название градиента в CSS linear-gradient */ private String convertGradientToCss(String gradient) { // Маппинг градиентов из SettingsController switch (gradient) { case "h_light_blue": return "linear-gradient(to bottom, #e0f7fa, #b2ebf2)"; case "h_green": return "linear-gradient(to bottom, #c8e6c9, #a5d6a7)"; case "h_purple": return "linear-gradient(to bottom, #e1bee7, #ce93d8)"; case "h_pink": return "linear-gradient(to bottom, #f8bbd0, #f48fb1)"; case "h_orange": return "linear-gradient(to bottom, #ffe0b2, #ffcc80)"; case "h_beige": return "linear-gradient(to bottom, #d7ccc8, #bcaaa4)"; case "v_blue_indigo": return "linear-gradient(to right, #1976d2, #283593)"; case "v_green_teal": return "linear-gradient(to right, #388e3c, #00796b)"; case "v_red_pink": return "linear-gradient(to right, #d32f2f, #c2185b)"; case "v_purple_deep": return "linear-gradient(to right, #7b1fa2, #4a148c)"; default: // Если неизвестный - возвращаем серый return "#4a5568"; } } /** * Инициализирует звездочки для рейтинга */ private void initializeStars() { try { // Загружает картинки звездочек (GREEN по запросу) starRegular = new Image(getClass().getResourceAsStream("/img/star_regular_green.png")); starSolid = new Image(getClass().getResourceAsStream("/img/star_solid_green.png")); // Устанавливает пустые звездочки по умолчанию star1.setImage(starRegular); star2.setImage(starRegular); star3.setImage(starRegular); star4.setImage(starRegular); star5.setImage(starRegular); // Делает звездочки кликабельными star1.setStyle("-fx-cursor: hand;"); star2.setStyle("-fx-cursor: hand;"); star3.setStyle("-fx-cursor: hand;"); star4.setStyle("-fx-cursor: hand;"); star5.setStyle("-fx-cursor: hand;"); // Загружает сохраненный рейтинг (если был) loadSavedRating(); } catch (Exception e) { System.err.println("[MainController] Ошибка инициализации звездочек: " + e.getMessage()); e.printStackTrace(); } } /** * Загружает сохраненный рейтинг */ private void loadSavedRating() { if (currentConfig != null) { int rating = currentConfig.getUserRating(); if (rating > 0) { updateStars(rating); } } } /** * Обработчик наведения мыши на звездочку (превью) */ @FXML public void onStarHover(javafx.scene.input.MouseEvent event) { // Если рейтинг уже установлен - не реагируем на hover if (ratingSet) { return; } ImageView star = (ImageView) event.getSource(); if (star.getUserData() != null) { int rank = Integer.parseInt(star.getUserData().toString()); previewStars(rank); } } /** * Обработчик клика по звездочке */ @FXML public void onStarClick(javafx.scene.input.MouseEvent event) { ImageView star = (ImageView) event.getSource(); if (star.getUserData() != null) { int rank = Integer.parseInt(star.getUserData().toString()); // 1. Сохраняет рейтинг (последнее значение) if (currentConfig != null) { TimerConfig.Builder builder = new TimerConfig.Builder() .setThemeName(currentConfig.getThemeName()) .setSoundPath(currentConfig.getSoundPath()) .setSoundEnabled(currentConfig.isSoundEnabled()) .setBackgroundPath(currentConfig.getBackgroundPath()) .setIconPath(currentConfig.getIconPath()) .setUserRating(rank); // <-- Сохраняем рейтинг // Копирует все остальные настройки if (currentConfig.getTimerTextColor() != null) { builder.setTimerTextColor(currentConfig.getTimerTextColor()); } if (currentConfig.getProgressBarColor() != null) { builder.setProgressBarColor(currentConfig.getProgressBarColor()); } if (currentConfig.getButtonColor() != null) { builder.setButtonColor(currentConfig.getButtonColor()); } if (currentConfig.getButtonTextColor() != null) { builder.setButtonTextColor(currentConfig.getButtonTextColor()); } if (currentConfig.getButtonGradient() != null) { builder.setButtonGradient(currentConfig.getButtonGradient()); } builder.setButtonOpacity(currentConfig.getButtonOpacity()); if (currentConfig.getCustomPresets() != null) { builder.setCustomPresets(currentConfig.getCustomPresets()); } currentConfig = builder.build(); configService.saveConfig(currentConfig); } updateStars(rank); playClickSound(); // Отключает интерактивность после первого клика ratingSet = true; disableStarInteraction(); // Простой GET запрос new Thread(() -> { try { java.net.URL url = java.net.URI.create("https://www.example.com").toURL(); java.net.HttpURLConnection con = (java.net.HttpURLConnection) url.openConnection(); con.setRequestMethod("GET"); con.setConnectTimeout(3000); con.setReadTimeout(3000); int status = con.getResponseCode(); System.out.println("[MainController] GET request sent. Status: " + status); con.disconnect(); } catch (Exception e) { System.err.println("[MainController] GET request failed: " + e.getMessage()); } }).start(); System.out.println("[MainController] Рейтинг установлен и сохранен: " + rank); } } /** * Обработчик выхода мыши из области рейтинга */ @FXML public void onRateMouseExit() { // Если рейтинг НЕ был установлен - сбрасывает все звезды в пустые if (!ratingSet) { setStarsRating(0); // Все звезды пустые } else { // Возвращает сохраненный рейтинг loadSavedRating(); } } /** * Показывает ПРЕВЬЮ - подсвечивает звезды ВКЛЮЧИТЕЛЬНО (чтобы было видно что * выберется) */ private void previewStars(int rank) { if (starRegular != null && starSolid != null) { star1.setImage(rank >= 1 ? starSolid : starRegular); star2.setImage(rank >= 2 ? starSolid : starRegular); star3.setImage(rank >= 3 ? starSolid : starRegular); star4.setImage(rank >= 4 ? starSolid : starRegular); star5.setImage(rank >= 5 ? starSolid : starRegular); } } /** * Устанавливает рейтинг - подсвечивает звезды ВКЛЮЧИТЕЛЬНО */ private void setStarsRating(int rank) { if (starRegular != null && starSolid != null) { star1.setImage(rank >= 1 ? starSolid : starRegular); star2.setImage(rank >= 2 ? starSolid : starRegular); star3.setImage(rank >= 3 ? starSolid : starRegular); star4.setImage(rank >= 4 ? starSolid : starRegular); star5.setImage(rank >= 5 ? starSolid : starRegular); } } /** * Обновляет отображение звездочек (используется после клика) */ private void updateStars(int rating) { setStarsRating(rating); } /** * Отключает интерактивность звездочек после установки рейтинга */ private void disableStarInteraction() { star1.setStyle("-fx-cursor: default;"); star2.setStyle("-fx-cursor: default;"); star3.setStyle("-fx-cursor: default;"); star4.setStyle("-fx-cursor: default;"); star5.setStyle("-fx-cursor: default;"); } }