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
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

## [Unreleased]

## [0.7.2] - 2026-04-11

### Changed

- ホーム設定 overlay と `config.toml` に `startup_update_check` を追加し、起動時の update check をユーザー設定で ON/OFF できるようにしました。`eitango version` による手動確認は従来どおり使えます。

## [0.7.1] - 2026-04-11

### Added
Expand Down Expand Up @@ -194,7 +200,9 @@
- 通知不要時に古い update tag が画面に残る問題を修正しました。
- `dev` など非 semver の build でも update availability を正しく判定するようにしました。

[Unreleased]: https://github.com/harumiWeb/eitango/compare/v0.7.0...HEAD
[Unreleased]: https://github.com/harumiWeb/eitango/compare/v0.7.2...HEAD
[0.7.2]: https://github.com/harumiWeb/eitango/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/harumiWeb/eitango/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/harumiWeb/eitango/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/harumiWeb/eitango/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/harumiWeb/eitango/compare/v0.5.2...v0.6.0
Expand Down
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ Regular study sessions are local-first and use the local SQLite database. Networ

- update checks are optional helper behavior, not a requirement for starting or continuing a study session
- the request is only used to fetch lightweight release metadata such as the latest version and release URL
- the home-screen notice revalidates the latest release asynchronously on every launch
- the home-screen notice revalidates the latest release asynchronously on every launch by default, and you can toggle it via the settings screen or `startup_update_check` in `config.toml`
- the first successful check seeds the cache without showing a notice
- `update-check.json` stores the most recent successful result and is used as a fallback when the request times out or fails
- later launches show a lightweight home-screen notice when a newer version is available
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ write_quit = ["esc"]

- 更新チェックは補助機能であり、学習開始や回答処理の必須要件ではありません
- 取得するのは主に最新 release の version / URL などの更新案内に必要な情報です
- ホーム画面の通知は起動ごとに非同期で latest release を再確認します
- ホーム画面の通知は既定では起動ごとに非同期で latest release を再確認し、ホーム設定または `config.toml` の `startup_update_check` で ON/OFF できます
- 初回の成功確認では通知せず、次回以降の起動で差分があればホーム画面に軽く表示します
- `update-check.json` には直前の successful check 結果を保存し、タイムアウトやオフライン時の fallback に使います
- `eitango version` は現在の build info に加えて latest release URL も表示します
Expand Down
1 change: 1 addition & 0 deletions assets/locale/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ questions = "Default questions"
write_difficulty = "Write difficulty"
write_difficulty_basic = "basic"
write_difficulty_hard = "hard"
update_check = "Startup update check"
audio_enabled = "Audio"
audio_voice = "Local voice"
audio_voice_auto = "auto"
Expand Down
1 change: 1 addition & 0 deletions assets/locale/ja.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ questions = "既定の問題数"
write_difficulty = "Write難易度"
write_difficulty_basic = "basic"
write_difficulty_hard = "hard"
update_check = "起動時更新チェック"
audio_enabled = "音声"
audio_voice = "ローカル音声"
audio_voice_auto = "自動"
Expand Down
4 changes: 2 additions & 2 deletions internal/app/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ func loadStatsCmd(st *store.Store) tea.Cmd {
}
}

