Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/api/versions/latest
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "0.26.2",
"releasedAt": "2025-08-31",
}
Binary file modified xyz.hotchpotch.hogandiff/messages.properties管理.xlsx
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public class AppResource {
}

/** プロパティファイルの相対パス */
private static Path APP_PROP_PATH = USER_HOME != null
public static Path APP_PROP_PATH = USER_HOME != null
? USER_HOME.resolve("hogandiff.properties")
: null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.awt.Color;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -316,6 +317,30 @@ private static <T> Function<String, T> decodeNotSupported(String msg) {
Boolean::valueOf,
true);

/** 起動時に新規バージョンの有無を確認するか */
public static final Key<Boolean> CHECK_UPDATES = new Key<>(
"application.checkUpdates",
() -> false,
String::valueOf,
Boolean::valueOf,
true);

/** 新バージョン有無の最終チェック日時 */
public static final Key<Instant> LAST_CHECK_UPDATES = new Key<>(
"application.lastCheckUpdates",
() -> null,
Instant::toString,
Instant::parse,
true);

/** 新バージョン有無チェックの最短間隔(時間) */
public static final Key<Integer> CHECK_UPDATES_INTERVAL_HOURS = new Key<>(
"application.checkUpdatesIntervalHours",
() -> 6,
String::valueOf,
Integer::valueOf,
false);

/** 全ての定義済み設定項目を含むセット */
// Collectors#toSet は現在の実装では immutable set を返すが
// 保証されないということなので、一応 Set#copyOf でラップしておく。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ public void initialize() {

// 4.値変更時のイベントハンドラの設定
// nop

// 5.その他
UpdateChecker.execute(false);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package xyz.hotchpotch.hogandiff.gui;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import xyz.hotchpotch.hogandiff.AppMain;
import xyz.hotchpotch.hogandiff.AppResource;
import xyz.hotchpotch.hogandiff.SettingKeys;
import xyz.hotchpotch.hogandiff.util.NetUtil;

/**
* このアプリケーションの更新チェック機能を提供します。<br>
*
* @author nmby
*/
public class UpdateChecker {

// [static members] ********************************************************

private static final AppResource ar = AppMain.appResource;
private static final ResourceBundle rb = ar.get();

/**
* 更新チェックを実行します。<br>
* {@code force} に {@code true} が指定されている場合は、強制的にチェックします。<br>
* {@code force} に {@code false} が指定されている場合は、
* ユーザーが更新チェックを無効にしている場合、過去数時間以内にチェックしている場合はスキップします。<br>
*
* @param force 強制的にチェックする場合は {@code true}
*/
public static void execute(boolean force) {
UpdateChecker checker = new UpdateChecker();
checker.checkUpdate(force);
}

// [instance members] ******************************************************

private UpdateChecker() {
}

private void checkUpdate(boolean force) {
if (!force) {
if (!ar.settings().get(SettingKeys.CHECK_UPDATES)) {
return;
}

Instant lastCheckAt = ar.settings().get(SettingKeys.LAST_CHECK_UPDATES);
int interval = ar.settings().get(SettingKeys.CHECK_UPDATES_INTERVAL_HOURS);
if (lastCheckAt != null && Instant.now().isBefore(lastCheckAt.plus(interval, ChronoUnit.HOURS))) {
return;
}
}

CompletableFuture
.supplyAsync(() -> NetUtil.getAsJson("https://nmby.github.io/hogandiff4/api/versions/latest"))
.thenAccept(json -> {
String latestVersion = json.getString("version");
if (!amILatest(latestVersion)) {
Platform.runLater(() -> {
Hyperlink link = UIUtil.createHyperlink(AppMain.WEB_URL);
VBox content = new VBox(10);
content.getChildren().addAll(
new Label(rb.getString("gui.UpdateChecker.020")
.formatted(AppMain.VERSION, latestVersion)),
link);
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(rb.getString("AppMain.010"));
alert.setHeaderText(rb.getString("gui.UpdateChecker.010"));
alert.getDialogPane().setContent(content);
alert.showAndWait();
});
} else if (force) {
Platform.runLater(() -> {
new Alert(
AlertType.INFORMATION,
rb.getString("gui.UpdateChecker.030").formatted(AppMain.VERSION),
ButtonType.OK)
.showAndWait();
});
}
ar.changeSetting(SettingKeys.LAST_CHECK_UPDATES, Instant.now());
})
.exceptionally(throwable -> {
throwable.printStackTrace();
return null;
});
}

private boolean amILatest(String latestVersion) {
Function<String, int[]> toVersionNumbers = (v) -> {
String[] parts = v.replace("v", "").split("\\.");
if (parts.length != 3) {
throw new IllegalArgumentException("Invalid version string: " + v);
}
int[] numbers = new int[parts.length];
for (int i = 0; i < parts.length; i++) {
numbers[i] = Integer.parseInt(parts[i]);
}
return numbers;
};

int[] latest = toVersionNumbers.apply(latestVersion);
int[] current = toVersionNumbers.apply(AppMain.VERSION);
for (int i = 0; i < latest.length; i++) {
if (latest[i] > current[i]) {
return false;
} else if (latest[i] < current[i]) {
return true;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,14 @@ public void init(MainController parent, Object... param) {
});

// 3.初期値の設定
new Thread(() -> {
Thread asyncInitGoogleTask = new Thread(() -> {
GoogleCredential credential = GoogleCredential.get(false);
Platform.runLater(() -> {
parent.googleCredential.setValue(credential);
});
}).start();
});
asyncInitGoogleTask.setDaemon(true);
asyncInitGoogleTask.start();

// 4.値変更時のイベントハンドラの設定
// nop
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,28 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.stream.Stream;

import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.control.Dialog;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.util.Callback;
import xyz.hotchpotch.hogandiff.AppMain;
import xyz.hotchpotch.hogandiff.AppResource;
import xyz.hotchpotch.hogandiff.SettingKeys;
import xyz.hotchpotch.hogandiff.gui.ChildController;
import xyz.hotchpotch.hogandiff.gui.MainController;
import xyz.hotchpotch.hogandiff.gui.dialogs.SettingDetailsDialogPane;
import xyz.hotchpotch.hogandiff.util.function.UnsafeConsumer;

/**
Expand All @@ -45,41 +38,6 @@ public class SettingsPane2 extends VBox implements ChildController {

// [static members] ********************************************************

private static enum LocaleItem {

// [static members] ----------------------------------------------------

/** 日本語 */
JA("日本語", Locale.JAPANESE, "jp.png"),

/** 英語 */
EN("English", Locale.ENGLISH, "us.png"),

/** 中国語(簡体字) */
ZH("簡体中文", Locale.SIMPLIFIED_CHINESE, "cn.png");

public static LocaleItem of(Locale locale) {
Objects.requireNonNull(locale);

return Stream.of(values())
.filter(item -> item.locale == locale)
.findFirst()
.orElseThrow();
}

// [instance members] --------------------------------------------------

private final String text;
private final Locale locale;
private final Image image;

LocaleItem(String text, Locale locale, String imageSrc) {
this.text = text;
this.locale = locale;
this.image = new Image(imageSrc);
}
}

// [instance members] ******************************************************

private final AppResource ar = AppMain.appResource;
Expand All @@ -88,9 +46,6 @@ public static LocaleItem of(Locale locale) {
@FXML
private GooglePane googlePane;

@FXML
private ComboBox<LocaleItem> localeComboBox;

@FXML
private Button openWorkDirButton;

Expand All @@ -100,6 +55,9 @@ public static LocaleItem of(Locale locale) {
@FXML
private Button deleteWorkDirButton;

@FXML
private Button detailsButton;

/**
* コンストラクタ<br>
*
Expand All @@ -121,30 +79,29 @@ public void init(MainController parent, Object... param) {

// 2.項目ごとの各種設定
googlePane.init(parent);
localeComboBox.setItems(FXCollections.observableArrayList(LocaleItem.values()));
localeComboBox.setButtonCell(cellFactory(false).call(null));
localeComboBox.setCellFactory(cellFactory(true));

openWorkDirButton.setOnAction(openDir);
changeWorkDirButton.setOnAction(changeDir);
deleteWorkDirButton.setOnAction(deleteDir);

localeComboBox.setOnAction(event -> {
if (ar.changeSetting(SettingKeys.APP_LOCALE, localeComboBox.getValue().locale)) {
new Alert(
AlertType.INFORMATION,
"%s%n%n%s%n%n%s".formatted(
rb.getString("gui.component.SettingsPane2.051"),
rb.getString("gui.component.SettingsPane2.052"),
rb.getString("gui.component.SettingsPane2.053")),
ButtonType.OK)
.showAndWait();
detailsButton.setOnAction(event -> {
try {
SettingDetailsDialogPane detailsContent = new SettingDetailsDialogPane();
detailsContent.init();
Dialog<Void> detailsDialog = new Dialog<>();
detailsDialog.setTitle(rb.getString("gui.component.SettingsPane2.060"));
detailsDialog.getDialogPane().setContent(detailsContent);
detailsDialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
detailsDialog.showAndWait();

} catch (IOException e) {
e.printStackTrace();
// nop
}
});

// 3.初期値の設定
Locale locale = ar.settings().get(SettingKeys.APP_LOCALE);
localeComboBox.setValue(LocaleItem.of(locale));
// nop

// 4.値変更時のイベントハンドラの設定
// nop
Expand Down Expand Up @@ -248,29 +205,4 @@ public void init(MainController parent, Object... param) {
});
}
};

private Callback<ListView<LocaleItem>, ListCell<LocaleItem>> cellFactory(boolean showText) {
return listView -> new ListCell<>() {
@Override
public void updateItem(LocaleItem item, boolean empty) {
super.updateItem(item, empty);

if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
ImageView iv = new ImageView(item.image);
iv.setFitHeight(17);
iv.setPreserveRatio(true);
setGraphic(iv);

if (showText) {
setText(item.text);
} else {
this.setAlignment(Pos.CENTER);
}
}
}
};
}
}
Loading