func updateCheckCmd(service updatecheck.Service, currentVersion string) tea.Cmd {
if service == nil {
func updateCheckCmd(service updatecheck.Service, currentVersion string, enabled bool) tea.Cmd {
if service == nil || !enabled {
return nil
}
return func() tea.Msg {
Expand Down
14 changes: 13 additions & 1 deletion internal/app/cmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ func TestUpdateCheckCmdUsesCheckNowAndReturnsResultEvenWhenServiceErrors(t *test
checkNowErr: errors.New("timeout"),
}

msg := updateCheckCmd(service, "v1.1.0")()
msg := updateCheckCmd(service, "v1.1.0", true)()
checked, ok := msg.(updateCheckedMsg)
if !ok {
t.Fatalf("updateCheckCmd() returned %T, want updateCheckedMsg", msg)
Expand All @@ -632,6 +632,18 @@ func TestUpdateCheckCmdUsesCheckNowAndReturnsResultEvenWhenServiceErrors(t *test
}
}

func TestUpdateCheckCmdReturnsNilWhenDisabled(t *testing.T) {
t.Parallel()

service := &stubUpdateService{}
if cmd := updateCheckCmd(service, "v1.1.0", false); cmd != nil {
t.Fatalf("updateCheckCmd() = %v, want nil", cmd)
}
if service.checkCalls != 0 || service.checkNowCalls != 0 {
t.Fatalf("service calls = check:%d checkNow:%d, want 0/0", service.checkCalls, service.checkNowCalls)
}
}

func newTestStore(t *testing.T) *store.Store {
t.Helper()

Expand Down
7 changes: 6 additions & 1 deletion internal/app/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
const (
settingsRowQuestionCount = iota
settingsRowWriteDifficulty
settingsRowUpdateCheck
settingsRowAudioEnabled
settingsRowAudioVoice
settingsRowAudioAutoplay
Expand Down Expand Up @@ -189,6 +190,7 @@ type RootModel struct {
settingsInput string
settingsEditing bool
settingsWriteDifficulty string
settingsUpdateCheckEnabled bool
settingsAudioEnabled bool
settingsAudioVoice string
settingsAudioAutoplay bool
Expand Down Expand Up @@ -260,7 +262,7 @@ func NewModel(store *store.Store, options Options) RootModel {

func (m RootModel) Init() tea.Cmd {
cmds := make([]tea.Cmd, 0, 2)
if cmd := updateCheckCmd(m.updateService, m.currentVersion); cmd != nil {
if cmd := updateCheckCmd(m.updateService, m.currentVersion, m.settings.UpdateCheckEnabled); cmd != nil {
cmds = append(cmds, cmd)
}
if m.startup != nil {
Expand Down Expand Up @@ -316,6 +318,7 @@ func (m RootModel) prepareSettingsOverlay() RootModel {
m.settingsInput = strconv.Itoa(m.settings.SessionSize)
m.settingsEditing = false
m.settingsWriteDifficulty = config.NormalizeWriteModeDifficulty(m.settings.WriteModeDifficulty)
m.settingsUpdateCheckEnabled = m.settings.UpdateCheckEnabled
m.settingsAudioEnabled = m.settings.AudioEnabled
m.settingsAudioVoices = nil
m.settingsAudioVoicesLoaded = false
Expand Down Expand Up @@ -464,6 +467,7 @@ func (m RootModel) settingsDraft() (config.Settings, bool, bool) {
draft := m.settings
draft.SessionSize = count
draft.WriteModeDifficulty = config.NormalizeWriteModeDifficulty(m.settingsWriteDifficulty)
draft.UpdateCheckEnabled = m.settingsUpdateCheckEnabled
draft.AudioEnabled = m.settingsAudioEnabled
draft.AudioVoice = m.settingsAudioVoice
draft.AudioAutoplay = m.settingsAudioAutoplay && m.settingsAudioAvailable()
Expand Down Expand Up @@ -509,6 +513,7 @@ func (m RootModel) applySettings(settings config.Settings) (RootModel, error) {
m.settingsEditing = false
m.settingsInput = strconv.Itoa(settings.SessionSize)
m.settingsWriteDifficulty = config.NormalizeWriteModeDifficulty(settings.WriteModeDifficulty)
m.settingsUpdateCheckEnabled = settings.UpdateCheckEnabled
m.settingsAudioEnabled = settings.AudioEnabled
m.settingsAudioVoices, m.settingsAudioVoicesLoaded = m.loadAudioVoices()
m.settingsAudioVoice = normalizeAudioVoiceInList(settings.AudioVoice, m.settingsAudioVoices, m.settingsAudioVoicesLoaded)
Expand Down
4 changes: 4 additions & 0 deletions internal/app/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ func (m RootModel) updateSettingsOverlay(msg tea.KeyPressMsg) (tea.Model, tea.Cm
m.settingsInput = strconv.Itoa(count)
case settingsRowWriteDifficulty:
m.settingsWriteDifficulty = config.WriteModeDifficultyBasic
case settingsRowUpdateCheck:
m.settingsUpdateCheckEnabled = false
case settingsRowAudioEnabled:
m.settingsAudioEnabled = false
m.settingsAudioAutoplay = false
Expand All @@ -370,6 +372,8 @@ func (m RootModel) updateSettingsOverlay(msg tea.KeyPressMsg) (tea.Model, tea.Cm
m.settingsInput = strconv.Itoa(count)
case settingsRowWriteDifficulty:
m.settingsWriteDifficulty = config.WriteModeDifficultyHard
case settingsRowUpdateCheck:
m.settingsUpdateCheckEnabled = true
case settingsRowAudioEnabled:
m.settingsAudioEnabled = true
case settingsRowAudioVoice:
Expand Down
73 changes: 73 additions & 0 deletions internal/app/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,79 @@ func TestUpdateHomeSettingsDifficultySwitchesWithArrowKeys(t *testing.T) {
}
}

func TestUpdateHomeSettingsUpdateCheckSwitchesWithArrowKeys(t *testing.T) {
t.Parallel()

model := NewModel(nil, Options{
Settings: config.Settings{
SessionSize: 10,
ReviewRatio: 0.4,
WriteModeDifficulty: config.WriteModeDifficultyBasic,
UpdateCheckEnabled: true,
AudioEnabled: true,
Language: i18n.LangJA,
ThemeMode: config.ThemeModeDefault,
},
})
model.loading = false
model = model.openSettingsOverlay()
model.settingsCursor = settingsRowUpdateCheck

next, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyLeft})
updated := next.(RootModel)
if updated.settingsUpdateCheckEnabled {
t.Fatal("settingsUpdateCheckEnabled after left = true, want false")
}

next, _ = updated.Update(tea.KeyPressMsg{Code: tea.KeyRight})
updated = next.(RootModel)
if !updated.settingsUpdateCheckEnabled {
t.Fatal("settingsUpdateCheckEnabled after right = false, want true")
}
}

func TestUpdateHomeSettingsSavePersistsUpdateCheckToggle(t *testing.T) {
t.Parallel()

path := filepath.Join(t.TempDir(), "config.toml")
model := NewModel(nil, Options{
Settings: config.Settings{
SessionSize: 10,
ReviewRatio: 0.4,
WriteModeDifficulty: config.WriteModeDifficultyBasic,
UpdateCheckEnabled: true,
AudioEnabled: true,
Language: i18n.LangJA,
ThemeMode: config.ThemeModeDefault,
},
ConfigPath: path,
})
model.loading = false
model = model.openSettingsOverlay()
model.settingsCursor = settingsRowUpdateCheck
model.settingsUpdateCheckEnabled = false

next, cmd := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
updated := next.(RootModel)
if cmd == nil {
t.Fatal("cmd = nil, want save settings command")
}

saved, _ := updated.Update(cmd())
final := saved.(RootModel)
if final.settings.UpdateCheckEnabled {
t.Fatal("settings.UpdateCheckEnabled = true, want false")
}

savedSettings, err := config.Load(path)
if err != nil {
t.Fatalf("Load(saved config) error = %v", err)
}
if savedSettings.UpdateCheckEnabled {
t.Fatal("saved UpdateCheckEnabled = true, want false")
}
}

func TestUpdateKeymapEditorSavesOverrideAndAppliesImmediately(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 2 additions & 0 deletions internal/app/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ func (m RootModel) renderSettingsOverlay() string {
"",
m.renderSettingsRow(settingsRowQuestionCount, i18n.T(i18n.SettingsQuestions), m.settingsQuestionDisplay()),
m.renderSettingsRow(settingsRowWriteDifficulty, i18n.T(i18n.SettingsWriteDifficulty), m.settingsWriteDifficultyLabel()),
m.renderSettingsRow(settingsRowUpdateCheck, i18n.T(i18n.SettingsUpdateCheck), audioStateLabel(m.settingsUpdateCheckEnabled)),
m.renderSettingsRow(settingsRowAudioEnabled, i18n.T(i18n.SettingsAudioEnabled), audioStateLabel(m.settingsAudioEnabled)),
m.renderSettingsRow(settingsRowAudioVoice, i18n.T(i18n.SettingsAudioVoice), m.settingsAudioVoiceLabel()),
Comment on lines 197 to 201
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

audioStateLabel(...) is now used to render the non-audio setting Startup update check. Since it sources its text from i18n.AudioStateOn/Off (under the [audio] locale table), this couples unrelated UI to the audio domain and the helper name is misleading. Consider introducing a more generic on/off label helper (and/or generic i18n keys) and using that here to keep semantics clear as more toggles are added.

Copilot uses AI. Check for mistakes.
m.renderSettingsRow(settingsRowAudioAutoplay, i18n.T(i18n.SettingsAudioAutoplay), audioStateLabel(m.settingsAudioAutoplay && m.settingsAudioAvailable())),
Expand Down Expand Up @@ -314,6 +315,7 @@ func (m RootModel) renderSettingsOverlayCompact() string {
"",
m.renderCompactSelectable(style, m.settingsCursor == settingsRowQuestionCount, i18n.T(i18n.SettingsQuestions), m.settingsQuestionDisplay()),
m.renderCompactSelectable(style, m.settingsCursor == settingsRowWriteDifficulty, i18n.T(i18n.SettingsWriteDifficulty), m.settingsWriteDifficultyLabel()),
m.renderCompactSelectable(style, m.settingsCursor == settingsRowUpdateCheck, i18n.T(i18n.SettingsUpdateCheck), audioStateLabel(m.settingsUpdateCheckEnabled)),
m.renderCompactSelectable(style, m.settingsCursor == settingsRowAudioEnabled, i18n.T(i18n.SettingsAudioEnabled), audioStateLabel(m.settingsAudioEnabled)),
m.renderCompactSelectable(style, m.settingsCursor == settingsRowAudioVoice, i18n.T(i18n.SettingsAudioVoice), m.settingsAudioVoiceLabel()),
m.renderCompactSelectable(style, m.settingsCursor == settingsRowAudioAutoplay, i18n.T(i18n.SettingsAudioAutoplay), audioStateLabel(m.settingsAudioAutoplay && m.settingsAudioAvailable())),
Expand Down
2 changes: 2 additions & 0 deletions internal/app/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ func TestRenderHomeWithSettingsOverlayUsesScreenSwitch(t *testing.T) {
model.settingsOpen = true
model.settingsInput = "10"
model.settingsWriteDifficulty = config.WriteModeDifficultyHard
model.settingsUpdateCheckEnabled = true
model.settingsAudioEnabled = true
model.settingsAudioAutoplay = true
model.settingsLanguage = i18n.LangJA
Expand All @@ -803,6 +804,7 @@ func TestRenderHomeWithSettingsOverlayUsesScreenSwitch(t *testing.T) {
for _, want := range []string{
i18n.T(i18n.SettingsWriteDifficulty),
i18n.T(i18n.SettingsWriteDifficultyHard),
i18n.T(i18n.SettingsUpdateCheck),
i18n.T(i18n.SettingsAudioEnabled),
i18n.T(i18n.SettingsAudioAutoplay),
i18n.T(i18n.SettingsTheme),
Expand Down
9 changes: 9 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Settings struct {
ReviewRatio float64
FocusModeDefault bool
WriteModeDifficulty string
UpdateCheckEnabled bool
AudioEnabled bool
AudioAutoplay bool
AudioVoice string
Expand All @@ -64,6 +65,7 @@ type fileSettings struct {
ReviewRatio *float64 `toml:"review_ratio"`
FocusModeDefault *bool `toml:"focus_mode_default"`
WriteModeDifficulty *string `toml:"write_mode_difficulty"`
UpdateCheckEnabled *bool `toml:"startup_update_check"`
AudioEnabled *bool `toml:"audio_enabled"`
AudioAutoplay *bool `toml:"audio_autoplay"`
AudioVoice *string `toml:"audio_voice"`
Expand All @@ -86,6 +88,7 @@ func DefaultSettings() Settings {
SessionSize: session.DefaultQuestionCount,
ReviewRatio: session.DefaultReviewRatio,
WriteModeDifficulty: WriteModeDifficultyBasic,
UpdateCheckEnabled: true,
AudioEnabled: true,
AudioAutoplay: false,
Language: i18n.DefaultLang,
Expand All @@ -98,6 +101,7 @@ func (s Settings) IsZero() bool {
s.ReviewRatio == 0 &&
!s.FocusModeDefault &&
s.WriteModeDifficulty == "" &&
!s.UpdateCheckEnabled &&
!s.AudioEnabled &&
!s.AudioAutoplay &&
s.AudioVoice == "" &&
Expand Down Expand Up @@ -143,6 +147,9 @@ func Load(path string) (Settings, error) {
}
settings.WriteModeDifficulty = writeModeDifficulty
}
if raw.UpdateCheckEnabled != nil {
settings.UpdateCheckEnabled = *raw.UpdateCheckEnabled
}
if raw.AudioEnabled != nil {
settings.AudioEnabled = *raw.AudioEnabled
}
Expand Down Expand Up @@ -216,6 +223,7 @@ func Save(path string, settings Settings) error {
ReviewRatio float64 `toml:"review_ratio"`
FocusModeDefault bool `toml:"focus_mode_default"`
WriteModeDifficulty string `toml:"write_mode_difficulty"`
UpdateCheckEnabled bool `toml:"startup_update_check"`
AudioEnabled bool `toml:"audio_enabled"`
AudioAutoplay bool `toml:"audio_autoplay"`
AudioVoice string `toml:"audio_voice"`
Expand All @@ -228,6 +236,7 @@ func Save(path string, settings Settings) error {
ReviewRatio: settings.ReviewRatio,
FocusModeDefault: settings.FocusModeDefault,
WriteModeDifficulty: settings.WriteModeDifficulty,
UpdateCheckEnabled: settings.UpdateCheckEnabled,
AudioEnabled: settings.AudioEnabled,
AudioAutoplay: settings.AudioAutoplay,
AudioVoice: settings.AudioVoice,
Expand Down
5 changes: 5 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ session_size = 12
review_ratio = 0.4
focus_mode_default = true
write_mode_difficulty = "hard"
startup_update_check = false
audio_enabled = false
audio_autoplay = true
audio_voice = " Samantha "
Expand All @@ -53,6 +54,9 @@ audio_voice = " Samantha "
if settings.WriteModeDifficulty != WriteModeDifficultyHard {
t.Fatalf("WriteModeDifficulty = %q, want %q", settings.WriteModeDifficulty, WriteModeDifficultyHard)
}
if settings.UpdateCheckEnabled {
t.Fatal("UpdateCheckEnabled = true, want false")
}
if settings.AudioEnabled {
t.Fatal("AudioEnabled = true, want false")
}
Expand Down Expand Up @@ -135,6 +139,7 @@ func TestSaveRoundTripsSettings(t *testing.T) {
ReviewRatio: 0.6,
FocusModeDefault: true,
WriteModeDifficulty: WriteModeDifficultyHard,
UpdateCheckEnabled: false,
AudioEnabled: false,
AudioAutoplay: true,
AudioVoice: "Samantha",
Expand Down
1 change: 1 addition & 0 deletions internal/i18n/i18n_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func TestAllJAKeysExistInEN(t *testing.T) {
i18n.HomeUpdateDetail, i18n.HomeUpdateHint, i18n.HomeKeys,
i18n.SettingsTitle, i18n.SettingsQuestions, i18n.SettingsWriteDifficulty,
i18n.SettingsWriteDifficultyBasic, i18n.SettingsWriteDifficultyHard,
i18n.SettingsUpdateCheck,
i18n.SettingsAudioEnabled, i18n.SettingsAudioVoice, i18n.SettingsAudioVoiceAuto, i18n.SettingsAudioVoiceUnavailable, i18n.SettingsAudioAutoplay,
i18n.SettingsLanguage, i18n.SettingsLanguageJA, i18n.SettingsLanguageEN,
i18n.SettingsTheme, i18n.SettingsThemeDefault, i18n.SettingsThemeNoColor,
Expand Down
1 change: 1 addition & 0 deletions internal/i18n/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
SettingsWriteDifficulty = "settings.write_difficulty"
SettingsWriteDifficultyBasic = "settings.write_difficulty_basic"
SettingsWriteDifficultyHard = "settings.write_difficulty_hard"
SettingsUpdateCheck = "settings.update_check"
SettingsAudioEnabled = "settings.audio_enabled"
SettingsAudioVoice = "settings.audio_voice"
SettingsAudioVoiceAuto = "settings.audio_voice_auto"
Expand Down
Loading