From a93ff6cefd0eb711f5e93eb3a78a3eebb3886826 Mon Sep 17 00:00:00 2001 From: mumeinosato <66110797+mumeinosato@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:39:16 +0900 Subject: [PATCH 1/7] =?UTF-8?q?UI/=E5=9F=BA=E7=9B=A4=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E3=81=AE=E5=A4=A7=E5=B9=85=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF?= =?UTF-8?q?=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=A8=E5=A4=9A=E8=A8=80?= =?UTF-8?q?=E8=AA=9E=E5=AF=BE=E5=BF=9C=E5=BC=B7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI・ロジック構造を全面的に整理し、可読性・保守性・拡張性を向上。設定・状態管理、UI初期化、プロセス監視、Discord RPC、キャラクター情報、アプリ定数などを専用クラスに分離。 多言語対応をJSONリソースベースに刷新し、言語切替・翻訳管理を強化。 テーマ・フォント・トレイアイコン・アップデートチェック等の基盤機能も新規実装し、今後の機能追加やUI拡張が容易な構成へ。 --- GkmStatus/AboutForm.Designer.cs | 180 ++ GkmStatus/AboutForm.cs | 123 +- GkmStatus/AboutForm.resx | 120 ++ GkmStatus/GkmStatus.csproj | 22 + GkmStatus/MainForm.Designer.cs | 747 ++++++- GkmStatus/MainForm.cs | 1997 ++++--------------- GkmStatus/MainForm.resx | 72 +- GkmStatus/Properties/Resources.Designer.cs | 63 + GkmStatus/Properties/Resources.resx | 120 ++ GkmStatus/Resources/I18n/english.json | 433 ++++ GkmStatus/Resources/I18n/japanese.json | 439 ++++ GkmStatus/Resources/produce_characters.json | 69 + GkmStatus/Translation.cs | 261 --- GkmStatus/src/AppConstants.cs | 54 + GkmStatus/src/AppLogic.cs | 121 ++ GkmStatus/src/Characters.cs | 75 + GkmStatus/src/ConfigManager.cs | 139 ++ GkmStatus/src/DiscordRpc.cs | 178 ++ GkmStatus/src/ProcessWatcher.cs | 82 + GkmStatus/src/UpdateService.cs | 108 + GkmStatus/src/native/NativeMethods.cs | 26 + GkmStatus/src/ui/CustomRenderer.cs | 92 + GkmStatus/src/ui/FontManager.cs | 147 ++ GkmStatus/src/ui/ThemeManager.cs | 189 ++ GkmStatus/src/ui/Translation.cs | 244 +++ GkmStatus/src/ui/TrayIconManager.cs | 138 ++ 26 files changed, 4246 insertions(+), 1993 deletions(-) create mode 100644 GkmStatus/AboutForm.Designer.cs create mode 100644 GkmStatus/AboutForm.resx create mode 100644 GkmStatus/Properties/Resources.Designer.cs create mode 100644 GkmStatus/Properties/Resources.resx create mode 100644 GkmStatus/Resources/I18n/english.json create mode 100644 GkmStatus/Resources/I18n/japanese.json create mode 100644 GkmStatus/Resources/produce_characters.json delete mode 100644 GkmStatus/Translation.cs create mode 100644 GkmStatus/src/AppConstants.cs create mode 100644 GkmStatus/src/AppLogic.cs create mode 100644 GkmStatus/src/Characters.cs create mode 100644 GkmStatus/src/ConfigManager.cs create mode 100644 GkmStatus/src/DiscordRpc.cs create mode 100644 GkmStatus/src/ProcessWatcher.cs create mode 100644 GkmStatus/src/UpdateService.cs create mode 100644 GkmStatus/src/native/NativeMethods.cs create mode 100644 GkmStatus/src/ui/CustomRenderer.cs create mode 100644 GkmStatus/src/ui/FontManager.cs create mode 100644 GkmStatus/src/ui/ThemeManager.cs create mode 100644 GkmStatus/src/ui/Translation.cs create mode 100644 GkmStatus/src/ui/TrayIconManager.cs diff --git a/GkmStatus/AboutForm.Designer.cs b/GkmStatus/AboutForm.Designer.cs new file mode 100644 index 0000000..6852e47 --- /dev/null +++ b/GkmStatus/AboutForm.Designer.cs @@ -0,0 +1,180 @@ +using GkmStatus.src.ui; + +namespace GkmStatus +{ + partial class AboutForm + { + private System.ComponentModel.IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + // GUIDesignerをだます + private void InitializeComponent() + { + + } + + void Cfg(T c, Action action) where T : Control => action(c); + private int S(int val) => (int)Math.Round(val * _scale); + + // 動的に上書きする + private void ApplyComponent() { + this.Text = I18n.T(I18n.Text_List.App_Name); + this.AutoScaleMode = AutoScaleMode.None; + this.FormBorderStyle = FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.ShowInTaskbar = false; + this.StartPosition = FormStartPosition.CenterParent; + this.BackColor = _backColor; + this.ForeColor = _foreColor; + this.ClientSize = new Size(S(320), S(280)); + + // Icon + var iconBox = new PictureBox + { + Size = new Size(S(64), S(64)), + Location = new Point((this.ClientSize.Width - S(64)) / 2, S(25)), + SizeMode = PictureBoxSizeMode.Zoom + }; + try + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + using Stream stream = assembly.GetManifestResourceStream("GkmStatus.Resources.app_icon_2048px.png"); + if (stream != null) + { + iconBox.Image = Image.FromStream(stream); + } + using Stream icoStream = assembly.GetManifestResourceStream("GkmStatus.Resources.app.ico"); + if (icoStream != null) this.Icon = new Icon(icoStream); + } + catch { } + this.Controls.Add(iconBox); + + // App Name + var lblName = new Label + { + Text = I18n.T(I18n.Text_List.App_Name), + Font = _titleFont, + TextAlign = ContentAlignment.MiddleCenter, + Location = new Point(0, S(95)), + Size = new Size(this.ClientSize.Width, S(30)) + }; + this.Controls.Add(lblName); + + // Version + var lblVersion = new Label + { + Text = "v" + Application.ProductVersion, + Font = _mainFont, + TextAlign = ContentAlignment.MiddleCenter, + Location = new Point(0, S(125)), + Size = new Size(this.ClientSize.Width, S(20)), + ForeColor = _foreColor.GetBrightness() > 0.5f ? Color.FromArgb(100, 100, 100) : Color.FromArgb(180, 180, 180) + }; + this.Controls.Add(lblVersion); + + // Author + var lblAuthor = new Label + { + Text = I18n.T(I18n.Text_List.About_Author), + Font = _mainFont, + TextAlign = ContentAlignment.MiddleCenter, + Location = new Point(0, S(150)), + Size = new Size(this.ClientSize.Width, S(20)) + }; + this.Controls.Add(lblAuthor); + + // Font Attribution + var lblFont = new Label + { + Text = I18n.T(I18n.Text_List.About_FontAttribution), + Font = new Font(_mainFont.FontFamily, 8f, GraphicsUnit.Point), + TextAlign = ContentAlignment.MiddleCenter, + Location = new Point(0, S(172)), + Size = new Size(this.ClientSize.Width, S(15)), + ForeColor = _foreColor.GetBrightness() > 0.5f ? Color.FromArgb(120, 120, 120) : Color.FromArgb(150, 150, 150) + }; + this.Controls.Add(lblFont); + + var lnkLicense = new LinkLabel + { + Text = I18n.T(I18n.Text_List.About_FontLicense), + Font = new Font(_mainFont.FontFamily, 8f, GraphicsUnit.Point), + TextAlign = ContentAlignment.MiddleCenter, + Location = new Point(0, S(187)), + Size = new Size(this.ClientSize.Width, S(15)), + LinkColor = COLOR_PRIMARY_DEFAULT, + ActiveLinkColor = Color.White, + VisitedLinkColor = COLOR_PRIMARY_DEFAULT + }; + lnkLicense.LinkClicked += (s, e) => ShowLicense(); + this.Controls.Add(lnkLicense); + + // OK Button + var btnOk = new Button + { + Text = "OK", + Size = new Size(S(90), S(35)), + Location = new Point((this.ClientSize.Width - S(90)) / 2, S(225)), + FlatStyle = FlatStyle.Flat, + Font = _mainFont, + Cursor = Cursors.Hand + }; + btnOk.FlatAppearance.BorderColor = _foreColor.GetBrightness() > 0.5f ? Color.Gray : Color.FromArgb(100, 100, 100); + btnOk.Click += (s, e) => this.Close(); + this.Controls.Add(btnOk); + } + + private void ShowLicense() + { + try + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + using Stream stream = assembly.GetManifestResourceStream("GkmStatus.Resources.fonts.IBM_Plex_Sans_JP.OFL.txt"); + if (stream != null) + { + using var reader = new StreamReader(stream); + string licenseText = reader.ReadToEnd(); + using var licenseForm = new Form + { + Text = "Font License", + Icon = this.Icon, + ShowIcon = true, + Size = new Size(S(500), S(400)), + StartPosition = FormStartPosition.CenterParent, + BackColor = _backColor, + ForeColor = _foreColor, + MinimizeBox = false, + MaximizeBox = false + }; + var txt = new TextBox + { + Multiline = true, + ReadOnly = true, + Dock = DockStyle.Fill, + Text = licenseText, + ScrollBars = ScrollBars.Vertical, + BackColor = _backColor, + ForeColor = _foreColor, + BorderStyle = BorderStyle.None, + Font = new Font(FontFamily.GenericMonospace, 9f, GraphicsUnit.Point) + }; + licenseForm.Controls.Add(txt); + licenseForm.ShowDialog(this); + } + } + catch (Exception ex) + { + MessageBox.Show("Could not load license: " + ex.Message); + } + } + } +} diff --git a/GkmStatus/AboutForm.cs b/GkmStatus/AboutForm.cs index 1fd3eac..d10502e 100644 --- a/GkmStatus/AboutForm.cs +++ b/GkmStatus/AboutForm.cs @@ -22,121 +22,17 @@ public AboutForm(Color backColor, Color foreColor, Font mainFont, float scale) _scale = scale; _titleFont = new Font(mainFont.FontFamily, 14f, FontStyle.Bold, GraphicsUnit.Point); - InitializeComponent(); - } - - private void InitializeComponent() - { - this.Text = I18n.T("About_Title"); - this.AutoScaleMode = AutoScaleMode.None; - this.FormBorderStyle = FormBorderStyle.FixedDialog; - this.MaximizeBox = false; - this.MinimizeBox = false; - this.ShowInTaskbar = false; - this.StartPosition = FormStartPosition.CenterParent; - this.BackColor = _backColor; - this.ForeColor = _foreColor; - this.ClientSize = new Size(S(320), S(280)); - - // Icon - var iconBox = new PictureBox - { - Size = new Size(S(64), S(64)), - Location = new Point((this.ClientSize.Width - S(64)) / 2, S(25)), - SizeMode = PictureBoxSizeMode.Zoom - }; - try - { - var assembly = System.Reflection.Assembly.GetExecutingAssembly(); - using Stream? stream = assembly.GetManifestResourceStream("GkmStatus.Resources.app_icon_2048px.png"); - if (stream != null) - { - iconBox.Image = Image.FromStream(stream); - } - using Stream? icoStream = assembly.GetManifestResourceStream("GkmStatus.Resources.app.ico"); - if (icoStream != null) this.Icon = new Icon(icoStream); - } - catch { } - this.Controls.Add(iconBox); - - // App Name - var lblName = new Label - { - Text = I18n.T("App_Name"), - Font = _titleFont, - TextAlign = ContentAlignment.MiddleCenter, - Location = new Point(0, S(95)), - Size = new Size(this.ClientSize.Width, S(30)) - }; - this.Controls.Add(lblName); - - // Version - var lblVersion = new Label - { - Text = "v" + Application.ProductVersion, - Font = _mainFont, - TextAlign = ContentAlignment.MiddleCenter, - Location = new Point(0, S(125)), - Size = new Size(this.ClientSize.Width, S(20)), - ForeColor = _foreColor.GetBrightness() > 0.5f ? Color.FromArgb(100, 100, 100) : Color.FromArgb(180, 180, 180) - }; - this.Controls.Add(lblVersion); - - // Author - var lblAuthor = new Label - { - Text = I18n.T("About_Author"), - Font = _mainFont, - TextAlign = ContentAlignment.MiddleCenter, - Location = new Point(0, S(150)), - Size = new Size(this.ClientSize.Width, S(20)) - }; - this.Controls.Add(lblAuthor); - - // Font Attribution - var lblFont = new Label - { - Text = I18n.T("About_FontAttribution"), - Font = new Font(_mainFont.FontFamily, 8f, GraphicsUnit.Point), - TextAlign = ContentAlignment.MiddleCenter, - Location = new Point(0, S(172)), - Size = new Size(this.ClientSize.Width, S(15)), - ForeColor = _foreColor.GetBrightness() > 0.5f ? Color.FromArgb(120, 120, 120) : Color.FromArgb(150, 150, 150) - }; - this.Controls.Add(lblFont); + //SetupInitializeComponent(); - var lnkLicense = new LinkLabel - { - Text = I18n.T("About_FontLicense"), - Font = new Font(_mainFont.FontFamily, 8f, GraphicsUnit.Point), - TextAlign = ContentAlignment.MiddleCenter, - Location = new Point(0, S(187)), - Size = new Size(this.ClientSize.Width, S(15)), - LinkColor = COLOR_PRIMARY_DEFAULT, - ActiveLinkColor = Color.White, - VisitedLinkColor = COLOR_PRIMARY_DEFAULT - }; - lnkLicense.LinkClicked += (s, e) => ShowLicense(); - this.Controls.Add(lnkLicense); - - // OK Button - var btnOk = new Button - { - Text = "OK", - Size = new Size(S(90), S(35)), - Location = new Point((this.ClientSize.Width - S(90)) / 2, S(225)), - FlatStyle = FlatStyle.Flat, - Font = _mainFont, - Cursor = Cursors.Hand - }; - btnOk.FlatAppearance.BorderColor = _foreColor.GetBrightness() > 0.5f ? Color.Gray : Color.FromArgb(100, 100, 100); - btnOk.Click += (s, e) => this.Close(); - this.Controls.Add(btnOk); + SuspendLayout(); + InitializeComponent(); + ApplyComponent(); + ResumeLayout(); } private static readonly Color COLOR_PRIMARY_DEFAULT = ColorTranslator.FromHtml("#7e87f4"); - private void ShowLicense() + private void ShowLicense(object sender, LinkLabelLinkClickedEventArgs e) { try { @@ -186,8 +82,6 @@ protected override void OnLoad(EventArgs e) SetTitleBarDarkMode(_backColor.GetBrightness() < 0.5f); } - private int S(int val) => (int)Math.Round(val * _scale); - [System.Runtime.InteropServices.LibraryImport("dwmapi.dll")] private static partial int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); @@ -201,5 +95,10 @@ private void SetTitleBarDarkMode(bool dark) } catch { } } + + private void Close(object sender, EventArgs e) + { + this.Close(); + } } } diff --git a/GkmStatus/AboutForm.resx b/GkmStatus/AboutForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/GkmStatus/AboutForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GkmStatus/GkmStatus.csproj b/GkmStatus/GkmStatus.csproj index 51a36f6..2d51a5d 100644 --- a/GkmStatus/GkmStatus.csproj +++ b/GkmStatus/GkmStatus.csproj @@ -27,6 +27,9 @@ + + + @@ -37,4 +40,23 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + \ No newline at end of file diff --git a/GkmStatus/MainForm.Designer.cs b/GkmStatus/MainForm.Designer.cs index ffe130b..478ef1a 100644 --- a/GkmStatus/MainForm.Designer.cs +++ b/GkmStatus/MainForm.Designer.cs @@ -1,16 +1,21 @@ -namespace GkmStatus +using GkmStatus.src; +using GkmStatus.src.native; +using GkmStatus.src.ui; +using System.Diagnostics; +using System.Net.NetworkInformation; +using System.Text; +using static GkmStatus.src.AppConstants; +using static GkmStatus.src.Characters; +using static GkmStatus.src.ui.I18n.Text_List; +using static GkmStatus.src.AppSettingsHelper; +using static GkmStatus.src.StartupManager; + +namespace GkmStatus { partial class MainForm { - /// - /// Required designer variable. - /// private System.ComponentModel.IContainer components = null; - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) @@ -20,20 +25,722 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// + //GUIDesignerをだます private void InitializeComponent() { - this.components = new System.ComponentModel.Container(); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(600, 750); - this.Text = "MainForm"; + + } + + void Cfg(T c, Action action) => action?.Invoke(c); + private int S(int value) => (int)Math.Round(value * uiScale); + + private int StanderdCmbHight() + { + //後でキャッシュする + using var temp = new ComboBox { Font = _fontManager.AppFont, FlatStyle = FlatStyle.Flat, DropDownStyle = ComboBoxStyle.DropDownList }; + return temp.PreferredHeight; + } + + private T WrapStylePanel(T control, Point loc, int w) where T : Control + { + int autoHeight = StanderdCmbHight(); + + Panel container = new() + { + Location = loc, + Size = new(w, autoHeight), + BackColor = Default_BackColor, + Parent = this + }; + + container.Paint += (s, e) => + { + if (container.Tag is Color col) + ControlPaint.DrawBorder(e.Graphics, container.ClientRectangle, col, ButtonBorderStyle.Solid); + }; + + control.Location = new Point(S(5), (autoHeight - TextRenderer.MeasureText("Ag", control.Font).Height) / 2 - S(1)); + control.Width = w - S(10); + control.Parent = container; + + control.Tag = container; + + return control; + } + + private Button CreateButton(Point l, int w, Color bc) + { + int btnHeight = Math.Max(S(35), TextRenderer.MeasureText("Ag", _fontManager.AppFontBold).Height + S(15)); + return new Button + { + Location = l, + Size = new Size(w, btnHeight), + BackColor = bc, + ForeColor = Color.White, + FlatStyle = FlatStyle.Flat, + Font = _fontManager.AppFontBold, + Cursor = Cursors.Hand, + Parent = this + }; + } + + private ComboBox CreateCombo(Point loc, int w, string[] i) + { + var cb = new ComboBox + { + DropDownStyle = ComboBoxStyle.DropDownList, + BackColor = Default_BackColor, + ForeColor = Default_ForeColor, + FlatStyle = FlatStyle.Flat, + Font = _fontManager.AppFont, + Location = loc, + Width = w, + Parent = this + }; + cb.Items.AddRange(i); + return cb; + } + + private NumericUpDown CreateNumeric(Point loc, int w) + { + var nud = new NumericUpDown + { + BorderStyle = BorderStyle.None, + BackColor = Default_BackColor, + ForeColor = Default_ForeColor, + Font = _fontManager.AppFont, + Minimum = 1, + Maximum = 100 + }; + return WrapStylePanel(nud, loc, w); + } + + private TextBox CreateText(Point loc, int w) + { + var tb = new TextBox + { + BorderStyle = BorderStyle.None, + BackColor = Default_BackColor, + ForeColor = Default_ForeColor, + Font = _fontManager.AppFont + }; + return WrapStylePanel(tb, loc, w); + } + + + private void ApplyComponent() + { + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; + + this.ClientSize = new Size(S(520), S(630)); + this.FormBorderStyle = FormBorderStyle.FixedSingle; + + this.MaximizeBox = false; + this.StartPosition = FormStartPosition.CenterScreen; + this.BackColor = Color.FromArgb(32, 34, 37); + this.ForeColor = Color.White; + this.Font = _fontManager.AppFont; + + try + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + using Stream stream = assembly.GetManifestResourceStream("GkmStatus.Resources.app.ico"); + if (stream != null) + { + this.Icon = new Icon(stream); + } + } + catch (Exception ex) + { + Debug.WriteLine("Icon Load Error: " + ex.Message); + } + + trayIcon = new NotifyIcon + { + Icon = this.Icon ?? SystemIcons.Application, + Visible = false + }; + + trayIcon.MouseClick += (s, e) => + { + if (e.Button == MouseButtons.Left) RestoreFromTray(); + }; + trayIcon.BalloonTipClicked += (s, e) => + { + if (trayIcon.Tag is string url && !string.IsNullOrEmpty(url)) + { + try { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } catch { } + trayIcon.Tag = null; + } + else + { + this.Invoke((MethodInvoker)(() => RestoreFromTray())); + } + }; + + var trayMenu = new ContextMenuStrip(); + trayMenu.Opening += (s, e) => + { + _trayIconManager.UpdateMenuState(rpc.Status); + Native.SetForegroundWindow(this.Handle); + string stateStr = GetStateString(cmbStateType.SelectedIndex); + _trayIconManager.SetProduceMenuEnabled(stateStr == "Idol" || stateStr == "Producing"); + }; + trayMenu.Items.Add(I18n.T(Tray_Open), null, (s, e) => RestoreFromTray()); + trayMenu.Items.Add(new ToolStripSeparator()); + + trayMenuDetails = new ToolStripMenuItem(I18n.T(Header_Details)) { + Name = "trayMenuDetails" + }; + trayMenuDetails.DropDownItems.Add(new ToolStripMenuItem("...")); + trayMenuDetails.DropDownOpening += (s, e) => + { + trayMenuDetails.DropDownItems.Clear(); + for (int i = 0; i < cmbDetailsType.Items.Count; i++) + { + string text = cmbDetailsType.Items[i]?.ToString() ?? ""; + var item = new ToolStripMenuItem(text); + if (cmbDetailsType.SelectedIndex == i) item.Checked = true; + int index = i; + item.Click += (sender, ev) => + { + cmbDetailsType.SelectedIndex = index; + UpdateRpc(); + if (notifyInBackgroundItem?.Checked == true) + { + trayIcon.Tag = null; + trayIcon.ShowBalloonTip(3000, I18n.T(App_Name), I18n.T(Notify_TrayDetailsChanged), ToolTipIcon.Info); + } + }; + trayMenuDetails.DropDownItems.Add(item); + } + }; + + trayMenuState = new ToolStripMenuItem(I18n.T(Header_State)) + { + Name = "trayMenuState" + }; + trayMenuState.DropDownItems.Add(new ToolStripMenuItem("...")); + trayMenuState.DropDownOpening += (s, e) => + { + trayMenuState.DropDownItems.Clear(); + for (int i = 0; i < cmbStateType.Items.Count; i++) + { + string text = cmbStateType.Items[i]?.ToString() ?? ""; + var item = new ToolStripMenuItem(text); + if (cmbStateType.SelectedIndex == i) item.Checked = true; + int index = i; + item.Click += (sender, ev) => + { + cmbStateType.SelectedIndex = index; + UpdateRpc(); + if (trayMenuProduce != null) + { + string stateStr = GetStateString(index); + trayMenuProduce.Enabled = (stateStr == "Idol" || stateStr == "Producing"); + } + + if (notifyInBackgroundItem?.Checked == true) + { + trayIcon.Tag = null; + trayIcon.ShowBalloonTip(3000, I18n.T(App_Name), I18n.T(Notify_TrayStateChanged), ToolTipIcon.Info); + } + }; + trayMenuState.DropDownItems.Add(item); + } + }; + + trayMenuProduce = new ToolStripMenuItem(I18n.T(Tray_ProducingIdol)) + { + Name = "trayMenuProduce" + }; + trayMenuProduce.DropDownItems.Add(new ToolStripMenuItem("...")); + trayMenuProduce.DropDownOpening += (s, e) => + { + trayMenuProduce.DropDownItems.Clear(); + string currentId = (GetStateString(cmbStateType.SelectedIndex) == "Idol") ? CurrentPresence.SelectedIdolCharacterId : CurrentPresence.SelectedProduceCharacterId; + + for (int i = 0; i < ProduceCharacters.Count; i++) + { + var pc = ProduceCharacters[i]; + string displayName = CurrentPresence.CharNameLangIndex == 1 ? pc.NameEn : pc.Display; + var item = new ToolStripMenuItem(displayName); + if (currentId == pc.Id) item.Checked = true; + + int index = i; + item.Click += (sender, ev) => + { + if (index >= 0 && index < cmbProduceCharacter.Items.Count) + { + cmbProduceCharacter.SelectedIndex = index; + } + UpdateRpc(); + + if (notifyInBackgroundItem?.Checked == true) + { + trayIcon.Tag = null; + trayIcon.ShowBalloonTip(3000, I18n.T(App_Name), I18n.T(Notify_TrayIdolChanged), ToolTipIcon.Info); + } + }; + + trayMenuProduce.DropDownItems.Add(item); + } + }; + + trayMenu.Items.Add(trayMenuDetails); + trayMenu.Items.Add(trayMenuState); + trayMenu.Items.Add(trayMenuProduce); + + var traySepSettings = new ToolStripSeparator { Tag = "SepSettings" }; + trayMenu.Items.Add(traySepSettings); + + trayMenuConnect = new ToolStripMenuItem(I18n.T(Button_Connect), null, (s, e) => InitializeRpc()) + { + Name = "trayMenuConnect" + }; + trayMenuPause = new ToolStripMenuItem(I18n.T(Button_Pause), null, (s, e) => PauseRpc()) + { + Name = "trayMenuPause" + }; + trayMenuDisconnect = new ToolStripMenuItem(I18n.T(Button_Disconnect), null, (s, e) => DisposeRpc()) + { + Name = "trayMenuDisconnect" + }; + + trayMenu.Items.AddRange([trayMenuConnect, trayMenuPause, trayMenuDisconnect]); + + trayMenu.Items.Add(new ToolStripSeparator()); + trayMenu.Items.Add(I18n.T(Tray_Exit), null, (s, e) => Application.Exit()); + + trayIcon.ContextMenuStrip = trayMenu; + + int y = S(40); + void AddHeader(int top, string tag) + { + var lbl = new Label {Location = new Point(S(20), top), AutoSize = true, Font = _fontManager.AppFontBold, ForeColor = Color.LightGray, Tag = tag }; + this.Controls.Add(lbl); + } + + AddHeader(y, "Header_GameName"); y += S(25); + cmbGameName = CreateCombo(new(S(20), y), S(210), [.. GameApps.Select(g => g.Name)]); + cmbGameName.SelectedIndex = 0; + cmbGameName.SelectedIndexChanged += CmbGameName_SelectedIndexChanged; + + lblGameAppGuide = new Label + { + Location = new Point(S(240), y - S(7)), + AutoSize = true, + Visible = false, + ForeColor = COLOR_PAUSE, + Font = _fontManager.AppFontMedium + }; + this.Controls.Add(lblGameAppGuide); + + gameAppGuideTimer = new System.Windows.Forms.Timer { Interval = 5000 }; + gameAppGuideTimer.Tick += (s, e) => + { + lblGameAppGuide.Visible = false; + gameAppGuideTimer.Stop(); + }; + + y += S(45); + + AddHeader(y, "Header_Details"); y += S(25); + cmbDetailsType = CreateCombo(new Point(S(20), y), S(180), [I18n.T(Details_None), I18n.T(Details_PName), I18n.T(Details_PLv), I18n.T(Details_Both)]); + cmbDetailsType.SelectedIndex = 3; cmbDetailsType.SelectedIndexChanged += UpdateDetailsInputs; + txtPName = CreateText(new Point(S(210), y), S(200)); + Native.SetPlaceholder(txtPName, I18n.T(Placeholder_PName)); + txtPName.MaxLength = 10; + txtPName.TextChanged += (s, e) => { if (!isInitializing) SaveSettings(); }; + numPLevel = CreateNumeric(new Point(S(420), y), S(80)); + numPLevel.ValueChanged += (s, e) => { if (!isInitializing) SaveSettings(); }; + y += S(45); + + AddHeader(y, "Header_State"); y += S(25); + + cmbStateType = CreateCombo( + new(S(20), y), + S(150), + [I18n.T(State_None), I18n.T(State_PID), I18n.T(State_Idol), I18n.T(State_Producing), I18n.T(State_Custom)] + ); + + cmbProduceCharacter = CreateCombo( + new(S(180), y), + S(165), + [] + ); + cmbProduceCharacter.Visible = false; + + cmbCharNameLang = CreateCombo( + new Point(S(350), y), + S(150), + [I18n.T(CharName_JP), I18n.T(CharName_EN)] + ); + cmbCharNameLang.Visible = false; + cmbCharNameLang.SelectedIndex = 0; + cmbCharNameLang.SelectedIndexChanged += (s, e) => + { + if (!isInitializing) + { + CurrentPresence.CharNameLangIndex = cmbCharNameLang.SelectedIndex; + RefreshProduceCharacterList(); + SaveSettings(); + } + }; + + foreach (var c in ProduceCharacters) + { + cmbProduceCharacter.Items.Add(c.Display); + } + cmbProduceCharacter.SelectedIndex = 0; + + cmbProduceCharacter.SelectedIndexChanged += (s, e) => + { + if (cmbProduceCharacter.SelectedIndex >= 0) + { + string characterId = ProduceCharacters[cmbProduceCharacter.SelectedIndex].Id; + string stateStr = GetStateString(cmbStateType.SelectedIndex); + if (stateStr == "Idol") CurrentPresence.SelectedIdolCharacterId = characterId; + else if (stateStr == "Producing") CurrentPresence.SelectedProduceCharacterId = characterId; + + if (!isInitializing) SaveSettings(); + } + }; + + txtStateCustom = CreateText(new Point(S(180), y), S(320)); txtStateCustom.Enabled = false; + cmbStateType.SelectedIndexChanged += CmbStateType_SelectedIndexChanged; + y += S(45); + + AddHeader(y, "Header_Timestamp"); y += S(25); + lblStartTime = new Label { Text = I18n.T(Timestamp_Label) + ": 00:00:00", Location = new Point(S(20), y + S(7)), AutoSize = true }; + this.Controls.Add(lblStartTime); + btnResetTime = CreateButton(new Point(S(170), y), S(120), COLOR_PRIMARY); + btnResetTime.Click += (s, e) => + { + startTime = DateTime.UtcNow; + UpdateTimestampLabel(); + lblResetGuide.Visible = true; + resetGuideTimer.Stop(); + resetGuideTimer.Start(); + if (rpc.IsInitialized == true) UpdateRpc(); + }; + y += S(45); + + lblResetGuide = new Label + { + Text = I18n.T(Timestamp_Guide), + Location = new Point(S(300), y - S(38)), + AutoSize = true, + Visible = false, + ForeColor = Color.Gray, + Font = _fontManager.AppFontMedium + }; + this.Controls.Add(lblResetGuide); + + resetGuideTimer = new System.Windows.Forms.Timer { Interval = 2000 }; + resetGuideTimer.Tick += (s, e) => + { + lblResetGuide.Visible = false; + resetGuideTimer.Stop(); + }; + + AddHeader(y, "Header_Buttons"); y += S(25); + cmbBtnMode = CreateCombo(new Point(S(20), y), S(150), [I18n.T(Button_ModeNone), I18n.T(Button_ModeStore), I18n.T(Button_ModeApp), I18n.T(Button_ModeCustom)]); + cmbBtnMode.SelectedIndexChanged += CmbBtnMode_SelectedIndexChanged; + + lblBtnModeNote = new Label + { + Location = new Point(S(180), y + S(3)), + AutoSize = true, + Visible = false, + ForeColor = Color.Gray, + Font = _fontManager.AppFontMedium + }; + this.Controls.Add(lblBtnModeNote); + + y += S(35); + txtBtn1Label = CreateText(new Point(S(20), y), S(150)); txtBtn1Label.MaxLength = 31; + txtBtn1Url = CreateText(new Point(S(180), y), S(320)); txtBtn1Url.MaxLength = 512; + y += S(35); + txtBtn2Label = CreateText(new Point(S(20), y), S(150)); txtBtn2Label.MaxLength = 31; + txtBtn2Url = CreateText(new Point(S(180), y), S(320)); txtBtn2Url.MaxLength = 512; + + lblBtnWarning = new Label + { + Text = "", + Location = new Point(S(20), y + S(40)), + AutoSize = true, + Visible = false, + ForeColor = COLOR_PAUSE, + Font = _fontManager.AppFontMedium + }; + this.Controls.Add(lblBtnWarning); + + void CheckBtnWarning(object sender, EventArgs e) + { + bool labelOver = Encoding.UTF8.GetByteCount(txtBtn1Label.Text) > 32 || Encoding.UTF8.GetByteCount(txtBtn2Label.Text) > 32; + bool urlJapanese = JapaneseRegex().IsMatch(txtBtn1Url.Text + txtBtn2Url.Text); + + var sb = new StringBuilder(); + if (labelOver) sb.AppendLine(I18n.T(Button_Warning_LabelLength)); + if (urlJapanese) sb.Append(I18n.T(Button_Warning_UrlJp)); + + string msg = sb.ToString().Trim(); + lblBtnWarning.Text = msg; + lblBtnWarning.Visible = !string.IsNullOrEmpty(msg); + + if (urlJapanese) lblBtnWarning.ForeColor = COLOR_ERROR; + else lblBtnWarning.ForeColor = COLOR_PAUSE; + } + txtBtn1Label.TextChanged += CheckBtnWarning; + txtBtn2Label.TextChanged += CheckBtnWarning; + txtBtn1Url.TextChanged += CheckBtnWarning; + txtBtn2Url.TextChanged += CheckBtnWarning; + + y += S(50); + + y = this.ClientSize.Height - S(80); + footerButtonsY = y; + this.Controls.Add(new Label { BorderStyle = BorderStyle.Fixed3D, Location = new Point(0, y), Size = new Size(S(520), S(2)) }); + y += S(20); + + btnConnect = CreateButton(new Point(S(20), y), S(100), COLOR_CONNECT); + btnConnect.Click += (s, e) => + { + if (rpc.Status == RpcStatus.Connected && !isManualPaused) + PauseRpc(); + else + InitializeRpc(); + }; + btnUpdate = CreateButton(new Point(S(130), y), S(100), COLOR_PRIMARY); + btnUpdate.Enabled = false; btnUpdate.Click += async (s, e) => + { + UpdateRpc(); + string originalText = lblStatus.Text; + Color originalColor = lblStatus.ForeColor; + + string updatedText = I18n.T(Status_Updated); + lblStatus.Text = updatedText; + lblStatus.ForeColor = COLOR_PRIMARY; + statusToolTip.SetToolTip(lblStatus, updatedText.Replace("\n", " ")); + AdjustStatusVerticalPosition(); + + await System.Threading.Tasks.Task.Delay(2000); + + if (this.IsDisposed) return; + + if (rpc.Status == RpcStatus.Connected && !isManualPaused) + { + lblStatus.ForeColor = originalColor; + lblStatus.Text = originalText; + statusToolTip.SetToolTip(lblStatus, originalText.Replace("\n", " ")); + AdjustStatusVerticalPosition(); + } + else if (rpc.Status == RpcStatus.Paused || isManualPaused) + { + UpdateUIForPause(); + } + else + { + UpdateUIForDisconnect(); + } + }; + btnDisconnect = CreateButton(new Point(S(240), y), S(100), COLOR_ERROR); + btnDisconnect.Enabled = false; btnDisconnect.Click += (s, e) => DisposeRpc(); + + lblStatus = new Label { Text = I18n.T(Status_Disconnected), Location = new Point(S(352), y), AutoSize = true, ForeColor = Color.Gray, Font = _fontManager.AppFontMedium }; + statusToolTip = new ToolTip(); + statusToolTip.SetToolTip(lblStatus, lblStatus.Text); + this.Controls.Add(btnConnect); this.Controls.Add(btnUpdate); this.Controls.Add(btnDisconnect); this.Controls.Add(lblStatus); + + + AdjustStatusVerticalPosition(); + + var menu = new MenuStrip(); + fileMenu = new(I18n.T(Menu_File)); + exitItem = new(I18n.T(Menu_Exit)) { ShortcutKeyDisplayString = "Alt+F4" }; + exitItem.Click += (s, e) => { Application.Exit(); }; + fileMenu.DropDownItems.Add(exitItem); + + settingsMenu = new(I18n.T(Menu_Settings)); + runAtStartupItem = new(I18n.T(Menu_RunAtStartup)) { CheckOnClick = true, Checked = IsRunAtStartup() }; + runAtStartupItem.CheckedChanged += (s, e) => SetRunAtStartup(runAtStartupItem.Checked); + + startMinimizedItem = new(I18n.T(Menu_StartMinimized)) { CheckOnClick = true }; + startMinimizedItem.CheckedChanged += (s, e) => SaveSettings(); + + autoConnectItem = new(I18n.T(Menu_AutoConnect)) { CheckOnClick = true }; + autoConnectItem.CheckedChanged += (s, e) => SaveSettings(); + + checkForUpdatesItem = new(I18n.T(Menu_CheckForUpdates)) { CheckOnClick = true, Checked = true }; + checkForUpdatesItem.CheckedChanged += (s, e) => SaveSettings(); + + notifyInBackgroundItem = new(I18n.T(Menu_NotifyInBackground)) { CheckOnClick = true, Checked = true }; + notifyInBackgroundItem.CheckedChanged += (s, e) => SaveSettings(); + + notifyOnMinimizeItem = new(I18n.T(Menu_NotifyOnMinimize)) { CheckOnClick = true, Checked = true }; + notifyOnMinimizeItem.CheckedChanged += (s, e) => SaveSettings(); + + minimizeToTrayItem = new(I18n.T(Menu_MinimizeToTray)) { CheckOnClick = true, Checked = true }; + minimizeToTrayItem.CheckedChanged += (s, e) => SaveSettings(); + + monitorItem = new(I18n.T(Menu_MonitorProcess)) { CheckOnClick = true, Checked = true }; + monitorItem.CheckedChanged += (s, e) => { + _processWatcher.Enabled = monitorItem.Checked; + if (_processWatcher.Enabled) _processWatcher.ForceCheck(); + + if (rpc.IsInitialized != true) UpdateUIForDisconnect(); + SaveSettings(); + }; + + ToolStripSeparator separatorSettings = new(); + settingsMenu.DropDownItems.AddRange([ + runAtStartupItem, + startMinimizedItem, + autoConnectItem, + checkForUpdatesItem, + separatorSettings, + monitorItem, + notifyInBackgroundItem, + notifyOnMinimizeItem, + minimizeToTrayItem + ]); + + settingsMenu.DropDown.Closing += (s, e) => + { + if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked) e.Cancel = true; + }; + + viewMenu = new ToolStripMenuItem(I18n.T(Menu_View)); + themeMenu = new ToolStripMenuItem(I18n.T(Menu_Theme)); + themeAuto = new ToolStripMenuItem(I18n.T(Menu_ThemeAuto)); + themeLight = new ToolStripMenuItem(I18n.T(Menu_ThemeLight)); + themeDark = new ToolStripMenuItem(I18n.T(Menu_ThemeDark)); + themeOLED = new ToolStripMenuItem(I18n.T(Menu_ThemeOLED)); + + themeAuto.Click += (s, e) => { _themeManager.ApplyTheme(this, AppTheme.Auto); ThemeManager.UpdateThemeChecks(themeAuto, themeLight, themeDark, themeOLED); SaveSettings(); }; + themeLight.Click += (s, e) => { _themeManager.ApplyTheme(this, AppTheme.Light); ThemeManager.UpdateThemeChecks(themeLight, themeAuto, themeDark, themeOLED); SaveSettings(); }; + themeDark.Click += (s, e) => { _themeManager.ApplyTheme(this, AppTheme.Dark); ThemeManager.UpdateThemeChecks(themeDark, themeAuto, themeLight, themeOLED); SaveSettings(); }; + themeOLED.Click += (s, e) => { _themeManager.ApplyTheme(this, AppTheme.OLED); ThemeManager.UpdateThemeChecks(themeOLED, themeAuto, themeLight, themeDark); SaveSettings(); }; + + themeMenu.DropDownItems.AddRange([themeAuto, themeLight, themeDark, themeOLED]); + themeMenu.DropDown.Closing += (s, e) => + { + if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked) e.Cancel = true; + }; + viewMenu.DropDownItems.Add(themeMenu); + + var separator2 = new ToolStripSeparator(); + langMenu = new ToolStripMenuItem(I18n.T(Menu_Language)); + langEnglish = new ToolStripMenuItem("English"); + langJapanese = new ToolStripMenuItem("日本語"); + + langEnglish.Click += (s, e) => { I18n.CurrentLanguage = I18n.Language.English; ThemeManager.UpdateThemeChecks(langEnglish, langJapanese); ApplyLanguage(); SaveSettings(); }; + langJapanese.Click += (s, e) => { I18n.CurrentLanguage = I18n.Language.Japanese; ThemeManager.UpdateThemeChecks(langJapanese, langEnglish); ApplyLanguage(); SaveSettings(); }; + + langMenu.DropDownItems.AddRange([langEnglish, langJapanese]); + langMenu.DropDown.Closing += (s, e) => + { + if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked) e.Cancel = true; + }; + viewMenu.DropDownItems.Add(separator2); + viewMenu.DropDownItems.Add(langMenu); + + helpMenu = new ToolStripMenuItem(I18n.T(Menu_Help)); + openAppLocationMenu = new ToolStripMenuItem(I18n.T(Menu_OpenAppLocation)); + openConfigLocationMenu = new ToolStripMenuItem(I18n.T(Menu_OpenConfigLocation)); + + openAppLocationMenu.Click += (s, e) => + { + try { Process.Start("explorer.exe", $"/select,\"{Process.GetCurrentProcess().MainModule?.FileName}\""); } + catch { MessageBox.Show(I18n.T(Error_Browser)); } + }; + + openConfigLocationMenu.Click += (s, e) => + { + try + { + if (File.Exists(CONFIG_PATH)) Process.Start("explorer.exe", $"/select,\"{CONFIG_PATH}\""); + else Process.Start("explorer.exe", $"\"{Path.GetDirectoryName(CONFIG_PATH)}\""); + } + catch { MessageBox.Show(I18n.T(Error_Browser)); } + }; + + githubMenu = new ToolStripMenuItem(I18n.T(Menu_Github)); + githubMenu.Click += (s, e) => { try { Process.Start(new ProcessStartInfo("https://github.com/Wea017net/GkmStatus") { UseShellExecute = true }); } catch { MessageBox.Show(I18n.T(Error_Browser)); } }; + + checkUpdateMenuItem = new ToolStripMenuItem(I18n.T(Menu_CheckUpdateNow)); + checkUpdateMenuItem.Click += (s, e) => _ = CheckForUpdatesAsync(manual: true); + + aboutMenu = new ToolStripMenuItem(I18n.T(Menu_About)); + aboutMenu.Click += (s, e) => ShowAboutDialog(); + + var separatorHelp = new ToolStripSeparator(); + helpMenu.DropDownItems.AddRange([openAppLocationMenu, openConfigLocationMenu, separatorHelp, githubMenu, checkUpdateMenuItem, aboutMenu]); + + menu.Items.Add(fileMenu); menu.Items.Add(settingsMenu); menu.Items.Add(viewMenu); menu.Items.Add(helpMenu); + this.Controls.Add(menu); + + ApplyLanguage(); + _trayIconManager?.UpdateMenuState(rpc.Status); } - #endregion + private NotifyIcon trayIcon; + private ToolStripMenuItem trayMenuDetails, trayMenuState, trayMenuProduce; + private ToolStripMenuItem trayMenuConnect; + private ToolStripMenuItem trayMenuPause; + private ToolStripMenuItem trayMenuDisconnect; + private ComboBox cmbGameName; + private Label lblGameAppGuide; + private System.Windows.Forms.Timer gameAppGuideTimer; + private ComboBox cmbDetailsType; + private TextBox txtPName; + private NumericUpDown numPLevel; + private ComboBox cmbStateType; + private ComboBox cmbProduceCharacter; + private ComboBox cmbCharNameLang; + private TextBox txtStateCustom; + private Label lblStartTime; + private Button btnResetTime; + private Label lblResetGuide; + private System.Windows.Forms.Timer resetGuideTimer; + private ComboBox cmbBtnMode; + private Label lblBtnModeNote; + private TextBox txtBtn1Label; + private TextBox txtBtn1Url; + private TextBox txtBtn2Label; + private TextBox txtBtn2Url; + private Label lblBtnWarning; + private Button btnConnect; + private Button btnUpdate; + private Button btnDisconnect; + private Label lblStatus; + private ToolTip statusToolTip; + private ToolStripMenuItem fileMenu, exitItem; + private ToolStripMenuItem settingsMenu; + private ToolStripMenuItem runAtStartupItem; + private ToolStripMenuItem startMinimizedItem; + private ToolStripMenuItem autoConnectItem; + private ToolStripMenuItem checkForUpdatesItem; + private ToolStripMenuItem monitorItem; + private ToolStripMenuItem notifyInBackgroundItem; + private ToolStripMenuItem notifyOnMinimizeItem; + private ToolStripMenuItem minimizeToTrayItem; + private ToolStripMenuItem viewMenu; + private ToolStripMenuItem themeMenu; + private ToolStripMenuItem themeAuto; + private ToolStripMenuItem themeLight; + private ToolStripMenuItem themeDark; + private ToolStripMenuItem themeOLED; + private ToolStripMenuItem langMenu; + private ToolStripMenuItem langEnglish; + private ToolStripMenuItem langJapanese; + private ToolStripMenuItem helpMenu; + private ToolStripMenuItem openAppLocationMenu; + private ToolStripMenuItem openConfigLocationMenu; + private ToolStripMenuItem githubMenu; + private ToolStripMenuItem checkUpdateMenuItem; + private ToolStripMenuItem aboutMenu; + } -} \ No newline at end of file +} diff --git a/GkmStatus/MainForm.cs b/GkmStatus/MainForm.cs index 1879093..6917692 100644 --- a/GkmStatus/MainForm.cs +++ b/GkmStatus/MainForm.cs @@ -1,275 +1,51 @@ -using System; -using System.Drawing; -using System.Drawing.Text; -using System.Diagnostics; -using System.Windows.Forms; using DiscordRPC; -using System.IO; -using System.Runtime.InteropServices; -using System.Linq; -using System.Collections.Generic; -using System.Text.Json; -using System.Text; +using System.Diagnostics; +using System.Drawing; using System.Runtime.Versioning; +using System.Text; using Button = DiscordRPC.Button; -using Microsoft.Win32; +using static GkmStatus.src.AppConstants; +using static GkmStatus.src.Characters; +using GkmStatus.src; +using static GkmStatus.src.ui.I18n.Text_List; +using GkmStatus.src.ui; +using GkmStatus.src.native; +using static GkmStatus.src.AppSettingsHelper; namespace GkmStatus { - public class AppConfig - { - public int ConfigVersion { get; set; } = 1; - public AppSettings Settings { get; set; } = new AppSettings(); - public PresenceSettings Presence { get; set; } = new PresenceSettings(); - } - - public class AppSettings - { - public bool StartMinimized { get; set; } = false; - public bool ConnectOnStart { get; set; } = false; - public bool AutoCheckUpdates { get; set; } = true; - public bool ShowBackgroundNotifications { get; set; } = true; - public bool NotifyOnMinimize { get; set; } = true; - public bool MinimizeToTray { get; set; } = true; - public bool AutoDetectGakumas { get; set; } = true; - public string SelectedTheme { get; set; } = "自動選択"; - public string SelectedLanguage { get; set; } = "日本語"; - public DateTime LastUpdateCheck { get; set; } = DateTime.MinValue; - } - - public class PresenceSettings - { - public string DetailsType { get; set; } = "Both"; - public string ProducerName { get; set; } = ""; - public int ProducerLevel { get; set; } = 1; - public string StateType { get; set; } = "Producing"; - public int CharNameLangIndex { get; set; } = 0; - public string? SelectedIdolCharacterId { get; set; } = "hanami_saki"; - public string? SelectedProduceCharacterId { get; set; } = "hanami_saki"; - public Dictionary StateHistory { get; set; } = []; - public int GameAppIndex { get; set; } = 0; - public string ButtonMode { get; set; } = "Store"; - public Dictionary ButtonHistory { get; set; } = []; - } - - public class ProduceCharacter - { - public string Id { get; set; } = ""; - public string Display { get; set; } = ""; - public string NameEn { get; set; } = ""; - } - - public class ButtonHistoryData - { - public string L1 { get; set; } = ""; public string U1 { get; set; } = ""; - public string L2 { get; set; } = ""; public string U2 { get; set; } = ""; - } - - [SupportedOSPlatform("windows")] - public class MyRenderer(bool isBright, CustomColorTable colorTable) : ToolStripProfessionalRenderer(colorTable) - { - private readonly bool _isBright = isBright; - private readonly CustomColorTable _colorTable = colorTable; - - protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) - { - using var b = new SolidBrush(e.ToolStrip.BackColor); - e.Graphics.FillRectangle(b, 0, 0, e.ToolStrip.Width, e.ToolStrip.Height); - } - - protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) - { - using var b = new SolidBrush(e.ToolStrip.BackColor); - e.Graphics.FillRectangle(b, e.AffectedBounds); - } - - protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) - { - if (e.Image != null) - { - e.Graphics.DrawImage(e.Image, e.ImageRectangle); - } - } - - protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) - { - Rectangle r = new(Point.Empty, e.Item.Size); - r.Width -= 1; - r.Height -= 1; - - if (e.Item.Pressed) - { - using var b = new SolidBrush(_colorTable.HoverBgColor); - e.Graphics.FillRectangle(b, r); - - Color borderColor = _isBright ? Color.Gray : Color.White; - using var p = new Pen(borderColor, 1); - e.Graphics.DrawRectangle(p, r); - } - else if (e.Item.Selected) - { - using var b = new SolidBrush(_colorTable.HoverBgColor); - e.Graphics.FillRectangle(b, r); - } - } - - protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) - { - e.TextColor = _isBright ? Color.Black : Color.White; - base.OnRenderItemText(e); - } - - protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) - { - Color line = _isBright ? Color.FromArgb(180, 180, 180) : Color.FromArgb(80, 80, 80); - using var p = new Pen(line); - int y = e.Item.Height / 2; - e.Graphics.DrawLine(p, 30, y, e.Item.Width - 5, y); - } - - protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) - { - e.ArrowColor = _isBright ? Color.Black : Color.White; - base.OnRenderArrow(e); - } - } - - [SupportedOSPlatform("windows")] - public class CustomColorTable(bool isBright) : ProfessionalColorTable - { - public Color HoverBgColor { get; } = isBright ? Color.FromArgb(180, 200, 200, 200) : Color.FromArgb(60, 255, 255, 255); - public Color CustomBorderColor { get; } = isBright ? Color.Gray : Color.White; - - public override Color MenuItemSelected => HoverBgColor; - public override Color MenuItemSelectedGradientBegin => HoverBgColor; - public override Color MenuItemSelectedGradientEnd => HoverBgColor; - public override Color MenuItemPressedGradientBegin => HoverBgColor; - public override Color MenuItemPressedGradientEnd => HoverBgColor; - public override Color MenuItemPressedGradientMiddle => HoverBgColor; - public override Color MenuItemBorder => CustomBorderColor; - public override Color MenuBorder => CustomBorderColor; - } - [SupportedOSPlatform("windows")] public partial class MainForm : Form { - [LibraryImport("user32.dll", EntryPoint = "SendMessageW", StringMarshalling = StringMarshalling.Utf16)] - private static partial IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, string lParam); - private const int EM_SETCUEBANNER = 0x1501; - - [LibraryImport("dwmapi.dll")] - private static partial int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); - - [LibraryImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool SetForegroundWindow(IntPtr hWnd); - - [LibraryImport("gdi32.dll")] - private static partial IntPtr AddFontMemResourceEx(IntPtr pbFont, uint cbFont, IntPtr pdv, out uint pcFonts); - - - private const string PROCESS_NAME = "gakumas"; - private static readonly List<(string Name, string AppId)> GameApps = - [ - ("学園アイドルマスター", "1352261574877778001"), - ("学マス", "1467733389170835486"), - ("THE IDOLM@STER Gakuen", "1467733691382890499"), - ("Gakuen iDOLM@STER", "1467734377197867040"), - ("Gakumas", "1467734892208193650") - ]; - private const string REG_RUN_KEY = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; - private const string APP_NAME = "GkmStatus"; - private readonly string configPath; - private static readonly JsonSerializerOptions jsonOptions = new() { WriteIndented = true, AllowTrailingCommas = true, PropertyNameCaseInsensitive = true }; - - private static readonly Color COLOR_ERROR = ColorTranslator.FromHtml("#fc5555"); - private static readonly Color COLOR_CONNECT = ColorTranslator.FromHtml("#68b900"); - private static readonly Color COLOR_PAUSE = ColorTranslator.FromHtml("#fc930f"); - private static readonly Color COLOR_PRIMARY = ColorTranslator.FromHtml("#7e87f4"); - private static readonly Color COLOR_TEXT_WHITE = Color.FromArgb(255, 255, 255); - private static readonly Color COLOR_DISABLED = Color.FromArgb(60, 63, 65); - - private static readonly List ProduceCharacters = - [ - new() { Id = "hanami_saki", Display = "花海咲季", NameEn = "Saki Hanami" }, - new() { Id = "tsukimura_temari", Display = "月村手毬", NameEn = "Temari Tsukimura" }, - new() { Id = "fujita_kotone", Display = "藤田ことね", NameEn = "Kotone Fujita" }, - new() { Id = "amaya_tsubame", Display = "雨夜燕", NameEn = "Tsubame Amaya" }, - new() { Id = "arimura_mao", Display = "有村麻央", NameEn = "Mao Arimura" }, - new() { Id = "katsuragi_lilja", Display = "葛城リーリヤ", NameEn = "Lilja Katsuragi" }, - new() { Id = "kuramoto_china", Display = "倉本千奈", NameEn = "China Kuramoto" }, - new() { Id = "shiun_sumika", Display = "紫雲清夏", NameEn = "Sumika Shiun" }, - new() { Id = "shinosawa_hiro", Display = "篠澤広", NameEn = "Hiro Shinosawa" }, - new() { Id = "juo_sena", Display = "十王星南", NameEn = "Sena Juo" }, - new() { Id = "hataya_misuzu", Display = "秦谷美鈴", NameEn = "Misuzu Hataya" }, - new() { Id = "hanami_ume", Display = "花海佑芽", NameEn = "Ume Hanami" }, - new() { Id = "himesaki_rinami", Display = "姫崎莉波", NameEn = "Rinami Himesaki" } - ]; - private readonly bool isInitializing = true; - - private ComboBox cmbGameName = null!; - private ComboBox cmbDetailsType = null!; - private TextBox txtPName = null!; - private NumericUpDown numPLevel = null!; - private ComboBox cmbStateType = null!; - private ComboBox cmbProduceCharacter = null!; - private ComboBox cmbCharNameLang = null!; - private TextBox txtStateCustom = null!; - private Label lblStartTime = null!; - private System.Windows.Forms.Button btnResetTime = null!; - private ComboBox cmbBtnMode = null!; - private TextBox txtBtn1Label = null!, txtBtn1Url = null!; - private TextBox txtBtn2Label = null!, txtBtn2Url = null!; - private System.Windows.Forms.Label lblStatus = null!; - private Label lblGameAppGuide = null!; - private System.Windows.Forms.Timer gameAppGuideTimer = null!; - private System.Windows.Forms.Button btnConnect = null!, btnDisconnect = null!, btnUpdate = null!; - private Label lblResetGuide = null!; - private Label lblBtnWarning = null!; - private Label lblBtnModeNote = null!; - private ToolTip statusToolTip = null!; - private System.Windows.Forms.Timer resetGuideTimer = null!; private int footerButtonsY; + private bool _isResettingDefaults; - private ToolStripMenuItem fileMenu = null!, exitItem = null!, settingsMenu = null!, runAtStartupItem = null!, startMinimizedItem = null!, autoConnectItem = null!, checkForUpdatesItem = null!, notifyInBackgroundItem = null!, notifyOnMinimizeItem = null!, minimizeToTrayItem = null!, monitorItem = null!; - private ToolStripMenuItem viewMenu = null!, themeMenu = null!, langMenu = null!, helpMenu = null!, githubMenu = null!, checkUpdateMenuItem = null!, aboutMenu = null!, openAppLocationMenu = null!, openConfigLocationMenu = null!; - private ToolStripMenuItem langEnglish = null!, langJapanese = null!; - private ToolStripMenuItem themeAuto = null!, themeLight = null!, themeDark = null!, themeOLED = null!; - private ToolStripMenuItem trayMenuConnect = null!, trayMenuPause = null!, trayMenuDisconnect = null!, trayMenuProduce = null!, trayMenuDetails = null!, trayMenuState = null!; - - private DiscordRpcClient? client; + private readonly DiscordRpcService rpc = new(); private bool isManualPaused = false; private DateTime startTime; - private System.Windows.Forms.Timer? monitorTimer; private System.Windows.Forms.Timer? connectionTimer; private System.Windows.Forms.Timer? clockTimer; private int connectionSeconds = 0; private const int CONNECTION_TIMEOUT = 30; - private bool wasProcessRunning = false; - private Font appFont = null!; - private Font appFontBold = null!; - private Font appFontMedium = null!; - private Font menuFont = null!; - private Color defaultBackground; private int lastStateIdx = -1; private int lastBtnIdx = -1; - private PresenceSettings currentPresence = new(); - private NotifyIcon trayIcon = null!; - private Icon? currentTrayIconStatus; private DateTime lastUpdateCheck = DateTime.MinValue; - private PrivateFontCollection? pfc; - private readonly List fontPointers = []; + private readonly float uiScale = 1.0f; - private float uiScale = 1.0f; - private bool isInternalMinimize = false; + private readonly UpdateService _updateService = new(); + private readonly ConfigManager _configManager = new(); + private readonly FontManager _fontManager = new(); + private readonly ThemeManager _themeManager; + private readonly ProcessWatcher _processWatcher; + private readonly TrayIconManager _trayIconManager; + private PresenceSettings CurrentPresence => _configManager.Config.Presence; public MainForm() { - InitializeComponent(); try { uiScale = (float)this.DeviceDpi / 96f; @@ -280,784 +56,182 @@ public MainForm() uiScale = 1.0f; } - SetupFonts(); - InitializeCustomUI(); - string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GkmStatus"); - configPath = Path.Combine(appDataPath, "config.json"); + SuspendLayout(); + //InitializeComponent(); + _fontManager.Initialize(uiScale); + _themeManager = new ThemeManager(_fontManager); + ApplyComponent(); + ResumeLayout(); - isInitializing = true; - LoadSettings(); + _trayIconManager = new TrayIconManager(this.trayIcon, this.Icon!); - if (startMinimizedItem.Checked) - { - this.WindowState = FormWindowState.Minimized; - } - - UpdateDetailsInputs(null, EventArgs.Empty); - CmbStateType_SelectedIndexChanged(null, EventArgs.Empty); - CmbBtnMode_SelectedIndexChanged(null, EventArgs.Empty); - - isInitializing = false; - InitializeLogic(); - - this.FormClosing += (s, e) => - { - if (minimizeToTrayItem?.Checked == true && e.CloseReason == CloseReason.UserClosing) - { - e.Cancel = true; - isInternalMinimize = true; - this.WindowState = FormWindowState.Minimized; - isInternalMinimize = false; - return; - } - if (trayIcon != null) { trayIcon.Visible = false; trayIcon.Dispose(); } - SaveSettings(); - foreach (var ptr in fontPointers) Marshal.FreeCoTaskMem(ptr); - pfc?.Dispose(); - }; - - } - - private int S(int value) => (int)Math.Round(value * uiScale); - - protected override void OnDpiChanged(DpiChangedEventArgs e) - { - float ratio = (float)e.DeviceDpiNew / e.DeviceDpiOld; - uiScale = (float)e.DeviceDpiNew / 96f; - - base.OnDpiChanged(e); - - this.Scale(new SizeF(ratio, ratio)); - - this.ClientSize = new Size(S(520), S(630)); - footerButtonsY = this.ClientSize.Height - S(80); - - SetupFonts(); - SetThemeColors(this.BackColor); - AdjustStatusVerticalPosition(); - } - - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - if (checkForUpdatesItem.Checked) _ = CheckForUpdatesAsync(); - if (startMinimizedItem.Checked) - { - this.Hide(); - trayIcon.Visible = true; - } - } - - protected override void OnResize(EventArgs e) - { - base.OnResize(e); - if (this.WindowState == FormWindowState.Minimized) - { - if (minimizeToTrayItem?.Checked == true && !isInternalMinimize) return; - - this.Hide(); - trayIcon.Visible = true; - if (notifyOnMinimizeItem?.Checked == true) - { - trayIcon.Tag = null; - trayIcon.ShowBalloonTip(2000, I18n.T("App_Name"), I18n.T("Notify_Minimized"), ToolTipIcon.Info); - } - } - } - - private void RestoreFromTray() - { - this.Show(); - this.WindowState = FormWindowState.Normal; - trayIcon.Visible = false; - this.Activate(); - ReapplyTheme(); - } - - private void ReapplyTheme() - { - if (themeLight.Checked) ApplyThemeLight(); - else if (themeDark.Checked) ApplyThemeDark(); - else if (themeOLED.Checked) ApplyThemeOLED(); - else ApplyThemeAuto(); - } - - private void SetupFonts() - { - try - { - if (pfc == null) - { - pfc = new PrivateFontCollection(); - var assembly = System.Reflection.Assembly.GetExecutingAssembly(); - - try { Debug.WriteLine("Manifest resources: " + string.Join(", ", assembly.GetManifestResourceNames())); } catch { } - - var manifestNames = assembly.GetManifestResourceNames(); - var ttfCandidates = manifestNames.Where(n => n.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) && n.Contains("plex", StringComparison.OrdinalIgnoreCase)).ToArray(); - - if (ttfCandidates.Length == 0) - { - ttfCandidates = [ - "GkmStatus.Resources.fonts.IBM_Plex_Sans_JP.IBMPlexSansJP-Regular.ttf", - "GkmStatus.Resources.fonts.IBM_Plex_Sans_JP.IBMPlexSansJP-Bold.ttf", - "GkmStatus.Resources.fonts.IBM_Plex_Sans_JP.IBMPlexSansJP-Medium.ttf" - ]; - } - - foreach (string resourceName in ttfCandidates) - { - using Stream? stream = assembly.GetManifestResourceStream(resourceName); - if (stream != null) - { - try - { - byte[] fontData = new byte[stream.Length]; - stream.ReadExactly(fontData, 0, (int)stream.Length); - IntPtr fontPtr = Marshal.AllocCoTaskMem(fontData.Length); - Marshal.Copy(fontData, 0, fontPtr, fontData.Length); - - pfc.AddMemoryFont(fontPtr, fontData.Length); - - AddFontMemResourceEx(fontPtr, (uint)fontData.Length, IntPtr.Zero, out uint _); - - fontPointers.Add(fontPtr); - Debug.WriteLine("Loaded and registered font resource: " + resourceName); - } - catch (Exception ex) { Debug.WriteLine("Failed loading font resource '" + resourceName + "': " + ex.Message); } - } - else - { - Debug.WriteLine("Font resource not found: " + resourceName); - } - } - } - - FontFamily? regular = null, bold = null, medium = null; - try { Debug.WriteLine("PrivateFontCollection families: " + string.Join(", ", pfc.Families.Select(f => f.Name))); } catch { } - foreach (var ff in pfc.Families) - { - string name = ff.Name; - if (name.Contains("IBM Plex Sans JP", StringComparison.OrdinalIgnoreCase)) - { - if (name.Contains("Bold", StringComparison.OrdinalIgnoreCase)) bold = ff; - else if (name.Contains("Medium", StringComparison.OrdinalIgnoreCase)) medium = ff; - else regular = ff; - } - } - - if (regular == null && pfc.Families.Length > 0) - { - regular = pfc.Families.FirstOrDefault(f => f.Name.Contains("IBM Plex Sans JP", StringComparison.OrdinalIgnoreCase)) - ?? pfc.Families[0]; - } - - if (regular != null) - { - float basePx = 13.3f * uiScale; - appFont = new Font(regular, basePx, GraphicsUnit.Pixel); - - if (bold != null) appFontBold = new Font(bold, basePx, GraphicsUnit.Pixel); - else appFontBold = new Font(regular, basePx, FontStyle.Bold, GraphicsUnit.Pixel); - - if (medium != null) appFontMedium = new Font(medium, basePx, GraphicsUnit.Pixel); - else appFontMedium = appFontBold; - - SetupMenuFont(); - return; - } - } - catch (Exception ex) - { - Debug.WriteLine("Font Load Error: " + ex.Message); - } - - - float basePxFallback = 13.3f * uiScale; - appFont = new Font("Meiryo UI", basePxFallback, GraphicsUnit.Pixel); - appFontBold = new Font("Meiryo UI", basePxFallback, FontStyle.Bold, GraphicsUnit.Pixel); - appFontMedium = new Font("Meiryo UI", basePxFallback, GraphicsUnit.Pixel); - - SetupMenuFont(); - } - - private void SetupMenuFont() - { - float menuPx = 12f * uiScale; - try - { - using var testFont = new Font("Yu Gothic UI", menuPx, GraphicsUnit.Pixel); - if (testFont.Name == "Yu Gothic UI") - { - menuFont = new Font("Yu Gothic UI", menuPx, GraphicsUnit.Pixel); - } - else - { - var family = SystemFonts.MessageBoxFont?.FontFamily ?? FontFamily.GenericSansSerif; - menuFont = new Font(family, menuPx, GraphicsUnit.Pixel); - } - } - catch - { - var family = SystemFonts.MessageBoxFont?.FontFamily ?? FontFamily.GenericSansSerif; - menuFont = new Font(family, menuPx, GraphicsUnit.Pixel); - } - } - - private void InitializeCustomUI() - { - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; - - this.ClientSize = new Size(S(520), S(630)); - this.FormBorderStyle = FormBorderStyle.FixedSingle; - - this.Text = I18n.T("App_Name"); - this.MaximizeBox = false; - this.StartPosition = FormStartPosition.CenterScreen; - this.BackColor = Color.FromArgb(32, 34, 37); - this.ForeColor = Color.White; - this.Font = appFont; - defaultBackground = this.BackColor; - - try - { - var assembly = System.Reflection.Assembly.GetExecutingAssembly(); - using Stream? stream = assembly.GetManifestResourceStream("GkmStatus.Resources.app.ico"); - if (stream != null) - { - this.Icon = new Icon(stream); - } - } - catch (Exception ex) - { - Debug.WriteLine("Icon Load Error: " + ex.Message); - } - - trayIcon = new NotifyIcon - { - Icon = this.Icon ?? SystemIcons.Application, - Text = I18n.T("App_Name"), - Visible = false - }; - - trayIcon.MouseClick += (s, e) => - { - if (e.Button == MouseButtons.Left) RestoreFromTray(); - }; - trayIcon.BalloonTipClicked += (s, e) => - { - if (trayIcon.Tag is string url && !string.IsNullOrEmpty(url)) - { - try { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } catch { } - trayIcon.Tag = null; - } - else - { - this.Invoke((MethodInvoker)(() => RestoreFromTray())); - } - }; - - var trayMenu = new ContextMenuStrip(); - trayMenu.Opening += (s, e) => - { - UpdateTrayMenuState(); - SetForegroundWindow(this.Handle); - if (trayMenuProduce != null) - { - string stateStr = GetStateString(cmbStateType.SelectedIndex); - trayMenuProduce.Enabled = (stateStr == "Idol" || stateStr == "Producing"); - trayMenuProduce.Text = (stateStr == "Idol") ? I18n.T("State_Idol") : I18n.T("Tray_ProducingIdol"); - } - }; - trayMenu.Items.Add(I18n.T("Tray_Open"), null, (s, e) => RestoreFromTray()); - trayMenu.Items.Add(new ToolStripSeparator()); - - trayMenuDetails = new ToolStripMenuItem(I18n.T("Header_Details")); - trayMenuDetails.DropDownItems.Add(new ToolStripMenuItem("...")); - trayMenuDetails.DropDownOpening += (s, e) => - { - trayMenuDetails.DropDownItems.Clear(); - for (int i = 0; i < cmbDetailsType.Items.Count; i++) - { - string text = cmbDetailsType.Items[i]?.ToString() ?? ""; - var item = new ToolStripMenuItem(text); - if (cmbDetailsType.SelectedIndex == i) item.Checked = true; - int index = i; - item.Click += (sender, ev) => - { - cmbDetailsType.SelectedIndex = index; - UpdateRpc(); - if (notifyInBackgroundItem?.Checked == true) - { - trayIcon.Tag = null; - trayIcon.ShowBalloonTip(3000, I18n.T("App_Name"), I18n.T("Notify_TrayDetailsChanged"), ToolTipIcon.Info); - } - }; - trayMenuDetails.DropDownItems.Add(item); - } - }; - - trayMenuState = new ToolStripMenuItem(I18n.T("Header_State")); - trayMenuState.DropDownItems.Add(new ToolStripMenuItem("...")); - trayMenuState.DropDownOpening += (s, e) => - { - trayMenuState.DropDownItems.Clear(); - for (int i = 0; i < cmbStateType.Items.Count; i++) - { - string text = cmbStateType.Items[i]?.ToString() ?? ""; - var item = new ToolStripMenuItem(text); - if (cmbStateType.SelectedIndex == i) item.Checked = true; - int index = i; - item.Click += (sender, ev) => - { - cmbStateType.SelectedIndex = index; - UpdateRpc(); - if (trayMenuProduce != null) - { - string stateStr = GetStateString(index); - trayMenuProduce.Enabled = (stateStr == "Idol" || stateStr == "Producing"); - trayMenuProduce.Text = (stateStr == "Idol") ? I18n.T("State_Idol") : I18n.T("Tray_ProducingIdol"); - } - - if (notifyInBackgroundItem?.Checked == true) - { - trayIcon.Tag = null; - trayIcon.ShowBalloonTip(3000, I18n.T("App_Name"), I18n.T("Notify_TrayStateChanged"), ToolTipIcon.Info); - } - }; - trayMenuState.DropDownItems.Add(item); - } - }; - - trayMenuProduce = new ToolStripMenuItem(I18n.T("Tray_ProducingIdol")); - trayMenuProduce.DropDownItems.Add(new ToolStripMenuItem("...")); - trayMenuProduce.DropDownOpening += (s, e) => - { - trayMenuProduce.DropDownItems.Clear(); - string? currentId = (GetStateString(cmbStateType.SelectedIndex) == "Idol") ? currentPresence.SelectedIdolCharacterId : currentPresence.SelectedProduceCharacterId; - - foreach (var pc in ProduceCharacters) - { - string displayName = currentPresence.CharNameLangIndex == 1 ? pc.NameEn : pc.Display; - var item = new ToolStripMenuItem(displayName); - if (currentId == pc.Id) item.Checked = true; - item.Click += (sender, ev) => - { - int index = ProduceCharacters.IndexOf(pc); - if (index >= 0 && index < cmbProduceCharacter.Items.Count) - { - cmbProduceCharacter.SelectedIndex = index; - } - UpdateRpc(); - - if (notifyInBackgroundItem?.Checked == true) - { - trayIcon.Tag = null; - trayIcon.ShowBalloonTip(3000, I18n.T("App_Name"), I18n.T("Notify_TrayIdolChanged"), ToolTipIcon.Info); - } - }; - trayMenuProduce.DropDownItems.Add(item); - } - }; - - trayMenu.Items.Add(trayMenuDetails); - trayMenu.Items.Add(trayMenuState); - trayMenu.Items.Add(trayMenuProduce); - - var traySepSettings = new ToolStripSeparator { Tag = "SepSettings" }; - trayMenu.Items.Add(traySepSettings); - - trayMenuConnect = new ToolStripMenuItem(I18n.T("Button_Connect"), null, (s, e) => InitializeRpc()); - trayMenuPause = new ToolStripMenuItem(I18n.T("Button_Pause"), null, (s, e) => PauseRpc()); - trayMenuDisconnect = new ToolStripMenuItem(I18n.T("Button_Disconnect"), null, (s, e) => DisposeRpc()); - - trayMenu.Items.AddRange([trayMenuConnect, trayMenuPause, trayMenuDisconnect]); - - trayMenu.Items.Add(new ToolStripSeparator()); - trayMenu.Items.Add(I18n.T("Tray_Exit"), null, (s, e) => Application.Exit()); - - trayIcon.ContextMenuStrip = trayMenu; - - int y = S(40); - void AddHeader(string text, int top, string tag) - { - var lbl = new Label { Text = text, Location = new Point(S(20), top), AutoSize = true, Font = appFontBold, ForeColor = Color.LightGray, Tag = tag }; - this.Controls.Add(lbl); - } - - AddHeader(I18n.T("Header_GameName"), y, "Header_GameName"); y += S(25); - cmbGameName = CreateCombo(new(S(20), y), S(210), [.. GameApps.Select(g => g.Name)]); - cmbGameName.SelectedIndex = 0; - cmbGameName.SelectedIndexChanged += CmbGameName_SelectedIndexChanged; - - lblGameAppGuide = new Label - { - Text = I18n.T("GameApp_Guide"), - Location = new Point(S(240), y - S(7)), - AutoSize = true, - Visible = false, - ForeColor = COLOR_PAUSE, - Font = appFontMedium - }; - this.Controls.Add(lblGameAppGuide); - - gameAppGuideTimer = new System.Windows.Forms.Timer { Interval = 5000 }; - gameAppGuideTimer.Tick += (s, e) => - { - lblGameAppGuide.Visible = false; - gameAppGuideTimer.Stop(); - }; - - y += S(45); - - AddHeader(I18n.T("Header_Details"), y, "Header_Details"); y += S(25); - cmbDetailsType = CreateCombo(new Point(S(20), y), S(180), [I18n.T("Details_None"), I18n.T("Details_PName"), I18n.T("Details_PLv"), I18n.T("Details_Both")]); - cmbDetailsType.SelectedIndex = 3; cmbDetailsType.SelectedIndexChanged += UpdateDetailsInputs; - txtPName = CreateText(new Point(S(210), y), S(200)); SetPlaceholder(txtPName, I18n.T("Placeholder_PName")); - txtPName.MaxLength = 10; - txtPName.TextChanged += (s, e) => { if (!isInitializing) SaveSettings(); }; - numPLevel = CreateNumeric(new Point(S(420), y), S(80)); - numPLevel.ValueChanged += (s, e) => { if (!isInitializing) SaveSettings(); }; - y += S(45); - - AddHeader(I18n.T("Header_State"), y, "Header_State"); y += S(25); - - cmbStateType = CreateCombo( - new(S(20), y), - S(150), - [I18n.T("State_None"), I18n.T("State_PID"), I18n.T("State_Idol"), I18n.T("State_Producing"), I18n.T("State_Custom")] - ); - - cmbProduceCharacter = CreateCombo( - new(S(180), y), - S(165), - [] - ); - cmbProduceCharacter.Visible = false; - - cmbCharNameLang = CreateCombo( - new Point(S(350), y), - S(150), - [I18n.T("CharName_JP"), I18n.T("CharName_EN")] - ); - cmbCharNameLang.Visible = false; - cmbCharNameLang.SelectedIndex = 0; - cmbCharNameLang.SelectedIndexChanged += (s, e) => - { - if (!isInitializing) - { - currentPresence.CharNameLangIndex = cmbCharNameLang.SelectedIndex; - RefreshProduceCharacterList(); - SaveSettings(); - } - }; - - foreach (var c in ProduceCharacters) - { - cmbProduceCharacter.Items.Add(c.Display); - } - cmbProduceCharacter.SelectedIndex = 0; - - cmbProduceCharacter.SelectedIndexChanged += (s, e) => - { - if (cmbProduceCharacter.SelectedIndex >= 0) - { - string characterId = ProduceCharacters[cmbProduceCharacter.SelectedIndex].Id; - string stateStr = GetStateString(cmbStateType.SelectedIndex); - if (stateStr == "Idol") currentPresence.SelectedIdolCharacterId = characterId; - else if (stateStr == "Producing") currentPresence.SelectedProduceCharacterId = characterId; - - if (!isInitializing) SaveSettings(); - } - }; - - txtStateCustom = CreateText(new Point(S(180), y), S(320)); txtStateCustom.Enabled = false; - cmbStateType.SelectedIndexChanged += CmbStateType_SelectedIndexChanged; - y += S(45); - - AddHeader(I18n.T("Header_Timestamp"), y, "Header_Timestamp"); y += S(25); - lblStartTime = new Label { Text = I18n.T("Timestamp_Label") + ": 00:00:00", Location = new Point(S(20), y + S(7)), AutoSize = true }; - this.Controls.Add(lblStartTime); - btnResetTime = CreateButton(I18n.T("Timestamp_Reset"), new Point(S(170), y), S(120), COLOR_PRIMARY); - btnResetTime.Click += (s, e) => + _processWatcher = new ProcessWatcher(PROCESS_NAME); + _processWatcher.ProcessStarted += (s, e) => { startTime = DateTime.UtcNow; UpdateTimestampLabel(); - lblResetGuide.Visible = true; - resetGuideTimer.Stop(); - resetGuideTimer.Start(); - if (client?.IsInitialized == true) UpdateRpc(); + InitializeRpc(); }; - y += S(45); - lblResetGuide = new Label + _processWatcher.ProcessStopped += (s, e) => { - Text = I18n.T("Timestamp_Guide"), - Location = new Point(S(300), y - S(38)), - AutoSize = true, - Visible = false, - ForeColor = Color.Gray, - Font = appFontMedium + PauseRpc(); }; - this.Controls.Add(lblResetGuide); - - resetGuideTimer = new System.Windows.Forms.Timer { Interval = 2000 }; - resetGuideTimer.Tick += (s, e) => - { - lblResetGuide.Visible = false; - resetGuideTimer.Stop(); - }; - - AddHeader(I18n.T("Header_Buttons"), y, "Header_Buttons"); y += S(25); - cmbBtnMode = CreateCombo(new Point(S(20), y), S(150), [I18n.T("Button_ModeNone"), I18n.T("Button_ModeStore"), I18n.T("Button_ModeApp"), I18n.T("Button_ModeCustom")]); - cmbBtnMode.SelectedIndexChanged += CmbBtnMode_SelectedIndexChanged; - - lblBtnModeNote = new Label - { - Text = I18n.T("Button_ModeNote"), - Location = new Point(S(180), y + S(3)), - AutoSize = true, - Visible = false, - ForeColor = Color.Gray, - Font = appFontMedium - }; - this.Controls.Add(lblBtnModeNote); - - y += S(35); - txtBtn1Label = CreateText(new Point(S(20), y), S(150)); txtBtn1Label.MaxLength = 31; - txtBtn1Url = CreateText(new Point(S(180), y), S(320)); txtBtn1Url.MaxLength = 512; - y += S(35); - txtBtn2Label = CreateText(new Point(S(20), y), S(150)); txtBtn2Label.MaxLength = 31; - txtBtn2Url = CreateText(new Point(S(180), y), S(320)); txtBtn2Url.MaxLength = 512; - - lblBtnWarning = new Label - { - Text = "", - Location = new Point(S(20), y + S(40)), - AutoSize = true, - Visible = false, - ForeColor = COLOR_PAUSE, - Font = appFontMedium - }; - this.Controls.Add(lblBtnWarning); - - void CheckBtnWarning(object? sender, EventArgs e) - { - bool labelOver = Encoding.UTF8.GetByteCount(txtBtn1Label.Text) > 32 || Encoding.UTF8.GetByteCount(txtBtn2Label.Text) > 32; - bool urlJapanese = JapaneseRegex().IsMatch(txtBtn1Url.Text + txtBtn2Url.Text); - - var sb = new StringBuilder(); - if (labelOver) sb.AppendLine(I18n.T("Button_Warning_LabelLength")); - if (urlJapanese) sb.Append(I18n.T("Button_Warning_UrlJp")); - string msg = sb.ToString().Trim(); - lblBtnWarning.Text = msg; - lblBtnWarning.Visible = !string.IsNullOrEmpty(msg); - - if (urlJapanese) lblBtnWarning.ForeColor = COLOR_ERROR; - else lblBtnWarning.ForeColor = COLOR_PAUSE; - } - txtBtn1Label.TextChanged += CheckBtnWarning; - txtBtn2Label.TextChanged += CheckBtnWarning; - txtBtn1Url.TextChanged += CheckBtnWarning; - txtBtn2Url.TextChanged += CheckBtnWarning; - - y += S(50); - - y = this.ClientSize.Height - S(80); - footerButtonsY = y; - this.Controls.Add(new Label { BorderStyle = BorderStyle.Fixed3D, Location = new Point(0, y), Size = new Size(S(520), S(2)) }); - y += S(20); - - btnConnect = CreateButton(I18n.T("Button_Connect"), new Point(S(20), y), S(100), COLOR_CONNECT); - btnConnect.Click += (s, e) => { if (client?.IsInitialized == true && !isManualPaused) PauseRpc(); else InitializeRpc(); }; - btnUpdate = CreateButton(I18n.T("Button_Update"), new Point(S(130), y), S(100), COLOR_PRIMARY); - btnUpdate.Enabled = false; btnUpdate.Click += async (s, e) => + rpc.Ready += (s, username) => { - UpdateRpc(); - string originalText = lblStatus.Text; - Color originalColor = lblStatus.ForeColor; - lblStatus.Text = I18n.T("Status_Updated"); - lblStatus.ForeColor = COLOR_PRIMARY; - AdjustStatusVerticalPosition(); - - await System.Threading.Tasks.Task.Delay(2000); - - if (this.IsDisposed) return; - if (lblStatus.Text == I18n.T("Status_Updated")) + SafeBeginInvoke(() => { - lblStatus.Text = originalText; - lblStatus.ForeColor = originalColor; - AdjustStatusVerticalPosition(); - } - }; - btnDisconnect = CreateButton(I18n.T("Button_Disconnect"), new Point(S(240), y), S(100), COLOR_ERROR); - btnDisconnect.Enabled = false; btnDisconnect.Click += (s, e) => DisposeRpc(); - - lblStatus = new Label { Text = I18n.T("Status_Disconnected"), Location = new Point(S(352), y), AutoSize = true, ForeColor = Color.Gray, Font = appFontMedium }; - statusToolTip = new ToolTip(); - statusToolTip.SetToolTip(lblStatus, lblStatus.Text); - this.Controls.Add(btnConnect); this.Controls.Add(btnUpdate); this.Controls.Add(btnDisconnect); this.Controls.Add(lblStatus); - - - AdjustStatusVerticalPosition(); - - var menu = new MenuStrip(); - fileMenu = new(I18n.T("Menu_File")); - exitItem = new(I18n.T("Menu_Exit")) { ShortcutKeyDisplayString = "Alt+F4" }; - exitItem.Click += (s, e) => { Application.Exit(); }; - fileMenu.DropDownItems.Add(exitItem); - - settingsMenu = new(I18n.T("Menu_Settings")); - runAtStartupItem = new(I18n.T("Menu_RunAtStartup")) { CheckOnClick = true, Checked = IsRunAtStartup() }; - runAtStartupItem.CheckedChanged += (s, e) => SetRunAtStartup(runAtStartupItem.Checked); - - startMinimizedItem = new(I18n.T("Menu_StartMinimized")) { CheckOnClick = true }; - startMinimizedItem.CheckedChanged += (s, e) => SaveSettings(); - - autoConnectItem = new(I18n.T("Menu_AutoConnect")) { CheckOnClick = true }; - autoConnectItem.CheckedChanged += (s, e) => SaveSettings(); - - checkForUpdatesItem = new(I18n.T("Menu_CheckForUpdates")) { CheckOnClick = true, Checked = true }; - checkForUpdatesItem.CheckedChanged += (s, e) => SaveSettings(); - - notifyInBackgroundItem = new(I18n.T("Menu_NotifyInBackground")) { CheckOnClick = true, Checked = true }; - notifyInBackgroundItem.CheckedChanged += (s, e) => SaveSettings(); - - notifyOnMinimizeItem = new(I18n.T("Menu_NotifyOnMinimize")) { CheckOnClick = true, Checked = true }; - notifyOnMinimizeItem.CheckedChanged += (s, e) => SaveSettings(); - - minimizeToTrayItem = new(I18n.T("Menu_MinimizeToTray")) { CheckOnClick = true, Checked = true }; - minimizeToTrayItem.CheckedChanged += (s, e) => SaveSettings(); - - monitorItem = new(I18n.T("Menu_MonitorProcess")) { CheckOnClick = true, Checked = true }; - monitorItem.CheckedChanged += (s, e) => { if (monitorTimer != null) monitorTimer.Enabled = monitorItem.Checked; if (client?.IsInitialized != true) UpdateUIForDisconnect(); SaveSettings(); }; + connectionTimer?.Stop(); + UpdateUIForConnected(username); - ToolStripSeparator separatorSettings = new(); - settingsMenu.DropDownItems.AddRange([ - runAtStartupItem, - startMinimizedItem, - autoConnectItem, - checkForUpdatesItem, - separatorSettings, - monitorItem, - notifyInBackgroundItem, - notifyOnMinimizeItem, - minimizeToTrayItem - ]); + System.Threading.Tasks.Task.Delay(500).ContinueWith(_ => + { + SafeBeginInvoke(UpdateRpc); + }); + }); + }; - settingsMenu.DropDown.Closing += (s, e) => + rpc.StatusChanged += (s, status) => { - if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked) e.Cancel = true; + SafeBeginInvoke(() => + { + if (status == RpcStatus.Error) + { + HandleConnectionError(rpc.LastErrorMessage ?? "Unknown RPC Error"); + } + else if (status == RpcStatus.Disconnected) + { + UpdateUIForDisconnect(); + } + }); }; - viewMenu = new ToolStripMenuItem(I18n.T("Menu_View")); - themeMenu = new ToolStripMenuItem(I18n.T("Menu_Theme")); - themeAuto = new ToolStripMenuItem(I18n.T("Menu_ThemeAuto")); - themeLight = new ToolStripMenuItem("ライト"); - themeDark = new ToolStripMenuItem("ダーク"); - themeOLED = new ToolStripMenuItem("OLED"); - themeAuto.Click += (s, e) => { ApplyThemeAuto(); UpdateThemeChecks(themeAuto, themeLight, themeDark, themeOLED); SaveSettings(); }; - themeLight.Click += (s, e) => { ApplyThemeLight(); UpdateThemeChecks(themeLight, themeAuto, themeDark, themeOLED); SaveSettings(); }; - themeDark.Click += (s, e) => { ApplyThemeDark(); UpdateThemeChecks(themeDark, themeAuto, themeLight, themeOLED); SaveSettings(); }; - themeOLED.Click += (s, e) => { ApplyThemeOLED(); UpdateThemeChecks(themeOLED, themeAuto, themeLight, themeDark); SaveSettings(); }; + isInitializing = true; + _configManager.Load(); + LoadSettings(); - themeMenu.DropDownItems.AddRange([themeAuto, themeLight, themeDark, themeOLED]); - themeMenu.DropDown.Closing += (s, e) => + if (startMinimizedItem.Checked) { - if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked) e.Cancel = true; - }; - viewMenu.DropDownItems.Add(themeMenu); + this.WindowState = FormWindowState.Minimized; + } + + UpdateDetailsInputs(null, EventArgs.Empty); + CmbStateType_SelectedIndexChanged(null, EventArgs.Empty); + CmbBtnMode_SelectedIndexChanged(null, EventArgs.Empty); - var separator2 = new ToolStripSeparator(); - langMenu = new ToolStripMenuItem(I18n.T("Menu_Language")); - langEnglish = new ToolStripMenuItem("English"); - langJapanese = new ToolStripMenuItem("日本語"); + isInitializing = false; + InitializeLogic(); - langEnglish.Click += (s, e) => { I18n.CurrentLanguage = "English"; UpdateThemeChecks(langEnglish, langJapanese); ApplyLanguage(); SaveSettings(); }; - langJapanese.Click += (s, e) => { I18n.CurrentLanguage = "日本語"; UpdateThemeChecks(langJapanese, langEnglish); ApplyLanguage(); SaveSettings(); }; - langMenu.DropDownItems.AddRange([langEnglish, langJapanese]); - langMenu.DropDown.Closing += (s, e) => + this.FormClosing += (s, e) => { - if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked) e.Cancel = true; - }; - viewMenu.DropDownItems.Add(separator2); - viewMenu.DropDownItems.Add(langMenu); + if (minimizeToTrayItem?.Checked == true && e.CloseReason == CloseReason.UserClosing) + { + e.Cancel = true; + this.Hide(); + this.ShowInTaskbar = false; - helpMenu = new ToolStripMenuItem(I18n.T("Menu_Help")); - openAppLocationMenu = new ToolStripMenuItem(I18n.T("Menu_OpenAppLocation")); - openConfigLocationMenu = new ToolStripMenuItem(I18n.T("Menu_OpenConfigLocation")); + trayIcon.Visible = true; - openAppLocationMenu.Click += (s, e) => - { - try { Process.Start("explorer.exe", $"/select,\"{Process.GetCurrentProcess().MainModule?.FileName}\""); } - catch { MessageBox.Show(I18n.T("Error_Browser")); } - }; + if (notifyOnMinimizeItem?.Checked == true) + { + trayIcon.Tag = null; + trayIcon.ShowBalloonTip(3000, I18n.T(App_Name), I18n.T(Notify_Minimized), ToolTipIcon.Info); + } - openConfigLocationMenu.Click += (s, e) => - { - try - { - if (File.Exists(configPath)) Process.Start("explorer.exe", $"/select,\"{configPath}\""); - else Process.Start("explorer.exe", $"\"{Path.GetDirectoryName(configPath)}\""); + return; } - catch { MessageBox.Show(I18n.T("Error_Browser")); } + + SaveSettings(); + + connectionTimer?.Stop(); + connectionTimer?.Dispose(); + connectionTimer = null; + + clockTimer?.Stop(); + clockTimer?.Dispose(); + clockTimer = null; + + rpc.Dispose(); + _processWatcher.Dispose(); + _fontManager.Dispose(); + _updateService.Dispose(); + + if (trayIcon != null) + trayIcon.Visible = false; + _trayIconManager?.Dispose(); }; - githubMenu = new ToolStripMenuItem(I18n.T("Menu_Github")); - githubMenu.Click += (s, e) => { try { Process.Start(new ProcessStartInfo("https://github.com/Wea017net/GkmStatus") { UseShellExecute = true }); } catch { MessageBox.Show(I18n.T("Error_Browser")); } }; + } - checkUpdateMenuItem = new ToolStripMenuItem(I18n.T("Menu_CheckUpdateNow")); - checkUpdateMenuItem.Click += (s, e) => _ = CheckForUpdatesAsync(manual: true); + private void SafeBeginInvoke(Action action) + { + if (IsDisposed || Disposing || !IsHandleCreated) + return; - aboutMenu = new ToolStripMenuItem(I18n.T("Menu_About")); - aboutMenu.Click += (s, e) => ShowAboutDialog(); + try + { + BeginInvoke(action); + } + catch (ObjectDisposedException) { } + catch (InvalidOperationException) { } + } - var separatorHelp = new ToolStripSeparator(); - helpMenu.DropDownItems.AddRange([openAppLocationMenu, openConfigLocationMenu, separatorHelp, githubMenu, checkUpdateMenuItem, aboutMenu]); + private void RestoreFromTray() + { + this.ShowInTaskbar = true; + this.Show(); + this.WindowState = FormWindowState.Normal; + trayIcon.Visible = false; + this.Activate(); + ReapplyTheme(); + } - menu.Items.Add(fileMenu); menu.Items.Add(settingsMenu); menu.Items.Add(viewMenu); menu.Items.Add(helpMenu); - this.Controls.Add(menu); + private void ReapplyTheme() + { + AppTheme theme = AppTheme.Auto; + if (themeLight.Checked) theme = AppTheme.Light; + else if (themeDark.Checked) theme = AppTheme.Dark; + else if (themeOLED.Checked) theme = AppTheme.OLED; - ApplyLanguage(); - UpdateTrayMenuState(); + _themeManager.ApplyTheme(this, theme); } private void ApplyLanguage() { - this.Text = $"{I18n.T("App_Name")} v{Application.ProductVersion}"; + this.Text = $"{I18n.T(App_Name)} v{Application.ProductVersion}"; if (fileMenu != null) { - fileMenu.Text = I18n.T("Menu_File"); - exitItem.Text = I18n.T("Menu_Exit"); - - settingsMenu.Text = I18n.T("Menu_Settings"); - runAtStartupItem.Text = I18n.T("Menu_RunAtStartup"); - startMinimizedItem.Text = I18n.T("Menu_StartMinimized"); - autoConnectItem.Text = I18n.T("Menu_AutoConnect"); - checkForUpdatesItem.Text = I18n.T("Menu_CheckForUpdates"); - notifyInBackgroundItem.Text = I18n.T("Menu_NotifyInBackground"); - notifyOnMinimizeItem.Text = I18n.T("Menu_NotifyOnMinimize"); - minimizeToTrayItem.Text = I18n.T("Menu_MinimizeToTray"); - monitorItem.Text = I18n.T("Menu_MonitorProcess"); - - viewMenu.Text = I18n.T("Menu_View"); - themeMenu.Text = I18n.T("Menu_Theme"); - themeAuto.Text = I18n.T("Menu_ThemeAuto"); - themeLight.Text = I18n.T("Menu_ThemeLight"); - themeDark.Text = I18n.T("Menu_ThemeDark"); - themeOLED.Text = I18n.T("Menu_ThemeOLED"); - langMenu.Text = I18n.T("Menu_Language"); - - helpMenu.Text = I18n.T("Menu_Help"); - openAppLocationMenu.Text = I18n.T("Menu_OpenAppLocation"); - openConfigLocationMenu.Text = I18n.T("Menu_OpenConfigLocation"); - githubMenu.Text = I18n.T("Menu_Github"); - checkUpdateMenuItem.Text = I18n.T("Menu_CheckUpdateNow"); - aboutMenu.Text = I18n.T("Menu_About"); + fileMenu.Text = I18n.T(Menu_File); + exitItem.Text = I18n.T(Menu_Exit); + + settingsMenu.Text = I18n.T(Menu_Settings); + runAtStartupItem.Text = I18n.T(Menu_RunAtStartup); + startMinimizedItem.Text = I18n.T(Menu_StartMinimized); + autoConnectItem.Text = I18n.T(Menu_AutoConnect); + checkForUpdatesItem.Text = I18n.T(Menu_CheckForUpdates); + notifyInBackgroundItem.Text = I18n.T(Menu_NotifyInBackground); + notifyOnMinimizeItem.Text = I18n.T(Menu_NotifyOnMinimize); + minimizeToTrayItem.Text = I18n.T(Menu_MinimizeToTray); + monitorItem.Text = I18n.T(Menu_MonitorProcess); + + viewMenu.Text = I18n.T(Menu_View); + themeMenu.Text = I18n.T(Menu_Theme); + themeAuto.Text = I18n.T(Menu_ThemeAuto); + themeLight.Text = I18n.T(Menu_ThemeLight); + themeDark.Text = I18n.T(Menu_ThemeDark); + themeOLED.Text = I18n.T(Menu_ThemeOLED); + langMenu.Text = I18n.T(Menu_Language); + + helpMenu.Text = I18n.T(Menu_Help); + openAppLocationMenu.Text = I18n.T(Menu_OpenAppLocation); + openConfigLocationMenu.Text = I18n.T(Menu_OpenConfigLocation); + githubMenu.Text = I18n.T(Menu_Github); + checkUpdateMenuItem.Text = I18n.T(Menu_CheckUpdateNow); + aboutMenu.Text = I18n.T(Menu_About); } foreach (Control c in this.Controls) @@ -1070,45 +244,42 @@ private void ApplyLanguage() if (trayIcon.ContextMenuStrip != null) { - trayIcon.ContextMenuStrip.Items[0].Text = I18n.T("Tray_Open"); - if (trayMenuDetails != null) trayMenuDetails.Text = I18n.T("Header_Details"); - if (trayMenuState != null) trayMenuState.Text = I18n.T("Header_State"); - if (trayMenuProduce != null) trayMenuProduce.Text = I18n.T("Tray_ProducingIdol"); - trayMenuConnect.Text = I18n.T("Button_Connect"); - trayMenuPause.Text = I18n.T("Button_Pause"); - trayMenuDisconnect.Text = I18n.T("Button_Disconnect"); - if (trayIcon.ContextMenuStrip.Items.Count > 0) trayIcon.ContextMenuStrip.Items[trayIcon.ContextMenuStrip.Items.Count - 1].Text = I18n.T("Tray_Exit"); + trayIcon.ContextMenuStrip.Items[0].Text = I18n.T(Tray_Open); + trayMenuDetails?.Text = I18n.T(Header_Details); + trayMenuState?.Text = I18n.T(Header_State); + trayMenuProduce?.Text = I18n.T(Tray_ProducingIdol); + trayMenuConnect.Text = I18n.T(Button_Connect); + trayMenuPause.Text = I18n.T(Button_Pause); + trayMenuDisconnect.Text = I18n.T(Button_Disconnect); + if (trayIcon.ContextMenuStrip.Items.Count > 0) trayIcon.ContextMenuStrip.Items[trayIcon.ContextMenuStrip.Items.Count - 1].Text = I18n.T(Tray_Exit); } - lblResetGuide.Text = I18n.T("Timestamp_Guide"); - lblGameAppGuide.Text = I18n.T("GameApp_Guide"); - if (lblBtnModeNote != null) lblBtnModeNote.Text = I18n.T("Button_ModeNote"); + lblResetGuide.Text = I18n.T(Timestamp_Guide); + lblGameAppGuide.Text = I18n.T(GameApp_Guide); + lblBtnModeNote?.Text = I18n.T(Button_ModeNote); UpdateTimestampLabel(); - btnResetTime.Text = I18n.T("Timestamp_Reset"); - btnUpdate.Text = I18n.T("Button_Update"); - btnDisconnect.Text = I18n.T("Button_Disconnect"); + btnResetTime.Text = I18n.T(Timestamp_Reset); + btnUpdate.Text = I18n.T(Button_Update); + btnDisconnect.Text = I18n.T(Button_Disconnect); - SetPlaceholder(txtPName, I18n.T("Placeholder_PName")); - SetPlaceholder(txtBtn1Label, I18n.T("Placeholder_BtnLabel", 1)); - SetPlaceholder(txtBtn1Url, I18n.T("Placeholder_BtnUrl", 1)); - SetPlaceholder(txtBtn2Label, I18n.T("Placeholder_BtnLabel", 2)); - SetPlaceholder(txtBtn2Url, I18n.T("Placeholder_BtnUrl", 2)); + Native.SetPlaceholder(txtPName, I18n.T(Placeholder_PName)); + Native.SetPlaceholder(txtBtn1Label, I18n.T(Placeholder_BtnLabel, 1)); - if (client?.IsInitialized == true) + if (rpc.Status != RpcStatus.Disconnected) { - if (connectionTimer?.Enabled == true) + if (connectionTimer?.Enabled == true || rpc.Status == RpcStatus.Connecting) { - lblStatus.Text = I18n.T("Status_Connecting", connectionSeconds); + lblStatus.Text = I18n.T(Status_Connecting, connectionSeconds); } - else if (!isManualPaused) + else if (rpc.Status == RpcStatus.Connected && !isManualPaused) { - UpdateUIForConnected(client.CurrentUser?.Username ?? "Unknown"); + UpdateUIForConnected(rpc.CurrentUsername ?? "Unknown"); } else { UpdateUIForPause(); - btnConnect.Text = I18n.T("Button_Resume"); + btnConnect.Text = I18n.T(Button_Resume); } } else @@ -1124,10 +295,10 @@ static void RefreshCombo(ComboBox cb, string[] items) cb.SelectedIndex = Math.Min(idx, cb.Items.Count - 1); } - RefreshCombo(cmbDetailsType, [I18n.T("Details_None"), I18n.T("Details_PName"), I18n.T("Details_PLv"), I18n.T("Details_Both")]); - RefreshCombo(cmbStateType, [I18n.T("State_None"), I18n.T("State_PID"), I18n.T("State_Idol"), I18n.T("State_Producing"), I18n.T("State_Custom")]); - RefreshCombo(cmbBtnMode, [I18n.T("Button_ModeNone"), I18n.T("Button_ModeStore"), I18n.T("Button_ModeApp"), I18n.T("Button_ModeCustom")]); - RefreshCombo(cmbCharNameLang, [I18n.T("CharName_JP"), I18n.T("CharName_EN")]); + RefreshCombo(cmbDetailsType, [I18n.T(Details_None), I18n.T(Details_PName), I18n.T(Details_PLv), I18n.T(Details_Both)]); + RefreshCombo(cmbStateType, [I18n.T(State_None), I18n.T(State_PID), I18n.T(State_Idol), I18n.T(State_Producing), I18n.T(State_Custom)]); + RefreshCombo(cmbBtnMode, [I18n.T(Button_ModeNone), I18n.T(Button_ModeStore), I18n.T(Button_ModeApp), I18n.T(Button_ModeCustom)]); + RefreshCombo(cmbCharNameLang, [I18n.T(CharName_JP), I18n.T(CharName_EN)]); CmbStateType_SelectedIndexChanged(null, EventArgs.Empty); CmbBtnMode_SelectedIndexChanged(null, EventArgs.Empty); @@ -1136,7 +307,7 @@ static void RefreshCombo(ComboBox cb, string[] items) private void CmbGameName_SelectedIndexChanged(object? sender, EventArgs e) { if (cmbGameName.SelectedIndex == -1) return; - currentPresence.GameAppIndex = cmbGameName.SelectedIndex; + CurrentPresence.GameAppIndex = cmbGameName.SelectedIndex; if (!isInitializing) { @@ -1149,131 +320,81 @@ private void CmbGameName_SelectedIndexChanged(object? sender, EventArgs e) private void ShowAboutDialog() { - using var about = new AboutForm(this.BackColor, this.ForeColor, appFont, uiScale); + using var about = new AboutForm(this.BackColor, this.ForeColor, _fontManager.AppFont, uiScale); about.ShowDialog(this); } - private static string NormalizeVersionString(string version) - { - if (string.IsNullOrEmpty(version)) return version; - version = version.Trim(); - if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase)) - version = version[1..]; - else if (version.StartsWith("Ver.", StringComparison.OrdinalIgnoreCase)) - version = version[4..]; - else if (version.StartsWith("Ver", StringComparison.OrdinalIgnoreCase)) - version = version[3..]; - return version.Trim(); - } - private async Task CheckForUpdatesAsync(bool manual = false) { if (!manual && (DateTime.UtcNow - lastUpdateCheck).TotalHours < 24) + return; + + var result = await _updateService.CheckForUpdatesAsync(Application.ProductVersion); + + if (result.IsRateLimited) { + if (manual) SafeBeginInvoke(() => MessageBox.Show(this, I18n.T(Update_RateLimit), "Update", MessageBoxButtons.OK, MessageBoxIcon.Warning)); return; } - try + if (!result.IsSuccess) { - using var client = new System.Net.Http.HttpClient(); - client.DefaultRequestHeaders.UserAgent.ParseAdd("GkmStatus-UpdateChecker"); - var response = await client.GetAsync("https://api.github.com/repos/Wea017net/GkmStatus/releases/latest"); - - if (response.StatusCode == System.Net.HttpStatusCode.Forbidden) - { - if (manual) this.Invoke(() => MessageBox.Show(this, I18n.T("Update_RateLimit"), "Update", MessageBoxButtons.OK, MessageBoxIcon.Warning)); - return; - } + if (manual) SafeBeginInvoke(() => MessageBox.Show(this, "Update check failed: " + result.ErrorMessage, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)); + return; + } - if (response.StatusCode == System.Net.HttpStatusCode.NotFound) - { - if (manual) this.Invoke(() => MessageBox.Show(this, I18n.T("Update_NoUpdate"), "Update", MessageBoxButtons.OK, MessageBoxIcon.Information)); - lastUpdateCheck = DateTime.UtcNow; - SaveSettings(); - return; - } + lastUpdateCheck = DateTime.UtcNow; + SaveSettings(); - response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(); - using var doc = JsonDocument.Parse(content); - if (doc.RootElement.TryGetProperty("tag_name", out var tag)) + if (result.HasUpdate) + { + SafeBeginInvoke(() => { - string latestStr = tag.GetString()?.TrimStart('v') ?? ""; - string currentStr = NormalizeVersionString(Application.ProductVersion); - if (Version.TryParse(latestStr, out var latest) && Version.TryParse(currentStr, out var current)) + if (manual) { - lastUpdateCheck = DateTime.UtcNow; - SaveSettings(); - - if (latest > current) - { - this.Invoke(() => - { - if (manual) - { - if (MessageBox.Show(this, I18n.T("Update_NewAvailable", latestStr), "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes) - { - try { Process.Start(new ProcessStartInfo("https://github.com/Wea017net/GkmStatus/releases/latest") { UseShellExecute = true }); } catch { } - } - } - else - { - trayIcon.Visible = true; - trayIcon.Tag = "https://github.com/Wea017net/GkmStatus/releases/latest"; - trayIcon.ShowBalloonTip(5000, I18n.T("Update_NotificationTitle"), I18n.T("Update_NotificationBody", latestStr), ToolTipIcon.Info); - } - }); - } - else if (manual) + if (MessageBox.Show(this, I18n.T(Update_NewAvailable, result.LatestVersion), "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes) { - this.Invoke(() => MessageBox.Show(this, I18n.T("Update_NoUpdate"), "Update", MessageBoxButtons.OK, MessageBoxIcon.Information)); + _ = StartupManager.OpenUrl(result.ReleaseUrl); } } - } - } - catch (Exception ex) - { - if (manual) this.Invoke(() => MessageBox.Show(this, "Update check failed: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)); + else + { + trayIcon.Visible = true; + trayIcon.Tag = result.ReleaseUrl; + trayIcon.ShowBalloonTip(5000, I18n.T(Update_NotificationTitle), I18n.T(Update_NotificationBody, result.LatestVersion), ToolTipIcon.Info); + } + }); } + else if (manual) + SafeBeginInvoke(() => MessageBox.Show(this, I18n.T(Update_NoUpdate), "Update", MessageBoxButtons.OK, MessageBoxIcon.Information)); } - private static string GetStateString(int i) => i switch { 0 => "None", 1 => "PID", 2 => "Idol", 3 => "Producing", 4 => "Custom", _ => "Producing" }; - private static int GetStateIndex(string s) => s switch { "None" => 0, "PID" => 1, "Idol" => 2, "Producing" => 3, "Custom" => 4, _ => 3 }; - - private static string GetDetailsString(int i) => i switch { 0 => "None", 1 => "PName", 2 => "PLv", 3 => "Both", _ => "Both" }; - private static int GetDetailsIndex(string s) => s switch { "None" => 0, "PName" => 1, "PLv" => 2, "Both" => 3, _ => 3 }; - - private static string GetButtonModeString(int i) => i switch { 0 => "None", 1 => "Store", 2 => "App", 3 => "Custom", _ => "None" }; - private static int GetButtonModeIndex(string s) => s switch { "None" => 0, "Store" => 1, "App" => 2, "Custom" => 3, _ => 0 }; - - private static string GetThemeString(int i) => i switch { 0 => "Auto", 1 => "Light", 2 => "Dark", 3 => "OLED", _ => "Auto" }; - private static int GetThemeIndex(string s) => s switch { "Auto" => 0, "Light" => 1, "Dark" => 2, "OLED" => 3, _ => 0 }; - - private static string GetLangString(int i) => i switch { 0 => "ja", 1 => "en", _ => "ja" }; - private static int GetLangIndex(string s) => s switch { "ja" => 0, "en" => 1, _ => 0 }; - private void CmbStateType_SelectedIndexChanged(object? sender, EventArgs e) { int idx = cmbStateType.SelectedIndex; if (idx == -1) return; - string stateStr = GetStateString(idx); + var stateType = GetStateType(idx); - if (!isInitializing && lastStateIdx != -1 && GetStateString(lastStateIdx) != "Idol" && GetStateString(lastStateIdx) != "Producing") + if (!isInitializing && lastStateIdx != -1) { - currentPresence.StateHistory[GetStateString(lastStateIdx)] = txtStateCustom.Text; + var lastStateType = GetStateType(lastStateIdx); + if (lastStateType != PresenceStateType.Idol && lastStateType != PresenceStateType.Producing) + { + CurrentPresence.StateHistory[GetStateString(lastStateType)] = txtStateCustom.Text; + } } - bool isProduceMode = (stateStr == "Idol" || stateStr == "Producing"); + bool isProduceMode = stateType is PresenceStateType.Idol or PresenceStateType.Producing; if (txtStateCustom.Tag is Panel container) { - container.Visible = !isProduceMode && (stateStr != "None"); + container.Visible = !isProduceMode && stateType != PresenceStateType.None; } cmbProduceCharacter.Visible = isProduceMode; cmbCharNameLang.Visible = isProduceMode; - if (stateStr == "PID") + if (stateType == PresenceStateType.PID) { if (txtStateCustom.Tag is Panel con) { @@ -1283,13 +404,13 @@ private void CmbStateType_SelectedIndexChanged(object? sender, EventArgs e) } txtStateCustom.Enabled = true; txtStateCustom.MaxLength = 8; - SetPlaceholder(txtStateCustom, I18n.T("Placeholder_PID")); - if (currentPresence.StateHistory.TryGetValue("PID", out var pidText)) + Native.SetPlaceholder(txtStateCustom, I18n.T(Placeholder_PID)); + if (CurrentPresence.StateHistory.TryGetValue(GetStateString(PresenceStateType.PID), out var pidText)) txtStateCustom.Text = pidText; else txtStateCustom.Text = ""; } - else if (stateStr == "Custom") + else if (stateType == PresenceStateType.Custom) { if (txtStateCustom.Tag is Panel con) { @@ -1299,8 +420,8 @@ private void CmbStateType_SelectedIndexChanged(object? sender, EventArgs e) } txtStateCustom.Enabled = true; txtStateCustom.MaxLength = 128; - SetPlaceholder(txtStateCustom, I18n.T("Placeholder_Custom")); - if (currentPresence.StateHistory.TryGetValue("Custom", out var customText)) + Native.SetPlaceholder(txtStateCustom, I18n.T(Placeholder_Custom)); + if (CurrentPresence.StateHistory.TryGetValue(GetStateString(PresenceStateType.Custom), out var customText)) txtStateCustom.Text = customText; else txtStateCustom.Text = ""; @@ -1310,10 +431,10 @@ private void CmbStateType_SelectedIndexChanged(object? sender, EventArgs e) txtStateCustom.Enabled = false; } - if (stateStr == "Idol" || stateStr == "Producing") + if (stateType is PresenceStateType.Idol or PresenceStateType.Producing) { - var currentId = (stateStr == "Idol") ? currentPresence.SelectedIdolCharacterId : currentPresence.SelectedProduceCharacterId; - int charIdx = ProduceCharacters.FindIndex(c => c.Id == currentId); + var currentId = (stateType == PresenceStateType.Idol) ? CurrentPresence.SelectedIdolCharacterId : CurrentPresence.SelectedProduceCharacterId; + int charIdx = FindProduceCharacterIndex(currentId); cmbProduceCharacter.SelectedIndex = charIdx >= 0 ? charIdx : 0; } @@ -1324,39 +445,45 @@ private void CmbStateType_SelectedIndexChanged(object? sender, EventArgs e) private void CmbBtnMode_SelectedIndexChanged(object? sender, EventArgs e) { if (cmbBtnMode.SelectedIndex == -1) return; - if (!isInitializing && GetButtonModeString(lastBtnIdx) == "Custom") + if (!isInitializing && GetButtonMode(lastBtnIdx) == PresenceButtonMode.Custom) { - currentPresence.ButtonHistory["Custom"] = new ButtonHistoryData { L1 = txtBtn1Label.Text, U1 = txtBtn1Url.Text, L2 = txtBtn2Label.Text, U2 = txtBtn2Url.Text }; + CurrentPresence.ButtonHistory[GetButtonModeString(PresenceButtonMode.Custom)] = new ButtonHistoryData { L1 = txtBtn1Label.Text, U1 = txtBtn1Url.Text, L2 = txtBtn2Label.Text, U2 = txtBtn2Url.Text }; } - string btnMode = GetButtonModeString(cmbBtnMode.SelectedIndex); + var btnMode = GetButtonMode(cmbBtnMode.SelectedIndex); - if (btnMode == "None") + if (btnMode == PresenceButtonMode.None) { txtBtn1Label.Text = ""; txtBtn1Url.Text = ""; txtBtn2Label.Text = ""; txtBtn2Url.Text = ""; SetBtnInputsEnabled(false); lblBtnModeNote.Visible = false; } - else if (btnMode == "Store") + else if (btnMode == PresenceButtonMode.Store) { - txtBtn1Label.Text = I18n.T("Button_StoreLabel_Mobile"); + txtBtn1Label.Text = I18n.T(Button_StoreLabel_Mobile); txtBtn1Url.Text = "https://app.adjust.com/1ai6ouao"; - txtBtn2Label.Text = I18n.T("Button_StoreLabel_DMM"); + txtBtn2Label.Text = I18n.T(Button_StoreLabel_DMM); txtBtn2Url.Text = "https://dmg-gakuen.idolmaster-official.jp/"; SetBtnInputsEnabled(false); lblBtnModeNote.Visible = true; } - else if (btnMode == "App") + else if (btnMode == PresenceButtonMode.App) { - txtBtn1Label.Text = I18n.T("Button_AboutPresence"); + txtBtn1Label.Text = I18n.T(Button_AboutPresence); txtBtn1Url.Text = "https://github.com/Wea017net/GkmStatus"; txtBtn2Label.Text = ""; txtBtn2Url.Text = ""; SetBtnInputsEnabled(false); lblBtnModeNote.Visible = true; } - else if (btnMode == "Custom") + else if (btnMode == PresenceButtonMode.Custom) { - SetPlaceholder(txtBtn1Label, I18n.T("Placeholder_BtnLabel", 1)); SetPlaceholder(txtBtn1Url, I18n.T("Placeholder_BtnUrl", 1)); - SetPlaceholder(txtBtn2Label, I18n.T("Placeholder_BtnLabel", 2)); SetPlaceholder(txtBtn2Url, I18n.T("Placeholder_BtnUrl", 2)); - if (currentPresence.ButtonHistory.TryGetValue("Custom", out var h)) { txtBtn1Label.Text = h.L1; txtBtn1Url.Text = h.U1; txtBtn2Label.Text = h.L2; txtBtn2Url.Text = h.U2; } + Native.SetPlaceholder(txtBtn1Label, I18n.T(Placeholder_BtnLabel, 1)); + Native.SetPlaceholder(txtBtn1Url, I18n.T(Placeholder_BtnUrl, 1)); + Native.SetPlaceholder(txtBtn2Label, I18n.T(Placeholder_BtnLabel, 2)); + Native.SetPlaceholder(txtBtn2Url, I18n.T(Placeholder_BtnUrl, 2)); + if (CurrentPresence.ButtonHistory.TryGetValue(GetButtonModeString(PresenceButtonMode.Custom), out var h)) + { + txtBtn1Label.Text = h.L1; txtBtn1Url.Text = h.U1; + txtBtn2Label.Text = h.L2; txtBtn2Url.Text = h.U2; + } else { txtBtn1Label.Text = ""; txtBtn1Url.Text = ""; txtBtn2Label.Text = ""; txtBtn2Url.Text = ""; } SetBtnInputsEnabled(true); lblBtnWarning.Visible = Encoding.UTF8.GetByteCount(txtBtn1Label.Text) > 32 || Encoding.UTF8.GetByteCount(txtBtn2Label.Text) > 32; @@ -1373,303 +500,164 @@ private void CmbBtnMode_SelectedIndexChanged(object? sender, EventArgs e) private void SaveSettings() { if (isInitializing) return; - try - { - string? dir = Path.GetDirectoryName(configPath); - if (dir != null && !Directory.Exists(dir)) Directory.CreateDirectory(dir); - string st = GetStateString(cmbStateType.SelectedIndex); - if (cmbStateType.SelectedIndex != -1 && st != "Idol" && st != "Producing") - currentPresence.StateHistory[st] = txtStateCustom.Text; + var config = _configManager.Config; + var settings = config.Settings; + var presence = config.Presence; - var config = new AppConfig - { - ConfigVersion = 1, - Settings = new AppSettings - { - StartMinimized = startMinimizedItem.Checked, - ConnectOnStart = autoConnectItem.Checked, - AutoCheckUpdates = checkForUpdatesItem.Checked, - ShowBackgroundNotifications = notifyInBackgroundItem.Checked, - NotifyOnMinimize = notifyOnMinimizeItem.Checked, - MinimizeToTray = minimizeToTrayItem.Checked, - AutoDetectGakumas = monitorItem.Checked, - SelectedTheme = GetThemeString( - themeLight.Checked ? 1 : (themeDark.Checked ? 2 : (themeOLED.Checked ? 3 : 0)) - ), - SelectedLanguage = GetLangString(langEnglish.Checked ? 1 : 0), - LastUpdateCheck = lastUpdateCheck - }, - Presence = currentPresence - }; + settings.StartMinimized = startMinimizedItem.Checked; + settings.ConnectOnStart = autoConnectItem.Checked; + settings.AutoCheckUpdates = checkForUpdatesItem.Checked; + settings.ShowBackgroundNotifications = notifyInBackgroundItem.Checked; + settings.NotifyOnMinimize = notifyOnMinimizeItem.Checked; + settings.MinimizeToTray = minimizeToTrayItem.Checked; + settings.AutoDetectGakumas = monitorItem.Checked; + var selectedTheme = themeLight.Checked ? AppTheme.Light : (themeDark.Checked ? AppTheme.Dark : (themeOLED.Checked ? AppTheme.OLED : AppTheme.Auto)); + settings.SelectedTheme = GetThemeString(selectedTheme); + settings.SelectedLanguage = GetLangString(GetLanguage(langEnglish.Checked ? 1 : 0)); + settings.LastUpdateCheck = lastUpdateCheck; - config.Presence.DetailsType = GetDetailsString(cmbDetailsType.SelectedIndex); - config.Presence.GameAppIndex = cmbGameName.SelectedIndex >= 0 ? cmbGameName.SelectedIndex : 0; - config.Presence.StateType = GetStateString(cmbStateType.SelectedIndex); - config.Presence.ButtonMode = GetButtonModeString(cmbBtnMode.SelectedIndex); - config.Presence.ProducerName = txtPName.Text; - config.Presence.ProducerLevel = (int)numPLevel.Value; + var detailsType = GetDetailsType(cmbDetailsType.SelectedIndex); + var stateType = GetStateType(cmbStateType.SelectedIndex); + var buttonMode = GetButtonMode(cmbBtnMode.SelectedIndex); - if (cmbProduceCharacter.SelectedIndex >= 0) - { - string characterId = ProduceCharacters[cmbProduceCharacter.SelectedIndex].Id; - string stateStr = GetStateString(cmbStateType.SelectedIndex); - if (stateStr == "Idol") config.Presence.SelectedIdolCharacterId = characterId; - else if (stateStr == "Producing") config.Presence.SelectedProduceCharacterId = characterId; - } + presence.DetailsType = GetDetailsString(detailsType); + presence.GameAppIndex = cmbGameName.SelectedIndex >= 0 ? cmbGameName.SelectedIndex : 0; + presence.ProducerName = txtPName.Text; + presence.ProducerLevel = (int)numPLevel.Value; + presence.StateType = GetStateString(stateType); + presence.ButtonMode = GetButtonModeString(buttonMode); + presence.CharNameLangIndex = cmbCharNameLang.SelectedIndex; - string json = JsonSerializer.Serialize(config, jsonOptions); - File.WriteAllText(configPath, json); + if (cmbProduceCharacter.SelectedIndex >= 0) + { + string characterId = ProduceCharacters[cmbProduceCharacter.SelectedIndex].Id; + if (stateType == PresenceStateType.Idol) presence.SelectedIdolCharacterId = characterId; + else if (stateType == PresenceStateType.Producing) presence.SelectedProduceCharacterId = characterId; } - catch (Exception ex) { Debug.WriteLine("Save Error: " + ex.Message); } + + _configManager.Save(); } private void LoadSettings() { - if (!File.Exists(configPath)) - { - SetDefaultSettings(); - return; - } + var config = _configManager.Config; + var settings = config.Settings; + var presence = config.Presence; + try { - string json = File.ReadAllText(configPath); - var config = JsonSerializer.Deserialize(json, jsonOptions) ?? throw new JsonException("Failed to deserialize config."); - - startMinimizedItem.Checked = config.Settings.StartMinimized; - autoConnectItem.Checked = config.Settings.ConnectOnStart; - checkForUpdatesItem.Checked = config.Settings.AutoCheckUpdates; - notifyInBackgroundItem.Checked = config.Settings.ShowBackgroundNotifications; - notifyOnMinimizeItem.Checked = config.Settings.NotifyOnMinimize; - minimizeToTrayItem.Checked = config.Settings.MinimizeToTray; - monitorItem.Checked = config.Settings.AutoDetectGakumas; - if (monitorTimer != null) + startMinimizedItem.Checked = settings.StartMinimized; + autoConnectItem.Checked = settings.ConnectOnStart; + checkForUpdatesItem.Checked = settings.AutoCheckUpdates; + notifyInBackgroundItem.Checked = settings.ShowBackgroundNotifications; + notifyOnMinimizeItem.Checked = settings.NotifyOnMinimize; + minimizeToTrayItem.Checked = settings.MinimizeToTray; + monitorItem.Checked = settings.AutoDetectGakumas; + + _processWatcher.Enabled = settings.AutoDetectGakumas; + if (_processWatcher.Enabled) { - monitorTimer.Enabled = monitorItem.Checked; - if (monitorTimer.Enabled) MonitorProcess(null, EventArgs.Empty); + _processWatcher.ForceCheck(); } - string theme = config.Settings.SelectedTheme ?? "Auto"; + var theme = GetTheme(settings.SelectedTheme); switch (theme) { - case "Light": themeLight.PerformClick(); break; - case "Dark": themeDark.PerformClick(); break; - case "OLED": themeOLED.PerformClick(); break; + case AppTheme.Light: themeLight.PerformClick(); break; + case AppTheme.Dark: themeDark.PerformClick(); break; + case AppTheme.OLED: themeOLED.PerformClick(); break; default: themeAuto.PerformClick(); break; } - string langStr = config.Settings.SelectedLanguage ?? (System.Globalization.CultureInfo.CurrentUICulture.Name.StartsWith("ja") ? "ja" : "en"); - if (langStr == "en" || langStr == "English") + var language = GetLanguage(settings.SelectedLanguage ?? (System.Globalization.CultureInfo.CurrentUICulture.Name.StartsWith("ja") ? "ja" : "en")); + if (language == I18n.Language.English) { - I18n.CurrentLanguage = "English"; + I18n.CurrentLanguage = I18n.Language.English; langEnglish.Checked = true; langJapanese.Checked = false; } else { - I18n.CurrentLanguage = "日本語"; + I18n.CurrentLanguage = I18n.Language.Japanese; langJapanese.Checked = true; langEnglish.Checked = false; } - lastUpdateCheck = config.Settings.LastUpdateCheck; - currentPresence = config.Presence; + lastUpdateCheck = settings.LastUpdateCheck; + + cmbGameName.SelectedIndex = Math.Clamp(presence.GameAppIndex, 0, cmbGameName.Items.Count - 1); + cmbDetailsType.SelectedIndex = GetDetailsIndex(GetDetailsType(presence.DetailsType ?? GetDetailsString(PresenceDetailsType.Both))); + txtPName.Text = presence.ProducerName; + numPLevel.Value = Math.Clamp(presence.ProducerLevel, 1, 100); - cmbGameName.SelectedIndex = Math.Clamp(currentPresence.GameAppIndex, 0, cmbGameName.Items.Count - 1); - cmbDetailsType.SelectedIndex = GetDetailsIndex(currentPresence.DetailsType ?? "Both"); - txtPName.Text = currentPresence.ProducerName; - numPLevel.Value = Math.Clamp(currentPresence.ProducerLevel, 1, 100); - cmbStateType.SelectedIndex = GetStateIndex(currentPresence.StateType ?? "Producing"); + var loadedStateType = GetStateType(presence.StateType ?? GetStateString(PresenceStateType.Producing)); + cmbStateType.SelectedIndex = GetStateIndex(loadedStateType); - string loadedState = currentPresence.StateType ?? "Producing"; - if (loadedState != "Idol" && loadedState != "Producing" && loadedState != "None") + if (loadedStateType is not PresenceStateType.Idol and not PresenceStateType.Producing and not PresenceStateType.None) { - if (currentPresence.StateHistory.TryGetValue(loadedState, out var savedText)) + var stateKey = GetStateString(loadedStateType); + if (presence.StateHistory.TryGetValue(stateKey, out var savedText)) txtStateCustom.Text = savedText; } - cmbCharNameLang.SelectedIndex = Math.Clamp(currentPresence.CharNameLangIndex, 0, 1); + cmbCharNameLang.SelectedIndex = Math.Clamp(presence.CharNameLangIndex, 0, 1); RefreshProduceCharacterList(); - string? currentId = (GetStateString(cmbStateType.SelectedIndex) == "Idol") ? currentPresence.SelectedIdolCharacterId : currentPresence.SelectedProduceCharacterId; - int charIdx = ProduceCharacters.FindIndex(c => c.Id == currentId); + string? currentId = (GetStateType(cmbStateType.SelectedIndex) == PresenceStateType.Idol) + ? presence.SelectedIdolCharacterId + : presence.SelectedProduceCharacterId; + + int charIdx = FindProduceCharacterIndex(currentId); cmbProduceCharacter.SelectedIndex = charIdx >= 0 ? charIdx : 0; - cmbBtnMode.SelectedIndex = GetButtonModeIndex(currentPresence.ButtonMode ?? "Store"); - if (config.Settings.ConnectOnStart) InitializeRpc(); + cmbBtnMode.SelectedIndex = GetButtonModeIndex(GetButtonMode(presence.ButtonMode ?? GetButtonModeString(PresenceButtonMode.Store))); + + if (settings.ConnectOnStart) InitializeRpc(); ApplyLanguage(); } catch (Exception ex) { - Debug.WriteLine("Load Error (resetting to defaults): " + ex.Message); - SetDefaultSettings(); + Debug.WriteLine("UI Load Error: " + ex.Message); + if (!_isResettingDefaults) + SetDefaultSettings(); } } private void SetDefaultSettings() { - themeAuto.PerformClick(); - monitorItem.Checked = true; - checkForUpdatesItem.Checked = true; - notifyInBackgroundItem.Checked = true; - notifyOnMinimizeItem.Checked = true; - cmbStateType.SelectedIndex = 2; - cmbBtnMode.SelectedIndex = 1; - - if (System.Globalization.CultureInfo.CurrentUICulture.Name.StartsWith("ja")) - { - I18n.CurrentLanguage = "日本語"; - langJapanese.Checked = true; - langEnglish.Checked = false; - } - else - { - I18n.CurrentLanguage = "English"; - langEnglish.Checked = true; - langJapanese.Checked = false; - } - - if (monitorTimer != null) - { - monitorTimer.Enabled = true; - MonitorProcess(null, EventArgs.Empty); - } - - ApplyLanguage(); - } - - private static bool IsRunAtStartup() - { - using RegistryKey? key = Registry.CurrentUser.OpenSubKey(REG_RUN_KEY, false); - return key?.GetValue(APP_NAME) != null; - } + if (_isResettingDefaults) + return; - private static void SetRunAtStartup(bool run) - { + _isResettingDefaults = true; try { - using RegistryKey? key = Registry.CurrentUser.OpenSubKey(REG_RUN_KEY, true); - if (key != null) - { - if (run) key.SetValue(APP_NAME, Application.ExecutablePath); - else key.DeleteValue(APP_NAME, false); - } - } - catch (Exception ex) - { - MessageBox.Show("スタートアップ設定の変更に失敗しました: " + ex.Message); - } - } - - private static void UpdateThemeChecks(ToolStripMenuItem selected, params ToolStripMenuItem[] others) { selected.Checked = true; foreach (var o in others) o.Checked = false; } - - private void ApplyThemeAuto() - { - try { object? v = Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", null); if (v is int intVal) { if (intVal == 0) ApplyThemeDark(); else ApplyThemeLight(); return; } } - catch { } - SetThemeColors(defaultBackground); - } - - private void ApplyThemeLight() => SetThemeColors(ColorTranslator.FromHtml("#f3f3f4")); - private void ApplyThemeDark() => SetThemeColors(defaultBackground); - private void ApplyThemeOLED() => SetThemeColors(Color.Black); - - private static IEnumerable GetAllChildControls(Control parent) { foreach (Control c in parent.Controls) { yield return c; foreach (var sub in GetAllChildControls(c)) yield return sub; } } - - private void SetThemeColors(Color bg) - { - this.BackColor = bg; - bool isBright = bg.GetBrightness() > 0.5f; - this.ForeColor = isBright ? Color.Black : Color.White; - SetTitleBarDarkMode(!isBright); - Color menuBg = isBright ? Color.FromArgb(235, 233, 233) : (bg.R == 0 ? Color.Black : Color.FromArgb(45, 47, 51)); - Color controlBg = isBright ? Color.White : Color.FromArgb(47, 49, 54); - Color borderColor = isBright ? Color.FromArgb(200, 200, 200) : Color.FromArgb(70, 70, 70); - - foreach (Control c in GetAllChildControls(this)) - { - if (c is Label lbl) - { - if (lbl.Tag is string tag && tag.StartsWith("Header_")) lbl.Font = appFontBold; - else if (lbl == lblGameAppGuide || lbl == lblResetGuide || lbl == lblStatus || lbl == lblBtnWarning || lbl == lblBtnModeNote) lbl.Font = appFontMedium; - else lbl.Font = appFont; - - if (lbl == lblResetGuide || lbl == lblBtnModeNote) lbl.ForeColor = Color.Gray; - else if (lbl == lblGameAppGuide) lbl.ForeColor = COLOR_PAUSE; - else if (lbl == lblBtnWarning || lbl == lblStatus) { /* Managed by logic */ } - else lbl.ForeColor = isBright ? Color.Black : Color.LightGray; - } - else - { - c.Font = appFont; - if (c is MenuStrip ms) - { - ms.BackColor = menuBg; - ms.Font = menuFont; - ms.Renderer = new MyRenderer(isBright, new CustomColorTable(isBright)); - foreach (ToolStripItem item in ms.Items) - { - if (item is ToolStripMenuItem tsmi) ApplyThemeToMenuItems(tsmi, menuBg, isBright); - else item.Font = menuFont; - } - } - else if (c is ComboBox cb) { cb.BackColor = controlBg; cb.ForeColor = isBright ? Color.Black : Color.White; } - else if (c is TextBox tb) { tb.BackColor = controlBg; tb.ForeColor = isBright ? Color.Black : Color.White; if (tb.Tag is Panel p) { p.BackColor = controlBg; p.Tag = borderColor; p.Invalidate(); } } - else if (c is NumericUpDown nu) { nu.BackColor = controlBg; nu.ForeColor = isBright ? Color.Black : Color.White; if (nu.Tag is Panel p) { p.BackColor = controlBg; p.Tag = borderColor; p.Invalidate(); } } - else if (c is CheckBox chk) { chk.ForeColor = isBright ? Color.Black : Color.White; } - else if (c is System.Windows.Forms.Button btn) { btn.Font = appFontBold; } - } - } - - if (trayIcon?.ContextMenuStrip != null) - { - var tms = trayIcon.ContextMenuStrip; - tms.BackColor = menuBg; - tms.Font = menuFont; - tms.Renderer = new MyRenderer(isBright, new CustomColorTable(isBright)); - foreach (ToolStripItem item in tms.Items) - { - if (item is ToolStripMenuItem tsmi) ApplyThemeToMenuItems(tsmi, menuBg, isBright); - else item.Font = menuFont; - } - } - } - - private void ApplyThemeToMenuItems(ToolStripMenuItem item, Color menuBg, bool isBright) - { - item.Font = menuFont; - if (item.DropDown != null) - { - item.DropDown.BackColor = menuBg; - item.DropDown.Font = menuFont; - item.DropDown.Renderer = new MyRenderer(isBright, new CustomColorTable(isBright)); + _configManager.ResetToDefault(); + LoadSettings(); } - foreach (ToolStripItem subItem in item.DropDownItems) + finally { - if (subItem is ToolStripMenuItem subMenu) ApplyThemeToMenuItems(subMenu, menuBg, isBright); - else subItem.Font = menuFont; + _isResettingDefaults = false; } } - private void SetTitleBarDarkMode(bool dark) { try { int useDark = dark ? 1 : 0; if (DwmSetWindowAttribute(this.Handle, 20, ref useDark, sizeof(int)) != 0) DwmSetWindowAttribute(this.Handle, 19, ref useDark, sizeof(int)); } catch { } } - private static void SetPlaceholder(IntPtr handle, string placeholderText) { SendMessage(handle, EM_SETCUEBANNER, 0, placeholderText); } - private static void SetPlaceholder(TextBox textBox, string placeholderText) { SetPlaceholder(textBox.Handle, placeholderText); } - private void InitializeLogic() { startTime = DateTime.UtcNow; UpdateTimestampLabel(); - monitorTimer = new System.Windows.Forms.Timer { Interval = 3000 }; - monitorTimer.Tick += MonitorProcess; - if (monitorItem?.Checked == true) monitorTimer.Enabled = true; - clockTimer = new System.Windows.Forms.Timer { Interval = 1000, Enabled = true }; - clockTimer.Tick += (s, e) => { UpdateTimestampLabel(); if (client?.IsInitialized == true) client.Invoke(); }; + clockTimer.Tick += (s, e) => + { + UpdateTimestampLabel(); + if (rpc.IsInitialized == true) + rpc.Invoke(); + }; } private void InitializeRpc() { + if (rpc.Status == RpcStatus.Connecting || rpc.Status == RpcStatus.Connected) return; if (connectionTimer?.Enabled == true) return; - if (client?.IsInitialized == true) { isManualPaused = false; UpdateUIForConnected(client.CurrentUser?.Username ?? "接続済み"); UpdateRpc(); return; } + if (cmbGameName.SelectedIndex < 0 || cmbGameName.SelectedIndex >= GameApps.Count) return; connectionSeconds = 0; if (connectionTimer is null) @@ -1677,55 +665,24 @@ private void InitializeRpc() connectionTimer = new System.Windows.Forms.Timer { Interval = 1000 }; connectionTimer.Tick += (s, e) => { - if (!connectionTimer.Enabled || lblStatus.ForeColor != COLOR_PAUSE) return; + if (rpc.Status != RpcStatus.Connecting) return; + connectionSeconds++; lblStatus.Text = I18n.T("Status_Connecting", connectionSeconds); - if (connectionSeconds >= CONNECTION_TIMEOUT) HandleConnectionError(I18n.T("Status_Timeout")); + if (connectionSeconds >= CONNECTION_TIMEOUT) HandleConnectionError(I18n.T(Status_Timeout)); }; } - lblStatus.Text = I18n.T("Status_Connecting", 0); lblStatus.ForeColor = COLOR_PAUSE; btnConnect.Enabled = false; - AdjustStatusVerticalPosition(); - UpdateTrayStatusIcon(COLOR_PAUSE, I18n.T("Tray_Status_Connecting")); - - client?.Deinitialize(); - client?.Dispose(); - client = null; - - string appId = GameApps[cmbGameName.SelectedIndex].AppId; - client = new DiscordRpcClient(appId) - { - Logger = new DiscordRPC.Logging.ConsoleLogger() { Level = DiscordRPC.Logging.LogLevel.Warning }, - SkipIdenticalPresence = false - }; - - client.OnReady += (sender, e) => - { - this.Invoke((MethodInvoker)(async () => - { - connectionTimer.Stop(); - UpdateUIForConnected(e.User.Username); - UpdateTrayMenuState(); - - await System.Threading.Tasks.Task.Delay(500); - if (client?.IsInitialized == true) - { - UpdateRpc(); - UpdateTrayMenuState(); - } - if (this.WindowState == FormWindowState.Minimized && (notifyInBackgroundItem?.Checked == true)) - { - trayIcon.Tag = null; - trayIcon.ShowBalloonTip(3000, I18n.T("App_Name"), I18n.T("Status_Connected_Notify", e.User.Username), ToolTipIcon.Info); - } - })); - }; + lblStatus.Text = I18n.T("Status_Connecting", 0); + lblStatus.ForeColor = COLOR_PAUSE; + btnConnect.Enabled = false; + AdjustStatusVerticalPosition(); + _trayIconManager?.UpdateStatusIcon(COLOR_PAUSE, I18n.T(Tray_Status_Connecting)); - client.OnError += (sender, e) => { this.Invoke((MethodInvoker)(() => { HandleConnectionError(I18n.T("Status_Error", e.Message)); })); }; connectionTimer.Start(); - try { if (!client.Initialize()) HandleConnectionError(I18n.T("Status_InitFailed")); } - catch (Exception ex) { HandleConnectionError(I18n.T("Status_Exception", ex.Message)); } + string appId = GameApps[cmbGameName.SelectedIndex].AppId; + rpc.Initialize(appId); } private void HandleConnectionError(string message) @@ -1734,47 +691,49 @@ private void HandleConnectionError(string message) lblStatus.Text = message; lblStatus.ForeColor = COLOR_ERROR; statusToolTip.SetToolTip(lblStatus, message); AdjustStatusVerticalPosition(); - btnConnect.Text = I18n.T("Button_Connect"); btnConnect.BackColor = COLOR_CONNECT; btnConnect.Enabled = true; + btnConnect.Text = I18n.T(Button_Connect); btnConnect.BackColor = COLOR_CONNECT; btnConnect.Enabled = true; btnUpdate.Enabled = false; btnUpdate.BackColor = COLOR_DISABLED; btnDisconnect.Enabled = false; btnDisconnect.BackColor = COLOR_DISABLED; - UpdateTrayStatusIcon(null); - UpdateTrayMenuState(); - client?.Dispose(); client = null; + _trayIconManager?.UpdateStatusIcon(null); + rpc.Deinitialize(); + _trayIconManager?.UpdateMenuState(rpc.Status); } private void PauseRpc() { - if (client?.IsInitialized == true) client.ClearPresence(); + if (rpc.IsInitialized == true) + rpc.Clear(); isManualPaused = true; UpdateUIForPause(); if (this.WindowState == FormWindowState.Minimized && (notifyInBackgroundItem?.Checked == true)) { trayIcon.Tag = null; - trayIcon.ShowBalloonTip(3000, I18n.T("App_Name"), I18n.T("Status_Disconnected_Notify"), ToolTipIcon.Info); + trayIcon.ShowBalloonTip(3000, I18n.T(App_Name), I18n.T(Status_Disconnected_Notify), ToolTipIcon.Info); } } + private void DisposeRpc() { connectionTimer?.Stop(); - if (client is not null) { client.ClearPresence(); client.Deinitialize(); client.Dispose(); client = null; } + rpc.Deinitialize(); isManualPaused = false; UpdateUIForDisconnect(); if (this.WindowState == FormWindowState.Minimized && (notifyInBackgroundItem?.Checked == true)) { trayIcon.Tag = null; - trayIcon.ShowBalloonTip(3000, I18n.T("App_Name"), I18n.T("Status_ManualDisconnected_Notify"), ToolTipIcon.Info); + trayIcon.ShowBalloonTip(3000, I18n.T(App_Name), I18n.T(Status_ManualDisconnected_Notify), ToolTipIcon.Info); } } private void UpdateUIForConnected(string username) { isManualPaused = false; - string status = I18n.T("Status_Connected", username); + string status = I18n.T(Status_Connected, username); lblStatus.Text = status; lblStatus.ForeColor = COLOR_CONNECT; statusToolTip.SetToolTip(lblStatus, status.Replace("\n", " ")); AdjustStatusVerticalPosition(); - btnConnect.Text = I18n.T("Button_Pause"); + btnConnect.Text = I18n.T(Button_Pause); btnConnect.BackColor = COLOR_PAUSE; btnConnect.Enabled = true; btnUpdate.Enabled = true; @@ -1783,17 +742,18 @@ private void UpdateUIForConnected(string username) btnDisconnect.BackColor = COLOR_ERROR; btnResetTime.Enabled = true; btnResetTime.BackColor = COLOR_PRIMARY; - UpdateTrayStatusIcon(COLOR_CONNECT, I18n.T("Tray_Status_Connected")); - UpdateTrayMenuState(); + _trayIconManager?.UpdateStatusIcon(COLOR_CONNECT, I18n.T(Tray_Status_Connected)); + _trayIconManager?.UpdateMenuState(rpc.Status); } + private void UpdateUIForPause() { - string status = I18n.T("Status_Paused"); + string status = I18n.T(Status_Paused); lblStatus.Text = status; lblStatus.ForeColor = COLOR_PAUSE; statusToolTip.SetToolTip(lblStatus, status.Replace("\n", " ")); AdjustStatusVerticalPosition(); - btnConnect.Text = I18n.T("Button_Resume"); + btnConnect.Text = I18n.T(Button_Resume); btnConnect.BackColor = COLOR_CONNECT; btnConnect.Enabled = true; btnUpdate.Enabled = false; @@ -1802,18 +762,19 @@ private void UpdateUIForPause() btnDisconnect.BackColor = COLOR_ERROR; btnResetTime.Enabled = true; btnResetTime.BackColor = COLOR_PRIMARY; - UpdateTrayStatusIcon(COLOR_PAUSE, I18n.T("Tray_Status_Paused")); - UpdateTrayMenuState(); + _trayIconManager?.UpdateStatusIcon(COLOR_PAUSE, I18n.T(Tray_Status_Paused)); + _trayIconManager?.UpdateMenuState(rpc.Status); } + private void UpdateUIForDisconnect() { connectionTimer?.Stop(); - string status = monitorItem.Checked ? I18n.T("Status_Disconnected_Auto") : I18n.T("Status_Disconnected"); + string status = monitorItem.Checked ? I18n.T(Status_Disconnected_Auto) : I18n.T(Status_Disconnected); lblStatus.Text = status; lblStatus.ForeColor = Color.Gray; statusToolTip.SetToolTip(lblStatus, status.Replace("\n", " ")); AdjustStatusVerticalPosition(); - btnConnect.Text = I18n.T("Button_Connect"); + btnConnect.Text = I18n.T(Button_Connect); btnConnect.BackColor = COLOR_CONNECT; btnConnect.Enabled = true; btnUpdate.Enabled = false; @@ -1822,119 +783,102 @@ private void UpdateUIForDisconnect() btnDisconnect.BackColor = COLOR_DISABLED; btnResetTime.Enabled = false; btnResetTime.BackColor = COLOR_DISABLED; - UpdateTrayStatusIcon(null); - UpdateTrayMenuState(); + _trayIconManager?.UpdateStatusIcon(null); + _trayIconManager?.UpdateMenuState(rpc.Status); } + private void UpdateRpc() { - if (client?.IsInitialized != true) return; - string SafeTrim(string text) => SafeTrimUtf8(text, 128); - bool IsValidUrl(string url) => !string.IsNullOrEmpty(url) && - (url.StartsWith("http://") || url.StartsWith("https://")) && - !JapaneseRegex().IsMatch(url); + if (rpc.Status != RpcStatus.Connected) + return; - string? pName = string.IsNullOrWhiteSpace(txtPName.Text) ? I18n.T("Placeholder_PName") : txtPName.Text; + string? pName = string.IsNullOrWhiteSpace(txtPName.Text) ? I18n.T(Placeholder_PName) : txtPName.Text; string? details = null; - string detailsStr = GetDetailsString(cmbDetailsType.SelectedIndex); - switch (detailsStr) { case "PName": details = $"{pName}"; break; case "PLv": details = $"PLv{numPLevel.Value}"; break; case "Both": details = $"{pName} | PLv{numPLevel.Value}"; break; } + var detailsType = GetDetailsType(cmbDetailsType.SelectedIndex); + switch (detailsType) + { + case PresenceDetailsType.PName: details = pName; break; + case PresenceDetailsType.PLv: details = $"PLv{numPLevel.Value}"; break; + case PresenceDetailsType.Both: details = $"{pName} | PLv{numPLevel.Value}"; break; + } string? state = null; - string stateStr = GetStateString(cmbStateType.SelectedIndex); + var stateType = GetStateType(cmbStateType.SelectedIndex); - if (stateStr == "PID") + if (stateType == PresenceStateType.PID) { - state = $"P-ID: {(string.IsNullOrWhiteSpace(txtStateCustom.Text) ? I18n.T("State_NotSet") : txtStateCustom.Text)}"; + state = $"P-ID: {(string.IsNullOrWhiteSpace(txtStateCustom.Text) ? I18n.T(State_NotSet) : txtStateCustom.Text)}"; } - else if (stateStr == "Idol" || stateStr == "Producing") + else if (stateType is PresenceStateType.Idol or PresenceStateType.Producing) { - string? charId = (stateStr == "Idol") ? currentPresence.SelectedIdolCharacterId : currentPresence.SelectedProduceCharacterId; - var pc = ProduceCharacters.Find(c => c.Id == charId); + string? charId = (stateType == PresenceStateType.Idol) ? CurrentPresence.SelectedIdolCharacterId : CurrentPresence.SelectedProduceCharacterId; + var pc = ProduceCharacters.FirstOrDefault(c => c.Id == charId); if (pc != null) { - string name = currentPresence.CharNameLangIndex == 1 ? pc.NameEn : pc.Display; - if (stateStr == "Producing") + string name = CurrentPresence.CharNameLangIndex == 1 ? pc.NameEn : pc.Display; + if (stateType == PresenceStateType.Producing) { - state = I18n.T("State_Producing_Format", name); + state = I18n.T(State_Producing_Format, name); } else { - state = I18n.T("State_Idol_Format", name); + state = I18n.T(State_Idol_Format, name); } } } - else if (stateStr == "Custom") + else if (stateType == PresenceStateType.Custom) { state = string.IsNullOrWhiteSpace(txtStateCustom.Text) ? "" : txtStateCustom.Text; } Button[]? buttons = null; - if (GetButtonModeString(cmbBtnMode.SelectedIndex) != "None") + if (GetButtonMode(cmbBtnMode.SelectedIndex) != PresenceButtonMode.None) { - string SafeTrimBtn(string t) => SafeTrimUtf8(t, 32); - var b1 = (!string.IsNullOrEmpty(txtBtn1Label.Text) && IsValidUrl(txtBtn1Url.Text)) ? new Button { Label = SafeTrimBtn(txtBtn1Label.Text), Url = txtBtn1Url.Text } : null; - var b2 = (!string.IsNullOrEmpty(txtBtn2Label.Text) && IsValidUrl(txtBtn2Url.Text)) ? new Button { Label = SafeTrimBtn(txtBtn2Label.Text), Url = txtBtn2Url.Text } : null; + var b1 = (!string.IsNullOrEmpty(txtBtn1Label.Text) && IsValidRpcButtonUrl(txtBtn1Url.Text)) ? new Button { Label = txtBtn1Label.Text, Url = txtBtn1Url.Text } : null; + var b2 = (!string.IsNullOrEmpty(txtBtn2Label.Text) && IsValidRpcButtonUrl(txtBtn2Url.Text)) ? new Button { Label = txtBtn2Label.Text, Url = txtBtn2Url.Text } : null; if (b1 != null && b2 != null) buttons = [b1, b2]; else if (b1 != null) buttons = [b1]; else if (b2 != null) buttons = [b2]; } if (!string.IsNullOrEmpty(details) && details.Length < 2) details = ""; if (!string.IsNullOrEmpty(state) && state.Length < 2) state = ""; - string gameName = GameApps[cmbGameName.SelectedIndex].Name; - try { client.SetPresence(new RichPresence { Details = SafeTrim(details ?? ""), State = SafeTrim(state ?? ""), Assets = new Assets { LargeImageKey = "app", LargeImageText = SafeTrimUtf8($"{I18n.T("App_Name")} v{Application.ProductVersion}", 128) }, Buttons = buttons, Timestamps = new Timestamps(startTime) }); } - catch (Exception ex) { Debug.WriteLine("RPC Update Error: " + ex.Message); } + try + { + rpc.UpdatePresence(new RichPresence + { + Details = details ?? "", + State = state ?? "", + Assets = new Assets + { + LargeImageKey = "app", + LargeImageText = $"{I18n.T(App_Name)} v{Application.ProductVersion}" + }, + Buttons = buttons, + Timestamps = new Timestamps(startTime) + }); + } + catch (Exception ex) + { + Debug.WriteLine("RPC Update Error: " + ex.Message); + } } - private void MonitorProcess(object? sender, EventArgs e) { var processes = Process.GetProcessesByName(PROCESS_NAME); bool isRunningNow = processes.Length > 0; if (isRunningNow && !wasProcessRunning) { startTime = DateTime.UtcNow; UpdateTimestampLabel(); InitializeRpc(); } else if (!isRunningNow && wasProcessRunning) { PauseRpc(); } wasProcessRunning = isRunningNow; } private void UpdateDetailsInputs(object? sender, EventArgs e) { - string detailsStr = GetDetailsString(cmbDetailsType.SelectedIndex); - txtPName.Enabled = (detailsStr == "PName" || detailsStr == "Both"); - numPLevel.Enabled = (detailsStr == "PLv" || detailsStr == "Both"); + var detailsType = GetDetailsType(cmbDetailsType.SelectedIndex); + txtPName.Enabled = (detailsType == PresenceDetailsType.PName || detailsType == PresenceDetailsType.Both); + numPLevel.Enabled = (detailsType == PresenceDetailsType.PLv || detailsType == PresenceDetailsType.Both); if (!isInitializing) SaveSettings(); } private void SetBtnInputsEnabled(bool e) { txtBtn1Label.Enabled = e; txtBtn1Url.Enabled = e; txtBtn2Label.Enabled = e; txtBtn2Url.Enabled = e; } private void UpdateTimestampLabel() { - bool isSessionActive = client?.IsInitialized == true; + bool isSessionActive = rpc.Status is RpcStatus.Connected or RpcStatus.Paused; TimeSpan ts = isSessionActive ? DateTime.UtcNow - startTime : TimeSpan.Zero; - lblStartTime.Text = $"{I18n.T("Timestamp_Label")}: {(int)ts.TotalHours:D2}:{ts.Minutes:D2}:{ts.Seconds:D2}"; + lblStartTime.Text = $"{I18n.T(Timestamp_Label)}: {(int)ts.TotalHours:D2}:{ts.Minutes:D2}:{ts.Seconds:D2}"; lblStartTime.ForeColor = this.BackColor.GetBrightness() > 0.5f ? Color.Black : Color.LightGray; } - private TextBox CreateText(Point loc, int w) - { - int autoHeight; - using (var temp = new ComboBox { Font = appFont, FlatStyle = FlatStyle.Flat, DropDownStyle = ComboBoxStyle.DropDownList }) - { - autoHeight = temp.PreferredHeight; - } - Panel container = new() { Location = loc, Size = new(w, autoHeight), BackColor = Color.FromArgb(47, 49, 54), Parent = this }; - container.Paint += (s, e) => { if (container.Tag is Color col) ControlPaint.DrawBorder(e.Graphics, container.ClientRectangle, col, ButtonBorderStyle.Solid); }; - TextBox tb = new() { BorderStyle = BorderStyle.None, BackColor = container.BackColor, ForeColor = Color.White, Font = appFont, Location = new(S(5), (autoHeight - TextRenderer.MeasureText("Ag", appFont).Height) / 2 - S(1)), Width = w - S(10) }; - container.Controls.Add(tb); - tb.Tag = container; return tb; - } - - private ComboBox CreateCombo(Point loc, int w, string[] i) - { - var cb = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Color.FromArgb(47, 49, 54), ForeColor = Color.White, FlatStyle = FlatStyle.Flat, Parent = this, Font = appFont, Location = loc, Width = w }; - cb.Items.AddRange(i); return cb; - } - - private NumericUpDown CreateNumeric(Point loc, int w) - { - int autoHeight; - using (var temp = new ComboBox { Font = appFont, FlatStyle = FlatStyle.Flat, DropDownStyle = ComboBoxStyle.DropDownList }) - { - autoHeight = temp.PreferredHeight; - } - Panel container = new() { Location = loc, Size = new(w, autoHeight), BackColor = Color.FromArgb(47, 49, 54), Parent = this }; - container.Paint += (s, e) => { if (container.Tag is Color col) ControlPaint.DrawBorder(e.Graphics, container.ClientRectangle, col, ButtonBorderStyle.Solid); }; - NumericUpDown nud = new() { BorderStyle = BorderStyle.None, BackColor = container.BackColor, ForeColor = Color.White, Font = appFont, Location = new(S(5), (autoHeight - TextRenderer.MeasureText("Ag", appFont).Height) / 2 - S(1)), Width = w - S(10), Minimum = 1, Maximum = 100 }; - container.Controls.Add(nud); - nud.Tag = container; return nud; - } - private void AdjustStatusVerticalPosition() { if (lblStatus == null || btnConnect == null) return; @@ -1945,110 +889,17 @@ private void AdjustStatusVerticalPosition() private void RefreshProduceCharacterList() { - string? currentId = (GetStateString(cmbStateType.SelectedIndex) == "Idol") ? currentPresence.SelectedIdolCharacterId : currentPresence.SelectedProduceCharacterId; + string? currentId = (GetStateType(cmbStateType.SelectedIndex) == PresenceStateType.Idol) ? CurrentPresence.SelectedIdolCharacterId : CurrentPresence.SelectedProduceCharacterId; cmbProduceCharacter.Items.Clear(); - bool isEn = currentPresence.CharNameLangIndex == 1; + bool isEn = CurrentPresence.CharNameLangIndex == 1; foreach (var c in ProduceCharacters) { cmbProduceCharacter.Items.Add(isEn ? c.NameEn : c.Display); } - int idx = ProduceCharacters.FindIndex(c => c.Id == currentId); + int idx = FindProduceCharacterIndex(currentId); cmbProduceCharacter.SelectedIndex = idx >= 0 ? idx : 0; } - - private System.Windows.Forms.Button CreateButton(string t, Point l, int w, Color bc) - { - int btnHeight = Math.Max(S(35), TextRenderer.MeasureText("Ag", appFontBold).Height + S(15)); - return new System.Windows.Forms.Button { Text = t, Location = l, Size = new Size(w, btnHeight), BackColor = bc, FlatStyle = FlatStyle.Flat, ForeColor = Color.White, Font = appFontBold, Parent = this, Cursor = Cursors.Hand }; - } - - private static string SafeTrimUtf8(string text, int maxBytes = 31) - { - if (string.IsNullOrEmpty(text)) return text; - var enc = Encoding.UTF8; - if (enc.GetByteCount(text) <= maxBytes) return text; - int lo = 0, hi = text.Length; - while (lo < hi) { int mid = (lo + hi + 1) / 2; if (enc.GetByteCount(text[..mid]) <= maxBytes) lo = mid; else hi = mid - 1; } - return text[..lo]; - } - - [System.Text.RegularExpressions.GeneratedRegex(@"[\p{IsHiragana}\p{IsKatakana}\p{IsCJKUnifiedIdeographs}]")] - private static partial System.Text.RegularExpressions.Regex JapaneseRegex(); - - private void UpdateTrayStatusIcon(Color? color, string? statusText = null) - { - if (trayIcon == null) return; - - var baseIcon = this.Icon ?? SystemIcons.Application; - string appDisplayName = I18n.T("App_Name"); - trayIcon.Text = string.IsNullOrEmpty(statusText) ? appDisplayName : $"{appDisplayName} - {statusText}"; - - if (color == null) - { - trayIcon.Icon = baseIcon; - currentTrayIconStatus?.Dispose(); - currentTrayIconStatus = null; - return; - } - - try - { - using var bitmap = baseIcon.ToBitmap(); - using (var g = Graphics.FromImage(bitmap)) - { - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; - int size = bitmap.Width / 4; - int x = bitmap.Width - size - 2; - int y = 2; - - using var brush = new SolidBrush(color.Value); - g.FillEllipse(brush, x, y, size, size); - - using var pen = new Pen(Color.White, 1); - g.DrawEllipse(pen, x, y, size, size); - } - - var newIcon = Icon.FromHandle(bitmap.GetHicon()); - trayIcon.Icon = newIcon; - - currentTrayIconStatus?.Dispose(); - currentTrayIconStatus = newIcon; - } - catch (Exception ex) - { - Debug.WriteLine("Tray Icon Status Draw Error: " + ex.Message); - } - } - - private void UpdateTrayMenuState() - { - if (trayIcon.ContextMenuStrip == null) return; - - bool isConnected = client?.IsInitialized == true; - bool isPaused = isConnected && isManualPaused; - - bool showSettings = isConnected && !isPaused; - - trayMenuDetails.Visible = showSettings; - trayMenuState.Visible = showSettings; - trayMenuProduce.Visible = showSettings; - - trayMenuConnect.Visible = !isConnected || isPaused; - trayMenuPause.Visible = isConnected && !isPaused; - trayMenuDisconnect.Visible = isConnected; - - foreach (ToolStripItem item in trayIcon.ContextMenuStrip.Items) - { - if (item is ToolStripSeparator sep) - { - if (sep.Tag?.ToString() == "SepSettings") - { - sep.Visible = showSettings; - } - } - } - } } } \ No newline at end of file diff --git a/GkmStatus/MainForm.resx b/GkmStatus/MainForm.resx index 1af7de1..9954251 100644 --- a/GkmStatus/MainForm.resx +++ b/GkmStatus/MainForm.resx @@ -1,17 +1,17 @@  - @@ -117,4 +117,22 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 17, 17 + + + 148, 17 + + + 290, 17 + + + 526, 17 + + + 721, 17 + + + 892, 17 + \ No newline at end of file diff --git a/GkmStatus/Properties/Resources.Designer.cs b/GkmStatus/Properties/Resources.Designer.cs new file mode 100644 index 0000000..4861f64 --- /dev/null +++ b/GkmStatus/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// このコードはツールによって生成されました。 +// ランタイム バージョン:4.0.30319.42000 +// +// このファイルへの変更は、以下の状況下で不正な動作の原因になったり、 +// コードが再生成されるときに損失したりします。 +// +//------------------------------------------------------------------------------ + +namespace GkmStatus.Properties { + using System; + + + /// + /// ローカライズされた文字列などを検索するための、厳密に型指定されたリソース クラスです。 + /// + // このクラスは StronglyTypedResourceBuilder クラスが ResGen + // または Visual Studio のようなツールを使用して自動生成されました。 + // メンバーを追加または削除するには、.ResX ファイルを編集して、/str オプションと共に + // ResGen を実行し直すか、または VS プロジェクトをビルドし直します。 + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// このクラスで使用されているキャッシュされた ResourceManager インスタンスを返します。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GkmStatus.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// すべてについて、現在のスレッドの CurrentUICulture プロパティをオーバーライドします + /// 現在のスレッドの CurrentUICulture プロパティをオーバーライドします。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/GkmStatus/Properties/Resources.resx b/GkmStatus/Properties/Resources.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GkmStatus/Properties/Resources.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GkmStatus/Resources/I18n/english.json b/GkmStatus/Resources/I18n/english.json new file mode 100644 index 0000000..0a1ea5d --- /dev/null +++ b/GkmStatus/Resources/I18n/english.json @@ -0,0 +1,433 @@ +{ + "english": [ + { + "key": "App_Name", + "text": "GkmStatus" + }, + + { + "key": "Menu_File", + "text": "&File" + }, + { + "key": "Menu_Exit", + "text": "Exit" + }, + { + "key": "Menu_Settings", + "text": "&Settings" + }, + { + "key": "Menu_RunAtStartup", + "text": "Run at system startup" + }, + { + "key": "Menu_StartMinimized", + "text": "Start minimized in tray" + }, + { + "key": "Menu_AutoConnect", + "text": "Try connecting on startup" + }, + { + "key": "Menu_CheckForUpdates", + "text": "Check for updates on startup" + }, + { + "key": "Menu_NotifyInBackground", + "text": "Notify in background" + }, + { + "key": "Menu_NotifyOnMinimize", + "text": "Notify on minimize" + }, + { + "key": "Menu_MinimizeToTray", + "text": "Minimize to tray on close" + }, + { + "key": "Menu_MonitorProcess", + "text": "Monitor gakumas.exe and auto connect/disconnect" + }, + { + "key": "Menu_Language", + "text": "Language" + }, + { + "key": "Menu_View", + "text": "&View" + }, + { + "key": "Menu_Theme", + "text": "Theme" + }, + { + "key": "Menu_ThemeAuto", + "text": "Automatic" + }, + { + "key": "Menu_ThemeLight", + "text": "Light" + }, + { + "key": "Menu_ThemeDark", + "text": "Dark" + }, + { + "key": "Menu_ThemeOLED", + "text": "OLED" + }, + { + "key": "Menu_Help", + "text": "&Help" + }, + { + "key": "Menu_Github", + "text": "Open GitHub page..." + }, + { + "key": "Menu_CheckUpdateNow", + "text": "Check for Updates..." + }, + { + "key": "Menu_About", + "text": "About this app..." + }, + { + "key": "Menu_OpenAppLocation", + "text": "Open app location..." + }, + { + "key": "Menu_OpenConfigLocation", + "text": "Open config location..." + }, + { + "key": "Tray_Open", + "text": "Open" + }, + { + "key": "Tray_ProducingIdol", + "text": "Producing Idol" + }, + { + "key": "Tray_Exit", + "text": "Exit" + }, + + { + "key": "Header_GameName", + "text": "Game Display Name (Name)" + }, + { + "key": "Header_Details", + "text": "Line 1 Info (Details)" + }, + { + "key": "Header_State", + "text": "Line 2 Info (State)" + }, + { + "key": "Header_Timestamp", + "text": "Elapsed Time (Timestamp)" + }, + { + "key": "Header_Buttons", + "text": "External Link Settings (Buttons)" + }, + + { + "key": "Details_None", + "text": "None" + }, + { + "key": "Details_PName", + "text": "Producer Name" + }, + { + "key": "Details_PLv", + "text": "Producer Level" + }, + { + "key": "Details_Both", + "text": "P-Name and PLv" + }, + { + "key": "Placeholder_PName", + "text": "Producer" + }, + + { + "key": "State_None", + "text": "None" + }, + { + "key": "State_PID", + "text": "P-ID" + }, + { + "key": "State_Producing", + "text": "Producing..." + }, + { + "key": "State_Idol", + "text": "In Charge" + }, + { + "key": "State_Custom", + "text": "Custom" + }, + { + "key": "Placeholder_PID", + "text": "Enter 8-digit P-ID" + }, + { + "key": "Placeholder_Custom", + "text": "Enter text (2-128 characters)" + }, + { + "key": "State_ProducingSuffix", + "text": " producing" + }, + { + "key": "State_IdolSuffix", + "text": "Producer of " + }, + { + "key": "State_NotSet", + "text": "Not Set" + }, + { + "key": "State_Producing_Format", + "text": "Producing {0}" + }, + { + "key": "State_Idol_Format", + "text": "Producer of {0}" + }, + + { + "key": "Timestamp_Label", + "text": "Elapsed" + }, + { + "key": "Timestamp_Reset", + "text": "Reset Timer" + }, + { + "key": "Timestamp_Guide", + "text": "Click 'Update' to apply" + }, + + { + "key": "Button_Mode", + "text": "Button Mode" + }, + { + "key": "Button_ModeNone", + "text": "None" + }, + { + "key": "Button_ModeStore", + "text": "Link to Game" + }, + { + "key": "Button_ModeApp", + "text": "Link to this app" + }, + { + "key": "Button_ModeCustom", + "text": "Custom" + }, + { + "key": "Button_ModeNote", + "text": "Only visible to others (Discord limitation)" + }, + { + "key": "Placeholder_BtnLabel", + "text": "Button {0} Label" + }, + { + "key": "Placeholder_BtnUrl", + "text": "Button {0} URL" + }, + { + "key": "Button_Warning_LabelLength", + "text": "Multi-byte chars will show up to 10 chars." + }, + { + "key": "Button_Warning_UrlJp", + "text": "Japanese characters are not allowed in URLs.\nPlease use percent-encoded strings." + }, + + { + "key": "Status_Disconnected", + "text": "Disconnected" + }, + { + "key": "Status_Disconnected_Auto", + "text": "Disconnected\n(Auto-connect enabled)" + }, + { + "key": "Status_Connected", + "text": "Connected:\n{0}" + }, + { + "key": "Status_Connected_Notify", + "text": "Connected to Discord and started displaying status." + }, + { + "key": "Status_Disconnected_Notify", + "text": "Disconnected from Discord." + }, + { + "key": "Status_ManualDisconnected_Notify", + "text": "Disconnected from Discord manually." + }, + { + "key": "Status_Connecting", + "text": "Connecting... ({0}s)" + }, + { + "key": "Status_Paused", + "text": "Connected\n(Display Paused)" + }, + { + "key": "Status_Timeout", + "text": "Timeout: \nPlease check Discord." + }, + { + "key": "Status_Error", + "text": "Error: {0}" + }, + { + "key": "Status_InitFailed", + "text": "Initialization Failed." + }, + { + "key": "Status_Exception", + "text": "Exception: {0}" + }, + { + "key": "Status_Updated", + "text": "Updated" + }, + { + "key": "Tray_Status_Connected", + "text": "Connected" + }, + { + "key": "Tray_Status_Paused", + "text": "Paused" + }, + { + "key": "Tray_Status_Connecting", + "text": "Connecting" + }, + + { + "key": "Button_Connect", + "text": "Connect" + }, + { + "key": "Button_Pause", + "text": "Pause" + }, + { + "key": "Button_Resume", + "text": "Resume" + }, + { + "key": "Button_Update", + "text": "Update" + }, + { + "key": "Button_Disconnect", + "text": "Disconnect" + }, + { + "key": "Button_StoreLabel_Mobile", + "text": "Play (iOS / Android)" + }, + { + "key": "Button_StoreLabel_DMM", + "text": "Play (DMM GAMES)" + }, + { + "key": "Button_AboutPresence", + "text": "About this presence..." + }, + + { + "key": "GameApp_Guide", + "text": "Changes apply after\ndisconnecting and reconnecting" + }, + { + "key": "About_Message", + "text": "GkmStatus\nv{0}\n\nMade by: Wea017net" + }, + { + "key": "About_Author", + "text": "Made by: Wea017net" + }, + { + "key": "About_FontAttribution", + "text": "Font: IBM Plex Sans JP" + }, + { + "key": "About_FontLicense", + "text": "View License" + }, + { + "key": "About_Title", + "text": "About this app" + }, + { + "key": "Error_Browser", + "text": "Could not open the browser." + }, + { + "key": "CharName_JP", + "text": "Japanese Names" + }, + { + "key": "CharName_EN", + "text": "English Names" + }, + { + "key": "Update_NotificationTitle", + "text": "New Version Available" + }, + { + "key": "Update_NotificationBody", + "text": "Version {0} is now available.\nClick here to open the download page." + }, + { + "key": "Update_NewAvailable", + "text": "A new version {0} is available.\nWould you like to open the download page?" + }, + { + "key": "Update_NoUpdate", + "text": "You are using the latest version." + }, + { + "key": "Update_RateLimit", + "text": "GitHub API rate limit exceeded. Please try again later." + }, + { + "key": "Notify_Minimized", + "text": "Minimized to system tray." + }, + { + "key": "Notify_TrayIdolChanged", + "text": "Applied the producing idol change and updated the presence." + }, + { + "key": "Notify_TrayDetailsChanged", + "text": "Applied the Details change and updated the presence." + }, + { + "key": "Notify_TrayStateChanged", + "text": "Applied the State change and updated the presence." + } + ] +} \ No newline at end of file diff --git a/GkmStatus/Resources/I18n/japanese.json b/GkmStatus/Resources/I18n/japanese.json new file mode 100644 index 0000000..d433983 --- /dev/null +++ b/GkmStatus/Resources/I18n/japanese.json @@ -0,0 +1,439 @@ +{ + "japanese": [ + { + "key": "App_Name", + "text": "学マステータス" + }, + { + "key": "Menu_File", + "text": "ファイル(&F)" + }, + { + "key": "Menu_Exit", + "text": "終了" + }, + { + "key": "Menu_Settings", + "text": "設定(&S)" + }, + { + "key": "Menu_RunAtStartup", + "text": "システム起動時に実行" + }, + { + "key": "Menu_StartMinimized", + "text": "最小化した状態(システムトレイ内)で起動" + }, + { + "key": "Menu_AutoConnect", + "text": "起動時に接続を試行" + }, + { + "key": "Menu_CheckForUpdates", + "text": "起動時にアプリの更新を確認" + }, + { + "key": "Menu_NotifyInBackground", + "text": "バックグラウンド動作時に通知" + }, + { + "key": "Menu_NotifyOnMinimize", + "text": "最小化時に通知" + }, + { + "key": "Menu_MinimizeToTray", + "text": "×でアプリを最小化(システムトレイに格納)" + }, + { + "key": "Menu_MonitorProcess", + "text": "gakumas.exeを監視して自動で接続/切断" + }, + { + "key": "Menu_Language", + "text": "言語" + }, + { + "key": "Menu_View", + "text": "表示(&V)" + }, + { + "key": "Menu_Theme", + "text": "テーマ" + }, + { + "key": "Menu_ThemeAuto", + "text": "自動選択" + }, + { + "key": "Menu_ThemeLight", + "text": "ライト" + }, + { + "key": "Menu_ThemeDark", + "text": "ダーク" + }, + { + "key": "Menu_ThemeOLED", + "text": "OLED" + }, + { + "key": "Menu_Help", + "text": "ヘルプ(&H)" + }, + { + "key": "Menu_Github", + "text": "GitHubページを開く..." + }, + { + "key": "Menu_CheckUpdateNow", + "text": "更新を確認..." + }, + { + "key": "Menu_About", + "text": "このアプリについて..." + }, + { + "key": "Menu_OpenAppLocation", + "text": "このアプリの場所を開く..." + }, + { + "key": "Menu_OpenConfigLocation", + "text": "設定ファイルの場所を開く..." + }, + { + "key": "Tray_Open", + "text": "開く" + }, + { + "key": "Tray_ProducingIdol", + "text": "プロデュース中のアイドル" + }, + { + "key": "Tray_Exit", + "text": "終了" + }, + { + "key": "Header_GameName", + "text": "ゲームの表示名 (Name)" + }, + { + "key": "Header_Details", + "text": "1行目の情報 (Details)" + }, + { + "key": "Header_State", + "text": "2行目の情報 (State)" + }, + { + "key": "Header_Timestamp", + "text": "経過時間 (Timestamp)" + }, + { + "key": "Header_Buttons", + "text": "外部リンクボタン設定 (Buttons)" + }, + { + "key": "Details_None", + "text": "なし" + }, + { + "key": "Details_PName", + "text": "プロデューサー名" + }, + + { + "key": "Details_None", + "text": "なし" + }, + { + "key": "Details_PName", + "text": "プロデューサー名" + }, + { + "key": "Details_PLv", + "text": "プロデューサーレベル" + }, + { + "key": "Details_Both", + "text": "プロデューサー名とPLv" + }, + { + "key": "Placeholder_PName", + "text": "プロデューサー" + }, + + { + "key": "State_None", + "text": "なし" + }, + { + "key": "State_PID", + "text": "P-ID" + }, + { + "key": "State_Producing", + "text": "プロデュース中" + }, + { + "key": "State_Idol", + "text": "担当アイドル" + }, + { + "key": "State_Custom", + "text": "カスタム" + }, + { + "key": "Placeholder_PID", + "text": "8桁のP-IDを入力" + }, + { + "key": "Placeholder_Custom", + "text": "自由入力 (全角: 2~42 文字 / 半角: 2~128 文字)" + }, + { + "key": "State_ProducingSuffix", + "text": "をプロデュース中" + }, + { + "key": "State_IdolSuffix", + "text": " 担当" + }, + { + "key": "State_NotSet", + "text": "未設定" + }, + { + "key": "State_Producing_Format", + "text": "{0}をプロデュース中" + }, + { + "key": "State_Idol_Format", + "text": "{0} 担当" + }, + + { + "key": "Timestamp_Label", + "text": "経過時間" + }, + { + "key": "Timestamp_Reset", + "text": "時間をリセット" + }, + { + "key": "Timestamp_Guide", + "text": "「更新」を押すと反映されます" + }, + + { + "key": "Button_Mode", + "text": "ボタン表示モード" + }, + { + "key": "Button_ModeNone", + "text": "なし" + }, + { + "key": "Button_ModeStore", + "text": "ゲームに誘導" + }, + { + "key": "Button_ModeApp", + "text": "このアプリに誘導" + }, + { + "key": "Button_ModeCustom", + "text": "カスタム" + }, + { + "key": "Button_ModeNote", + "text": "Discordの仕様により自分からは確認できません" + }, + { + "key": "Placeholder_BtnLabel", + "text": "ボタン{0}つ目のラベル" + }, + { + "key": "Placeholder_BtnUrl", + "text": "ボタン{0}つ目のURL" + }, + { + "key": "Button_Warning_LabelLength", + "text": "全角の場合は最大10文字まで表示されます。" + }, + { + "key": "Button_Warning_UrlJp", + "text": "URLに日本語を含めることはできません。\nパーセントエンコーディング後の文字列を使用してください。" + }, + + { + "key": "Status_Connected", + "text": "接続完了:\n{0}" + }, + { + "key": "Status_Connected_Notify", + "text": "Discordに接続してステータスの表示を開始しました。" + }, + { + "key": "Status_Disconnected", + "text": "未接続" + }, + { + "key": "Status_Disconnected_Auto", + "text": "未接続\n(自動接続が有効)" + }, + { + "key": "Status_Disconnected_Notify", + "text": "Discordへのステータス表示を停止しました。" + }, + { + "key": "Status_ManualDisconnected_Notify", + "text": "アプリとDiscordの接続を切断しました。" + }, + { + "key": "Status_Connecting", + "text": "接続試行中... ({0}秒)" + }, + { + "key": "Status_Paused", + "text": "接続済み\n(表示を停止中)" + }, + { + "key": "Status_Timeout", + "text": "タイムアウト:\nDiscordの起動を要確認" + }, + { + "key": "Status_Error", + "text": "エラー: {0}" + }, + { + "key": "Status_InitFailed", + "text": "初期化失敗。" + }, + { + "key": "Status_Exception", + "text": "例外: {0}" + }, + { + "key": "Status_Updated", + "text": "更新しました" + }, + { + "key": "Tray_Status_Connected", + "text": "接続完了" + }, + { + "key": "Tray_Status_Paused", + "text": "表示停止中" + }, + { + "key": "Tray_Status_Connecting", + "text": "接続試行中" + }, + + { + "key": "Button_Connect", + "text": "接続" + }, + { + "key": "Button_Pause", + "text": "一時停止" + }, + { + "key": "Button_Resume", + "text": "再開" + }, + { + "key": "Button_Update", + "text": "更新" + }, + { + "key": "Button_Disconnect", + "text": "切断" + }, + { + "key": "Button_StoreLabel_Mobile", + "text": "プレイ (iOS / Android)" + }, + { + "key": "Button_StoreLabel_DMM", + "text": "プレイ (DMM GAMES)" + }, + { + "key": "Button_AboutPresence", + "text": "この表示について..." + }, + + { + "key": "GameApp_Guide", + "text": "この変更は一度切断して\n再接続したあとに適用されます" + }, + { + "key": "About_Message", + "text": "GkmStatus\nv{0}\n\n制作者: Wea017net" + }, + { + "key": "About_Author", + "text": "制作者: Wea017net" + }, + { + "key": "About_FontAttribution", + "text": "使用フォント: IBM Plex Sans JP" + }, + { + "key": "About_FontLicense", + "text": "ライセンスを表示" + }, + { + "key": "About_Title", + "text": "このアプリについて" + }, + { + "key": "Error_Browser", + "text": "ブラウザを開けませんでした。" + }, + { + "key": "CharName_JP", + "text": "日本語表記" + }, + { + "key": "CharName_EN", + "text": "英語表記" + }, + { + "key": "Update_NotificationTitle", + "text": "新しいバージョンが利用可能です" + }, + { + "key": "Update_NotificationBody", + "text": "新しいバージョン {0} が公開されています。\nクリックしてダウンロードページを開きます。" + }, + { + "key": "Update_NewAvailable", + "text": "新しいバージョン {0} が公開されています。\nダウンロードページを開きますか?" + }, + { + "key": "Update_NoUpdate", + "text": "最新バージョンを使用しています。" + }, + { + "key": "Update_RateLimit", + "text": "GitHub APIの回数制限に達しました。しばらく時間をおいてから再度お試しください。" + }, + { + "key": "Notify_Minimized", + "text": "システムトレイに最小化しました。" + }, + { + "key": "Notify_TrayIdolChanged", + "text": "プロデュース中のアイドルの変更を適用して情報を更新しました。" + }, + { + "key": "Notify_TrayDetailsChanged", + "text": "1行目の情報(Details)の変更を適用して情報を更新しました。" + }, + { + "key": "Notify_TrayStateChanged", + "text": "2行目の情報(State)の変更を適用して情報を更新しました。" + } + ] +} \ No newline at end of file diff --git a/GkmStatus/Resources/produce_characters.json b/GkmStatus/Resources/produce_characters.json new file mode 100644 index 0000000..cd35399 --- /dev/null +++ b/GkmStatus/Resources/produce_characters.json @@ -0,0 +1,69 @@ + { + "characters": [ + { + "Id": "hanami_saki", + "Display": "花海咲季", + "NameEn": "Saki Hanami" + }, + { + "Id": "tsukimura_temari", + "Display": "月村手毬", + "NameEn": "Temari Tsukimura" + }, + { + "Id": "fujita_kotone", + "Display": "藤田ことね", + "NameEn": "Kotone Fujita" + }, + { + "Id": "amaya_tsubame", + "Display": "雨夜燕", + "NameEn": "Tsubame Amaya" + }, + { + "Id": "arimura_mao", + "Display": "有村麻央", + "NameEn": "Mao Arimura" + }, + { + "Id": "katsuragi_lilja", + "Display": "葛城リーリヤ", + "NameEn": "Lilja Katsuragi" + }, + { + "Id": "kuramoto_china", + "Display": "倉本千奈", + "NameEn": "China Kuramoto" + }, + { + "Id": "shiun_sumika", + "Display": "紫雲清夏", + "NameEn": "Sumika Shiun" + }, + { + "Id": "shinosawa_hiro", + "Display": "篠澤広", + "NameEn": "Hiro Shinosawa" + }, + { + "Id": "juo_sena", + "Display": "十王星南", + "NameEn": "Sena Juo" + }, + { + "Id": "hataya_misuzu", + "Display": "秦谷美鈴", + "NameEn": "Misuzu Hataya" + }, + { + "Id": "hanami_ume", + "Display": "花海佑芽", + "NameEn": "Ume Hanami" + }, + { + "Id": "himesaki_rinami", + "Display": "姫崎莉波", + "NameEn": "Rinami Himesaki" + } + ] +} \ No newline at end of file diff --git a/GkmStatus/Translation.cs b/GkmStatus/Translation.cs deleted file mode 100644 index cdef901..0000000 --- a/GkmStatus/Translation.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System.Collections.Generic; - -namespace GkmStatus -{ - public static class I18n - { - public static string CurrentLanguage { get; set; } = "日本語"; - - private static readonly Dictionary> Translations = new() - { - ["日本語"] = new Dictionary - { - ["App_Name"] = "学マステータス", - - ["Menu_File"] = "ファイル(&F)", - ["Menu_Exit"] = "終了", - ["Menu_Settings"] = "設定(&S)", - ["Menu_RunAtStartup"] = "システム起動時に実行", - ["Menu_StartMinimized"] = "最小化した状態(システムトレイ内)で起動", - ["Menu_AutoConnect"] = "起動時に接続を試行", - ["Menu_CheckForUpdates"] = "起動時にアプリの更新を確認", - ["Menu_NotifyInBackground"] = "バックグラウンド動作時に通知", - ["Menu_NotifyOnMinimize"] = "最小化時に通知", - ["Menu_MinimizeToTray"] = "×でアプリを最小化(システムトレイに格納)", - ["Menu_MonitorProcess"] = "gakumas.exeを監視して自動で接続/切断", - ["Menu_Language"] = "言語", - ["Menu_View"] = "表示(&V)", - ["Menu_Theme"] = "テーマ", - ["Menu_ThemeAuto"] = "自動選択", - ["Menu_ThemeLight"] = "ライト", - ["Menu_ThemeDark"] = "ダーク", - ["Menu_ThemeOLED"] = "OLED", - ["Menu_Help"] = "ヘルプ(&H)", - ["Menu_Github"] = "GitHubページを開く...", - ["Menu_CheckUpdateNow"] = "更新を確認...", - ["Menu_About"] = "このアプリについて...", - ["Menu_OpenAppLocation"] = "このアプリの場所を開く...", - ["Menu_OpenConfigLocation"] = "設定ファイルの場所を開く...", - ["Tray_Open"] = "開く", - ["Tray_ProducingIdol"] = "プロデュース中のアイドル", - ["Tray_Exit"] = "終了", - - ["Header_GameName"] = "ゲームの表示名 (Name)", - ["Header_Details"] = "1行目の情報 (Details)", - ["Header_State"] = "2行目の情報 (State)", - ["Header_Timestamp"] = "経過時間 (Timestamp)", - ["Header_Buttons"] = "外部リンクボタン設定 (Buttons)", - - ["Details_None"] = "なし", - ["Details_PName"] = "プロデューサー名", - ["Details_PLv"] = "プロデューサーレベル", - ["Details_Both"] = "プロデューサー名とPLv", - ["Placeholder_PName"] = "プロデューサー", - - ["State_None"] = "なし", - ["State_PID"] = "P-ID", - ["State_Producing"] = "プロデュース中", - ["State_Idol"] = "担当アイドル", - ["State_Custom"] = "カスタム", - ["Placeholder_PID"] = "8桁のP-IDを入力", - ["Placeholder_Custom"] = "自由入力 (全角: 2~42 文字 / 半角: 2~128 文字)", - ["State_ProducingSuffix"] = "をプロデュース中", - ["State_IdolSuffix"] = " 担当", - ["State_NotSet"] = "未設定", - ["State_Producing_Format"] = "{0}をプロデュース中", - ["State_Idol_Format"] = "{0} 担当", - - ["Timestamp_Label"] = "経過時間", - ["Timestamp_Reset"] = "時間をリセット", - ["Timestamp_Guide"] = "「更新」を押すと反映されます", - - ["Button_Mode"] = "ボタン表示モード", - ["Button_ModeNone"] = "なし", - ["Button_ModeStore"] = "ゲームに誘導", - ["Button_ModeApp"] = "このアプリに誘導", - ["Button_ModeCustom"] = "カスタム", - ["Button_ModeNote"] = "Discordの仕様により自分からは確認できません", - ["Placeholder_BtnLabel"] = "ボタン{0}つ目のラベル", - ["Placeholder_BtnUrl"] = "ボタン{0}つ目のURL", - ["Button_Warning_LabelLength"] = "全角の場合は最大10文字まで表示されます。", - ["Button_Warning_UrlJp"] = "URLに日本語を含めることはできません。\nパーセントエンコーディング後の文字列を使用してください。", - - ["Status_Connected"] = "接続完了:\n{0}", - ["Status_Connected_Notify"] = "Discordに接続してステータスの表示を開始しました。", - ["Status_Disconnected"] = "未接続", - ["Status_Disconnected_Auto"] = "未接続\n(自動接続が有効)", - ["Status_Disconnected_Notify"] = "Discordへのステータス表示を停止しました。", - ["Status_ManualDisconnected_Notify"] = "アプリとDiscordの接続を切断しました。", - ["Status_Connecting"] = "接続試行中... ({0}秒)", - ["Status_Paused"] = "接続済み\n(表示を停止中)", - ["Status_Timeout"] = "タイムアウト:\nDiscordの起動を要確認", - ["Status_Error"] = "エラー: {0}", - ["Status_InitFailed"] = "初期化失敗。", - ["Status_Exception"] = "例外: {0}", - ["Status_Updated"] = "更新しました", - ["Tray_Status_Connected"] = "接続完了", - ["Tray_Status_Paused"] = "表示停止中", - ["Tray_Status_Connecting"] = "接続試行中", - - ["Button_Connect"] = "接続", - ["Button_Pause"] = "一時停止", - ["Button_Resume"] = "再開", - ["Button_Update"] = "更新", - ["Button_Disconnect"] = "切断", - ["Button_StoreLabel_Mobile"] = "プレイ (iOS / Android)", - ["Button_StoreLabel_DMM"] = "プレイ (DMM GAMES)", - ["Button_AboutPresence"] = "この表示について...", - - ["GameApp_Guide"] = "この変更は一度切断して\n再接続したあとに適用されます", - ["About_Message"] = "GkmStatus\nv{0}\n\n制作者: Wea017net", - ["About_Author"] = "制作者: Wea017net", - ["About_FontAttribution"] = "使用フォント: IBM Plex Sans JP", - ["About_FontLicense"] = "ライセンスを表示", - ["About_Title"] = "このアプリについて", - ["Error_Browser"] = "ブラウザを開けませんでした。", - ["CharName_JP"] = "日本語表記", - ["CharName_EN"] = "英語表記", - ["Update_NotificationTitle"] = "新しいバージョンが利用可能です", - ["Update_NotificationBody"] = "新しいバージョン {0} が公開されています。\nクリックしてダウンロードページを開きます。", - ["Update_NewAvailable"] = "新しいバージョン {0} が公開されています。\nダウンロードページを開きますか?", - ["Update_NoUpdate"] = "最新バージョンを使用しています。", - ["Update_RateLimit"] = "GitHub APIの回数制限に達しました。しばらく時間をおいてから再度お試しください。", - ["Notify_Minimized"] = "システムトレイに最小化しました。", - ["Notify_TrayIdolChanged"] = "プロデュース中のアイドルの変更を適用して情報を更新しました。", - ["Notify_TrayDetailsChanged"] = "1行目の情報(Details)の変更を適用して情報を更新しました。", - ["Notify_TrayStateChanged"] = "2行目の情報(State)の変更を適用して情報を更新しました。" - }, - ["English"] = new Dictionary - { - ["App_Name"] = "GkmStatus", - - ["Menu_File"] = "&File", - ["Menu_Exit"] = "Exit", - ["Menu_Settings"] = "&Settings", - ["Menu_RunAtStartup"] = "Run at system startup", - ["Menu_StartMinimized"] = "Start minimized in tray", - ["Menu_AutoConnect"] = "Try connecting on startup", - ["Menu_CheckForUpdates"] = "Check for updates on startup", - ["Menu_NotifyInBackground"] = "Notify in background", - ["Menu_NotifyOnMinimize"] = "Notify on minimize", - ["Menu_MinimizeToTray"] = "Minimize to tray on close", - ["Menu_MonitorProcess"] = "Monitor gakumas.exe and auto connect/disconnect", - ["Menu_Language"] = "Language", - ["Menu_View"] = "&View", - ["Menu_Theme"] = "Theme", - ["Menu_ThemeAuto"] = "Automatic", - ["Menu_ThemeLight"] = "Light", - ["Menu_ThemeDark"] = "Dark", - ["Menu_ThemeOLED"] = "OLED", - ["Menu_Help"] = "&Help", - ["Menu_Github"] = "Open GitHub page...", - ["Menu_CheckUpdateNow"] = "Check for Updates...", - ["Menu_About"] = "About this app...", - ["Menu_OpenAppLocation"] = "Open app location...", - ["Menu_OpenConfigLocation"] = "Open config location...", - ["Tray_Open"] = "Open", - ["Tray_ProducingIdol"] = "Producing Idol", - ["Tray_Exit"] = "Exit", - - ["Header_GameName"] = "Game Display Name (Name)", - ["Header_Details"] = "Line 1 Info (Details)", - ["Header_State"] = "Line 2 Info (State)", - ["Header_Timestamp"] = "Elapsed Time (Timestamp)", - ["Header_Buttons"] = "External Link Settings (Buttons)", - - ["Details_None"] = "None", - ["Details_PName"] = "Producer Name", - ["Details_PLv"] = "Producer Level", - ["Details_Both"] = "P-Name and PLv", - ["Placeholder_PName"] = "Producer", - - ["State_None"] = "None", - ["State_PID"] = "P-ID", - ["State_Producing"] = "Producing...", - ["State_Idol"] = "In Charge", - ["State_Custom"] = "Custom", - ["Placeholder_PID"] = "Enter 8-digit P-ID", - ["Placeholder_Custom"] = "Enter text (2-128 characters)", - ["State_ProducingSuffix"] = " producing", - ["State_IdolSuffix"] = "Producer of ", - ["State_NotSet"] = "Not Set", - ["State_Producing_Format"] = "Producing {0}", - ["State_Idol_Format"] = "Producer of {0}", - - ["Timestamp_Label"] = "Elapsed", - ["Timestamp_Reset"] = "Reset Timer", - ["Timestamp_Guide"] = "Click 'Update' to apply", - - ["Button_Mode"] = "Button Mode", - ["Button_ModeNone"] = "None", - ["Button_ModeStore"] = "Link to Game", - ["Button_ModeApp"] = "Link to this app", - ["Button_ModeCustom"] = "Custom", - ["Button_ModeNote"] = "Only visible to others (Discord limitation)", - ["Placeholder_BtnLabel"] = "Button {0} Label", - ["Placeholder_BtnUrl"] = "Button {0} URL", - ["Button_Warning_LabelLength"] = "Multi-byte chars will show up to 10 chars.", - ["Button_Warning_UrlJp"] = "Japanese characters are not allowed in URLs.\nPlease use percent-encoded strings.", - - ["Status_Disconnected"] = "Disconnected", - ["Status_Disconnected_Auto"] = "Disconnected\n(Auto-connect enabled)", - ["Status_Connected"] = "Connected:\n{0}", - ["Status_Connected_Notify"] = "Connected to Discord and started displaying status.", - ["Status_Disconnected_Notify"] = "Disconnected from Discord.", - ["Status_ManualDisconnected_Notify"] = "Disconnected from Discord manually.", - ["Status_Connecting"] = "Connecting... ({0}s)", - ["Status_Paused"] = "Connected\n(Display Paused)", - ["Status_Timeout"] = "Timeout: \nPlease check Discord.", - ["Status_Error"] = "Error: {0}", - ["Status_InitFailed"] = "Initialization Failed.", - ["Status_Exception"] = "Exception: {0}", - ["Status_Updated"] = "Updated", - ["Tray_Status_Connected"] = "Connected", - ["Tray_Status_Paused"] = "Paused", - ["Tray_Status_Connecting"] = "Connecting", - - ["Button_Connect"] = "Connect", - ["Button_Pause"] = "Pause", - ["Button_Resume"] = "Resume", - ["Button_Update"] = "Update", - ["Button_Disconnect"] = "Disconnect", - ["Button_StoreLabel_Mobile"] = "Play (iOS / Android)", - ["Button_StoreLabel_DMM"] = "Play (DMM GAMES)", - ["Button_AboutPresence"] = "About this presence...", - - ["GameApp_Guide"] = "Changes apply after\ndisconnecting and reconnecting", - ["About_Message"] = "GkmStatus\nv{0}\n\nMade by: Wea017net", - ["About_Author"] = "Made by: Wea017net", - ["About_FontAttribution"] = "Font: IBM Plex Sans JP", - ["About_FontLicense"] = "View License", - ["About_Title"] = "About this app", - ["Error_Browser"] = "Could not open the browser.", - ["CharName_JP"] = "Japanese Names", - ["CharName_EN"] = "English Names", - ["Update_NotificationTitle"] = "New Version Available", - ["Update_NotificationBody"] = "Version {0} is now available.\nClick here to open the download page.", - ["Update_NewAvailable"] = "A new version {0} is available.\nWould you like to open the download page?", - ["Update_NoUpdate"] = "You are using the latest version.", - ["Update_RateLimit"] = "GitHub API rate limit exceeded. Please try again later.", - ["Notify_Minimized"] = "Minimized to system tray.", - ["Notify_TrayIdolChanged"] = "Applied the producing idol change and updated the presence.", - ["Notify_TrayDetailsChanged"] = "Applied the Details change and updated the presence.", - ["Notify_TrayStateChanged"] = "Applied the State change and updated the presence." - } - }; - - public static string T(string key) - { - if (Translations.TryGetValue(CurrentLanguage, out var locale) && locale.TryGetValue(key, out var value)) - { - return value; - } - return key; // Fallback to key name - } - - public static string T(string key, params object[] args) - { - return string.Format(T(key), args); - } - } -} diff --git a/GkmStatus/src/AppConstants.cs b/GkmStatus/src/AppConstants.cs new file mode 100644 index 0000000..1ff3652 --- /dev/null +++ b/GkmStatus/src/AppConstants.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GkmStatus.src +{ + internal class AppConstants + { + public const string PROCESS_NAME = "gakumas"; + public const string APP_NAME = "GkmStatus"; + private const string RESOURCE_PREFIX = "GkmStatus.Resources."; + + + public static readonly List<(string Name, string AppId)> GameApps = + [ + ("学園アイドルマスター", "1352261574877778001"), + ("学マス", "1467733389170835486"), + ("THE IDOLM@STER Gakuen", "1467733691382890499"), + ("Gakuen iDOLM@STER", "1467734377197867040"), + ("Gakumas", "1467734892208193650") + ]; + + public const string GITHUB_REPO_URL = "https://api.github.com/repos/Wea017net/GkmStatus/releases/latest"; + public const string HTTP_USER_AGENT = "GkmStatus-UpdateChecker"; + + public const string REG_RUN_KEY = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; + + public const string ProduceCharacter_Data_Path = RESOURCE_PREFIX + "produce_characters.json"; + public const string Localization_Data_Path = RESOURCE_PREFIX + "I18n"; + + public static readonly string CONFIG_PATH = Path.Combine(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GkmStatus"), "config.json"); + + public const string WINDOWS_THEME_REG_KEY = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + + // Fonts + public const string FONT_REGULAR = RESOURCE_PREFIX + "fonts.IBM_Plex_Sans_JP.IBMPlexSansJP-Regular.ttf"; + public const string FONT_BOLD = RESOURCE_PREFIX + "fonts.IBM_Plex_Sans_JP.IBMPlexSansJP-Bold.ttf"; + public const string FONT_MEDIUM = RESOURCE_PREFIX + "fonts.IBM_Plex_Sans_JP.IBMPlexSansJP-Medium.ttf"; + + // Colors + public static readonly Color COLOR_ERROR = ColorTranslator.FromHtml("#fc5555"); + public static readonly Color COLOR_CONNECT = ColorTranslator.FromHtml("#68b900"); + public static readonly Color COLOR_PAUSE = ColorTranslator.FromHtml("#fc930f"); + public static readonly Color COLOR_PRIMARY = ColorTranslator.FromHtml("#7e87f4"); + public static readonly Color COLOR_DISABLED = Color.FromArgb(60, 63, 65); + + public static readonly Color Default_BackColor = Color.FromArgb(47, 49, 54); + public static readonly Color Default_ForeColor = Color.White; + + public static readonly Color COLOR_LIGHT_BG = ColorTranslator.FromHtml("#f3f3f4"); + public static readonly Color COLOR_DARK_BG = Color.FromArgb(32, 34, 37); + public static readonly Color COLOR_OLED_BG = Color.Black; + } +} diff --git a/GkmStatus/src/AppLogic.cs b/GkmStatus/src/AppLogic.cs new file mode 100644 index 0000000..715c813 --- /dev/null +++ b/GkmStatus/src/AppLogic.cs @@ -0,0 +1,121 @@ +using Microsoft.Win32; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using static GkmStatus.src.AppConstants; +using GkmStatus.src.ui; + +namespace GkmStatus.src +{ + public enum PresenceStateType + { + None, + PID, + Idol, + Producing, + Custom + } + + public enum PresenceDetailsType + { + None, + PName, + PLv, + Both + } + + public enum PresenceButtonMode + { + None, + Store, + App, + Custom + } + + public static class StartupManager + { + public static bool IsRunAtStartup() + { + try + { + using RegistryKey? key = Registry.CurrentUser.OpenSubKey(REG_RUN_KEY, false); + return key?.GetValue(APP_NAME) != null; + } + catch { return false; } + } + + public static void SetRunAtStartup(bool run) + { + try + { + using RegistryKey? key = Registry.CurrentUser.OpenSubKey(REG_RUN_KEY, true); + if (key != null) + { + if (run) key.SetValue(APP_NAME, Application.ExecutablePath); + else key.DeleteValue(APP_NAME, false); + } + } + catch (Exception ex) + { + MessageBox.Show("スタートアップ設定の変更に失敗しました: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + public static bool OpenUrl(string url) + { + try + { + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + return true; + } + catch + { + return false; + } + } + } + + public static class AppSettingsHelper + { + private static readonly Regex _japaneseRegex = new(@"[\p{IsHiragana}\p{IsKatakana}\p{IsCJKUnifiedIdeographs}]", RegexOptions.Compiled); + + public static Regex JapaneseRegex() => _japaneseRegex; + + public static PresenceStateType GetStateType(int i) => Enum.IsDefined(typeof(PresenceStateType), i) ? (PresenceStateType)i : PresenceStateType.Producing; + public static PresenceStateType GetStateType(string? s) => Enum.TryParse(s, out var t) ? t : PresenceStateType.Producing; + public static string GetStateString(PresenceStateType t) => t.ToString(); + public static int GetStateIndex(PresenceStateType t) => (int)t; + public static string GetStateString(int i) => GetStateString(GetStateType(i)); + public static int GetStateIndex(string s) => GetStateIndex(GetStateType(s)); + + public static PresenceDetailsType GetDetailsType(int i) => Enum.IsDefined(typeof(PresenceDetailsType), i) ? (PresenceDetailsType)i : PresenceDetailsType.Both; + public static PresenceDetailsType GetDetailsType(string? s) => Enum.TryParse(s, out var t) ? t : PresenceDetailsType.Both; + public static string GetDetailsString(PresenceDetailsType t) => t.ToString(); + public static int GetDetailsIndex(PresenceDetailsType t) => (int)t; + public static string GetDetailsString(int i) => GetDetailsString(GetDetailsType(i)); + public static int GetDetailsIndex(string s) => GetDetailsIndex(GetDetailsType(s)); + + public static PresenceButtonMode GetButtonMode(int i) => Enum.IsDefined(typeof(PresenceButtonMode), i) ? (PresenceButtonMode)i : PresenceButtonMode.None; + public static PresenceButtonMode GetButtonMode(string? s) => Enum.TryParse(s, out var t) ? t : PresenceButtonMode.None; + public static string GetButtonModeString(PresenceButtonMode t) => t.ToString(); + public static int GetButtonModeIndex(PresenceButtonMode t) => (int)t; + public static string GetButtonModeString(int i) => GetButtonModeString(GetButtonMode(i)); + public static int GetButtonModeIndex(string s) => GetButtonModeIndex(GetButtonMode(s)); + + public static AppTheme GetTheme(int i) => Enum.IsDefined(typeof(AppTheme), i) ? (AppTheme)i : AppTheme.Auto; + public static AppTheme GetTheme(string? s) => Enum.TryParse(s, out var t) ? t : AppTheme.Auto; + public static string GetThemeString(AppTheme t) => t.ToString(); + public static string GetThemeString(int i) => GetThemeString(GetTheme(i)); + public static int GetThemeIndex(string s) => GetThemeIndex(GetTheme(s)); + public static int GetThemeIndex(AppTheme t) => (int)t; + + public static I18n.Language GetLanguage(int i) => i == 1 ? I18n.Language.English : I18n.Language.Japanese; + public static I18n.Language GetLanguage(string? s) => (s == "en" || s == "English") ? I18n.Language.English : I18n.Language.Japanese; + public static string GetLangString(I18n.Language l) => l == I18n.Language.English ? "en" : "ja"; + public static string GetLangString(int i) => GetLangString(GetLanguage(i)); + + public static bool IsValidRpcButtonUrl(string? url) => !string.IsNullOrEmpty(url) + && (url.StartsWith("http://") || url.StartsWith("https://")) + && !_japaneseRegex.IsMatch(url); + } +} diff --git a/GkmStatus/src/Characters.cs b/GkmStatus/src/Characters.cs new file mode 100644 index 0000000..3fe2fd6 --- /dev/null +++ b/GkmStatus/src/Characters.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text.Json; +using static GkmStatus.src.AppConstants; + +namespace GkmStatus.src +{ + public sealed class ProduceCharacter + { + public string Id { get; set; } = ""; + public string Display { get; set; } = ""; + public string NameEn { get; set; } = ""; + } + + internal static class Characters + { + private sealed class ProduceCharactersData + { + public List Characters { get; set; } = []; + } + + private static readonly JsonSerializerOptions JsonOptions = new() + { + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true + }; + + public static IReadOnlyList ProduceCharacters { get; } = LoadProduceCharacters(); + + public static int FindProduceCharacterIndex(string? id) + { + if (string.IsNullOrEmpty(id)) return -1; + + for (int i = 0; i < ProduceCharacters.Count; i++) + { + if (string.Equals(ProduceCharacters[i].Id, id, StringComparison.Ordinal)) + return i; + } + + return -1; + } + + private static List LoadProduceCharacters() + { + + try + { + var asm = System.Reflection.Assembly.GetExecutingAssembly(); + using Stream? stream = asm.GetManifestResourceStream(ProduceCharacter_Data_Path); + + if (stream is null) + { + Debug.WriteLine($"Character JSON resource not found: {ProduceCharacter_Data_Path}"); + return []; + } + + var root = JsonSerializer.Deserialize(stream, JsonOptions); + if (root?.Characters is { Count: > 0 }) + { + return root.Characters; + } + + Debug.WriteLine("Character JSON is empty or invalid."); + } + catch (Exception ex) + { + Debug.WriteLine("Character JSON load error: " + ex.Message); + } + + return []; + } + } +} \ No newline at end of file diff --git a/GkmStatus/src/ConfigManager.cs b/GkmStatus/src/ConfigManager.cs new file mode 100644 index 0000000..e3e6d96 --- /dev/null +++ b/GkmStatus/src/ConfigManager.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using static GkmStatus.src.AppConstants; +using static GkmStatus.src.AppSettingsHelper; +using GkmStatus.src.ui; + +namespace GkmStatus.src +{ + public class ConfigManager + { + private static readonly JsonSerializerOptions _jsonOptions = new() + { + WriteIndented = true, + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true, + }; + + public AppConfig Config { get; private set; } + + public ConfigManager() + { + Config = new AppConfig(); + } + + public void Load() + { + if (!File.Exists(CONFIG_PATH)) + { + Config = Config = CreateDefaultConfig(); + return; + } + + try + { + string json = File.ReadAllText(CONFIG_PATH); + Config = JsonSerializer.Deserialize(json, _jsonOptions) ?? CreateDefaultConfig(); + } + catch (Exception e) + { + Debug.WriteLine("Failed to load config, using default. Error: " + e.Message); + Config = CreateDefaultConfig(); + } + } + + public void Save() + { + try + { + string? dir = Path.GetDirectoryName(CONFIG_PATH); + if (dir != null && !Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + string json = JsonSerializer.Serialize(Config, _jsonOptions); + File.WriteAllText(CONFIG_PATH, json); + } + catch (Exception e) + { + Debug.WriteLine("Failed to save config. Error: " + e.Message); + } + } + + public void ResetToDefault() + { + Config = CreateDefaultConfig(); + } + + public static AppConfig CreateDefaultConfig() + { + var isJapanese = System.Globalization.CultureInfo.CurrentUICulture.Name.StartsWith("ja"); + + return new AppConfig + { + Settings = new AppSettings + { + AutoDetectGakumas = true, + AutoCheckUpdates = true, + ShowBackgroundNotifications = true, + NotifyOnMinimize = true, + MinimizeToTray = true, + SelectedTheme = GetThemeString(0), + SelectedLanguage = GetLangString(isJapanese ? I18n.Language.Japanese : I18n.Language.English) + }, + Presence = new PresenceSettings + { + StateType = GetStateString(PresenceStateType.Idol), + ButtonMode = GetButtonModeString(PresenceButtonMode.Store), + DetailsType = GetDetailsString(PresenceDetailsType.Both) + } + }; + } + } + + public class AppConfig + { + public int ConfigVersion { get; set; } = 1; + public AppSettings Settings { get; set; } = new AppSettings(); + public PresenceSettings Presence { get; set; } = new PresenceSettings(); + } + + public class AppSettings + { + public bool StartMinimized { get; set; } = false; + public bool ConnectOnStart { get; set; } = false; + public bool AutoCheckUpdates { get; set; } = true; + public bool ShowBackgroundNotifications { get; set; } = true; + public bool NotifyOnMinimize { get; set; } = true; + public bool MinimizeToTray { get; set; } = true; + public bool AutoDetectGakumas { get; set; } = true; + public string SelectedTheme { get; set; } = GetThemeString(0); + public string SelectedLanguage { get; set; } = GetLangString(I18n.Language.Japanese); + public DateTime LastUpdateCheck { get; set; } = DateTime.MinValue; + } + + public class PresenceSettings + { + public string DetailsType { get; set; } = GetDetailsString(PresenceDetailsType.Both); + public string ProducerName { get; set; } = ""; + public int ProducerLevel { get; set; } = 1; + public string StateType { get; set; } = GetStateString(PresenceStateType.Producing); + public int CharNameLangIndex { get; set; } = 0; + public string? SelectedIdolCharacterId { get; set; } = "hanami_saki"; + public string? SelectedProduceCharacterId { get; set; } = "hanami_saki"; + public Dictionary StateHistory { get; set; } = []; + public int GameAppIndex { get; set; } = 0; + public string ButtonMode { get; set; } = GetButtonModeString(PresenceButtonMode.Store); + public Dictionary ButtonHistory { get; set; } = []; + } + + public class ButtonHistoryData + { + public string L1 { get; set; } = ""; public string U1 { get; set; } = ""; + public string L2 { get; set; } = ""; public string U2 { get; set; } = ""; + } + +} diff --git a/GkmStatus/src/DiscordRpc.cs b/GkmStatus/src/DiscordRpc.cs new file mode 100644 index 0000000..09c83e4 --- /dev/null +++ b/GkmStatus/src/DiscordRpc.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Text; +using DiscordRPC; +using DiscordRPC.Logging; + +namespace GkmStatus.src +{ + public enum RpcStatus + { + Disconnected, + Connecting, + Connected, + Paused, + Error + } + + public class DiscordRpcService: IDisposable + { + private DiscordRpcClient? client; + private bool disposed; + public RpcStatus Status { get; private set; } = RpcStatus.Disconnected; + public string? CurrentUsername { get; private set; } + public string? LastErrorMessage { get; private set; } + + public event EventHandler? StatusChanged; + public event EventHandler? Ready; + + public bool IsInitialized => client?.IsInitialized ?? false; + + public void Initialize(string appId) + { + if (client != null) + Deinitialize(); + + CurrentUsername = null; + LastErrorMessage = null; + + client = new DiscordRpcClient(appId) + { + Logger = new ConsoleLogger() { Level = LogLevel.Warning }, + SkipIdenticalPresence = false + }; + + client.OnReady += (s, e) => + { + Status= RpcStatus.Connected; + CurrentUsername = e.User.Username; + LastErrorMessage = null; + Ready?.Invoke(this, e.User.Username); + StatusChanged?.Invoke(this, Status); + }; + + client.OnError += (s, e) => + { + Status = RpcStatus.Error; + LastErrorMessage = e.Message; + StatusChanged?.Invoke(this, Status); + }; + + client.OnClose += (s, e) => + { + Status = RpcStatus.Disconnected; + CurrentUsername = null; + StatusChanged?.Invoke(this, Status); + }; + + client.OnConnectionFailed += (s, e) => + { + Status = RpcStatus.Error; + LastErrorMessage = "Discord connection failed."; + StatusChanged?.Invoke(this, Status); + }; + + Status = RpcStatus.Connecting; + StatusChanged?.Invoke(this, Status); + + try + { + if(!client.Initialize()) + { + Status = RpcStatus.Error; + LastErrorMessage = "Failed to initialize Discord RPC client."; + StatusChanged?.Invoke(this, Status); + } + } catch (Exception ex) + { + Status = RpcStatus.Error; + LastErrorMessage = ex.Message; + StatusChanged?.Invoke(this, Status); + } + } + + public void Deinitialize() + { + if(client != null) + { + client.ClearPresence(); + client.Deinitialize(); + client.Dispose(); + client = null; + } + + Status = RpcStatus.Disconnected; + CurrentUsername = null; + LastErrorMessage = null; + StatusChanged?.Invoke(this, Status); + } + + public void Clear() + { + client?.ClearPresence(); + Status = RpcStatus.Paused; + StatusChanged?.Invoke(this, Status); + } + + public void Invoke() => client?.Invoke(); + + public void UpdatePresence(RichPresence presence) + { + if (client == null || !client.IsInitialized) + return; + + presence.Details = SafeTrimUtf8(presence.Details, 128); + presence.State = SafeTrimUtf8(presence.State, 128); + + if(presence.Buttons != null) + { + foreach(var btn in presence.Buttons) + btn.Label = SafeTrimUtf8(btn.Label, 32); + } + + client.SetPresence(presence); + } + + private static string SafeTrimUtf8(string? text, int maxBytes) + { + if (string.IsNullOrEmpty(text)) + return ""; + + var enc = Encoding.UTF8; + + if(enc.GetByteCount(text) <= maxBytes) + return text; + + int lo = 0, hi = text.Length; + while (lo < hi) + { + int mid = (lo + hi + 1) / 2; + + if (enc.GetByteCount(text[..mid]) <= maxBytes) + lo = mid; + else + hi = mid - 1; + } + + return text[..lo]; + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) return; + + if (disposing) + { + Deinitialize(); + } + + disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/GkmStatus/src/ProcessWatcher.cs b/GkmStatus/src/ProcessWatcher.cs new file mode 100644 index 0000000..b5f0870 --- /dev/null +++ b/GkmStatus/src/ProcessWatcher.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Windows.Forms; + +namespace GkmStatus.src +{ + public class ProcessWatcher : IDisposable + { + private readonly System.Windows.Forms.Timer _timer; + private readonly string _processName; + private bool _wasRunning; + private bool _disposed; + + public event EventHandler? ProcessStarted; + + public event EventHandler? ProcessStopped; + + public bool IsRunning { get; private set; } + + public ProcessWatcher(string processName, int interval = 3000) + { + _processName = processName; + _timer = new System.Windows.Forms.Timer { Interval = interval }; + _timer.Tick += OnTick; + } + + public bool Enabled + { + get => _timer.Enabled; + set + { + if (value == _timer.Enabled) + return; + + if (value) + _timer.Start(); + else + _timer.Stop(); + } + } + + public void ForceCheck() => OnTick(this, EventArgs.Empty); + + private void OnTick(object? sender, EventArgs e) + { + var processes = Process.GetProcessesByName(_processName); + bool isNowRunning = processes.Length > 0; + + if(isNowRunning && !_wasRunning) { + IsRunning = true; + ProcessStarted?.Invoke(this, EventArgs.Empty); + }else if(!isNowRunning && _wasRunning) { + IsRunning = false; + ProcessStopped?.Invoke(this, EventArgs.Empty); + } + + _wasRunning = isNowRunning; + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _timer.Stop(); + _timer.Dispose(); + } + + _disposed = true; + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/GkmStatus/src/UpdateService.cs b/GkmStatus/src/UpdateService.cs new file mode 100644 index 0000000..87e83cb --- /dev/null +++ b/GkmStatus/src/UpdateService.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Text.Json; + +namespace GkmStatus.src +{ + public class UpdateCheckResult + { + public bool IsSuccess { get; set; } + public bool HasUpdate { get; set; } + public string LatestVersion { get; set; } = string.Empty; + public string ReleaseUrl { get; set; } = string.Empty; + public string? ErrorMessage { get; set; } + public bool IsRateLimited { get; set; } + } + + public class UpdateService:IDisposable + { + private readonly HttpClient _httpClient; + private bool _disposed; + + + public UpdateService() + { + _httpClient= new HttpClient(); + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(AppConstants.HTTP_USER_AGENT); + } + + public async Task CheckForUpdatesAsync(string currentVersion) + { + var result = new UpdateCheckResult(); + + try + { + using var response = await _httpClient.GetAsync(AppConstants.GITHUB_REPO_URL); + + if(response.StatusCode == HttpStatusCode.Forbidden) + { + result.IsRateLimited = true; + return result; + } + + if(response.StatusCode == HttpStatusCode.NotFound) + return result; + + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + + using var doc = JsonDocument.Parse(content); + + if(doc.RootElement.TryGetProperty("tag_name", out var tag)) + { + string latestStr = tag.GetString()?.TrimStart('v') ?? ""; + string currentStr = NormalizeVersion(currentVersion); + string htmlUrl = doc.RootElement.TryGetProperty("html_url", out var urlProp) ? urlProp.GetString() ?? "" : ""; + + if(Version.TryParse(latestStr, out var latest) && Version.TryParse(currentStr, out var current)) { + result.IsSuccess = true; + result.LatestVersion = latestStr; + result.ReleaseUrl = htmlUrl; + result.HasUpdate = latest > current; + } + } + } catch(Exception ex) + { + result.IsSuccess = false; + result.ErrorMessage = ex.Message; + } + + return result; + } + + private static string NormalizeVersion(string version) + { + if(string.IsNullOrEmpty(version)) + return version; + + if(version.StartsWith("v",StringComparison.OrdinalIgnoreCase)) + version = version[1..]; + else if(version.StartsWith("Ver",StringComparison.OrdinalIgnoreCase)) + version = version[3..]; + else if (version.StartsWith("Ver.", StringComparison.OrdinalIgnoreCase)) + version = version[4..]; + + return version.Trim(); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + + if (disposing) + { + _httpClient.Dispose(); + } + + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/GkmStatus/src/native/NativeMethods.cs b/GkmStatus/src/native/NativeMethods.cs new file mode 100644 index 0000000..27246ab --- /dev/null +++ b/GkmStatus/src/native/NativeMethods.cs @@ -0,0 +1,26 @@ +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace GkmStatus.src.native +{ + [SupportedOSPlatform("windows")] + internal static partial class Native + { + public const int EM_SETCUEBANNER = 0x1501; + + [LibraryImport("user32.dll", EntryPoint = "SendMessageW", StringMarshalling = StringMarshalling.Utf16)] + public static partial IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, string lParam); + + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool DestroyIcon(IntPtr hIcon); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool SetForegroundWindow(IntPtr hWnd); + + public static void SetPlaceholder(IntPtr handle, string placeholderText) { SendMessage(handle, EM_SETCUEBANNER, 0, placeholderText); } + public static void SetPlaceholder(TextBox textBox, string placeholderText) { SetPlaceholder(textBox.Handle, placeholderText); } + + } +} \ No newline at end of file diff --git a/GkmStatus/src/ui/CustomRenderer.cs b/GkmStatus/src/ui/CustomRenderer.cs new file mode 100644 index 0000000..f5fae16 --- /dev/null +++ b/GkmStatus/src/ui/CustomRenderer.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Versioning; +using System.Text; + +namespace GkmStatus.src.ui +{ + [SupportedOSPlatform("windows")] + public class MyRenderer(bool isBright, CustomColorTable colorTable) : ToolStripProfessionalRenderer(colorTable) + { + private readonly bool _isBright = isBright; + private readonly CustomColorTable _colorTable = colorTable; + + protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) + { + using var b = new SolidBrush(e.ToolStrip.BackColor); + e.Graphics.FillRectangle(b, 0, 0, e.ToolStrip.Width, e.ToolStrip.Height); + } + + protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) + { + using var b = new SolidBrush(e.ToolStrip.BackColor); + e.Graphics.FillRectangle(b, e.AffectedBounds); + } + + protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) + { + if (e.Image != null) + { + e.Graphics.DrawImage(e.Image, e.ImageRectangle); + } + } + + protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) + { + Rectangle r = new(Point.Empty, e.Item.Size); + r.Width -= 1; + r.Height -= 1; + + if (e.Item.Pressed) + { + using var b = new SolidBrush(_colorTable.HoverBgColor); + e.Graphics.FillRectangle(b, r); + + Color borderColor = _isBright ? Color.Gray : Color.White; + using var p = new Pen(borderColor, 1); + e.Graphics.DrawRectangle(p, r); + } + else if (e.Item.Selected) + { + using var b = new SolidBrush(_colorTable.HoverBgColor); + e.Graphics.FillRectangle(b, r); + } + } + + protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) + { + e.TextColor = _isBright ? Color.Black : Color.White; + base.OnRenderItemText(e); + } + + protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) + { + Color line = _isBright ? Color.FromArgb(180, 180, 180) : Color.FromArgb(80, 80, 80); + using var p = new Pen(line); + int y = e.Item.Height / 2; + e.Graphics.DrawLine(p, 30, y, e.Item.Width - 5, y); + } + + protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) + { + e.ArrowColor = _isBright ? Color.Black : Color.White; + base.OnRenderArrow(e); + } + } + + [SupportedOSPlatform("windows")] + public class CustomColorTable(bool isBright) : ProfessionalColorTable + { + public Color HoverBgColor { get; } = isBright ? Color.FromArgb(180, 200, 200, 200) : Color.FromArgb(60, 255, 255, 255); + public Color CustomBorderColor { get; } = isBright ? Color.Gray : Color.White; + + public override Color MenuItemSelected => HoverBgColor; + public override Color MenuItemSelectedGradientBegin => HoverBgColor; + public override Color MenuItemSelectedGradientEnd => HoverBgColor; + public override Color MenuItemPressedGradientBegin => HoverBgColor; + public override Color MenuItemPressedGradientEnd => HoverBgColor; + public override Color MenuItemPressedGradientMiddle => HoverBgColor; + public override Color MenuItemBorder => CustomBorderColor; + public override Color MenuBorder => CustomBorderColor; + } +} diff --git a/GkmStatus/src/ui/FontManager.cs b/GkmStatus/src/ui/FontManager.cs new file mode 100644 index 0000000..d5377f0 --- /dev/null +++ b/GkmStatus/src/ui/FontManager.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing.Text; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; +using static GkmStatus.src.AppConstants; + +namespace GkmStatus.src +{ + [SupportedOSPlatform("windows")] + public partial class FontManager : IDisposable + { + [LibraryImport("gdi32.dll")] + private static partial IntPtr AddFontMemResourceEx(IntPtr pbFont, uint cbFont, IntPtr pdv, out uint pcFonts); + + private PrivateFontCollection? _pfc; + private readonly List _fontPointers = []; + + public Font AppFont { get; private set; } = null!; + public Font AppFontBold { get; private set; } = null!; + public Font AppFontMedium { get; private set; } = null!; + public Font MenuFont { get; private set; } = null!; + + public void Initialize(float uiScale) + { + SetupFonts(uiScale); + } + + private void SetupFonts(float uiScale) + { + try + { + _pfc ??= new PrivateFontCollection(); + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + + var manifestNames = assembly.GetManifestResourceNames(); + var ttfCandidates = manifestNames.Where(n => + n.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) && + n.Contains("plex", StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (ttfCandidates.Length == 0) + { + ttfCandidates = [ + FONT_REGULAR, + FONT_BOLD, + FONT_MEDIUM + ]; + } + + foreach (string resourceName in ttfCandidates) + { + using Stream? stream = assembly.GetManifestResourceStream(resourceName); + if (stream != null) + { + try + { + byte[] fontData = new byte[stream.Length]; + stream.ReadExactly(fontData, 0, (int)stream.Length); + IntPtr fontPtr = Marshal.AllocCoTaskMem(fontData.Length); + Marshal.Copy(fontData, 0, fontPtr, fontData.Length); + + _pfc.AddMemoryFont(fontPtr, fontData.Length); + AddFontMemResourceEx(fontPtr, (uint)fontData.Length, IntPtr.Zero, out uint _); + _fontPointers.Add(fontPtr); + } + catch (Exception ex) { Debug.WriteLine($"Failed loading font '{resourceName}': {ex.Message}"); } + } + } + + FontFamily? regular = null, bold = null, medium = null; + foreach (var ff in _pfc.Families) + { + string name = ff.Name; + if (name.Contains("IBM Plex Sans JP", StringComparison.OrdinalIgnoreCase)) + { + if (name.Contains("Bold", StringComparison.OrdinalIgnoreCase)) bold = ff; + else if (name.Contains("Medium", StringComparison.OrdinalIgnoreCase)) medium = ff; + else regular = ff; + } + } + + // フォントの生成 + float basePx = 13.3f * uiScale; + regular ??= _pfc.Families.Length > 0 ? _pfc.Families[0] : null; + + if (regular != null) + { + AppFont = new Font(regular, basePx, GraphicsUnit.Pixel); + AppFontBold = bold != null ? new Font(bold, basePx, GraphicsUnit.Pixel) : new Font(regular, basePx, FontStyle.Bold, GraphicsUnit.Pixel); + AppFontMedium = medium != null ? new Font(medium, basePx, GraphicsUnit.Pixel) : new Font(regular, basePx, GraphicsUnit.Pixel); + SetupMenuFont(uiScale); + return; + } + } + catch (Exception ex) + { + Debug.WriteLine("Font Load Error: " + ex.Message); + } + + // フォールバック + float fallbackPx = 13.3f * uiScale; + AppFont = new Font("Meiryo UI", fallbackPx, GraphicsUnit.Pixel); + AppFontBold = new Font("Meiryo UI", fallbackPx, FontStyle.Bold, GraphicsUnit.Pixel); + AppFontMedium = new Font("Meiryo UI", fallbackPx, GraphicsUnit.Pixel); + SetupMenuFont(uiScale); + } + + private void SetupMenuFont(float uiScale) + { + float menuPx = 12f * uiScale; + try + { + using var testFont = new Font("Yu Gothic UI", menuPx, GraphicsUnit.Pixel); + if (testFont.Name == "Yu Gothic UI") + { + MenuFont = new Font("Yu Gothic UI", menuPx, GraphicsUnit.Pixel); + } + else + { + var family = SystemFonts.MessageBoxFont?.FontFamily ?? FontFamily.GenericSansSerif; + MenuFont = new Font(family, menuPx, GraphicsUnit.Pixel); + } + } + catch + { + var family = SystemFonts.MessageBoxFont?.FontFamily ?? FontFamily.GenericSansSerif; + MenuFont = new Font(family, menuPx, GraphicsUnit.Pixel); + } + } + + public void Dispose() + { + AppFont?.Dispose(); + AppFontBold?.Dispose(); + AppFontMedium?.Dispose(); + MenuFont?.Dispose(); + + foreach (var ptr in _fontPointers) Marshal.FreeCoTaskMem(ptr); + _fontPointers.Clear(); + _pfc?.Dispose(); + + GC.SuppressFinalize(this); + } + } +} diff --git a/GkmStatus/src/ui/ThemeManager.cs b/GkmStatus/src/ui/ThemeManager.cs new file mode 100644 index 0000000..12fd2a3 --- /dev/null +++ b/GkmStatus/src/ui/ThemeManager.cs @@ -0,0 +1,189 @@ +using GkmStatus.src.ui; +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using static GkmStatus.src.AppConstants; + +namespace GkmStatus.src +{ + public enum AppTheme + { + Auto, + Light, + Dark, + OLED + } + + public partial class ThemeManager(FontManager fontManager) + { + [LibraryImport("dwmapi.dll")] + private static partial int DwmSetWindowAttribute(nint hwnd, int attr, ref int attrValue, int attrSize); + + private readonly FontManager _fontManager = fontManager; + + public void ApplyTheme(Form form, AppTheme theme) + { + Color color = theme switch + { + AppTheme.Auto => GetWindowsThemeColor(), + AppTheme.Light => COLOR_LIGHT_BG, + AppTheme.Dark => COLOR_DARK_BG, + AppTheme.OLED => COLOR_OLED_BG, + _ => COLOR_DARK_BG + }; + + ApplyColorsToForm(form, color); + } + + private static Color GetWindowsThemeColor() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(WINDOWS_THEME_REG_KEY); + var value = key?.GetValue("AppsUseLightTheme"); + if (value is int i && i == 1) + return COLOR_LIGHT_BG; + } + catch { } + return COLOR_DARK_BG; + } + + private void ApplyColorsToForm(Form form, Color bg) + { + form.SuspendLayout(); + + bool isBright = bg.GetBrightness() > 0.5; + form.BackColor = bg; + form.ForeColor = isBright ? Color.Black : Color.White; + + SetTitleBarDarkMode(form.Handle, !isBright); + + Color menuBg = isBright ? Color.FromArgb(235, 233, 233) : (bg.R == 0 ? Color.Black : Color.FromArgb(45, 47, 51)); + Color controlBg = isBright ? Color.White : Color.FromArgb(47, 49, 54); + Color borderColor = isBright ? Color.FromArgb(200, 200, 200) : Color.FromArgb(70, 70, 70); + + foreach (Control c in GetAllControls(form)) + { + ApplyToControl(c, isBright, menuBg, controlBg, borderColor); + } + + form.ResumeLayout(true); + } + + private void ApplyToControl(Control c, bool isBright, Color menuBg, Color controlBg, Color borderColor) + { + if (c is Label lbl) + { + ApplyLabelTheme(lbl, isBright); + } + else if (c is MenuStrip ms) + { + ms.BackColor = menuBg; + ms.Font = _fontManager.MenuFont; + ms.Renderer = new MyRenderer(isBright, new CustomColorTable(isBright)); + foreach (ToolStripItem item in ms.Items) + { + if (item is ToolStripMenuItem tsmi) ApplyThemeToMenuItems(tsmi, menuBg, isBright); + } + } + else if (c is ComboBox cb) + { + cb.BackColor = controlBg; + cb.ForeColor = isBright ? Color.Black : Color.White; + } + else if (c is TextBox tb) + { + tb.BackColor = controlBg; + tb.ForeColor = isBright ? Color.Black : Color.White; + if (tb.Tag is Panel p) { UpdatePanelBorder(p, controlBg, borderColor); } + } + else if (c is NumericUpDown nu) + { + nu.BackColor = controlBg; + nu.ForeColor = isBright ? Color.Black : Color.White; + if (nu.Tag is Panel p) { UpdatePanelBorder(p, controlBg, borderColor); } + } + else if (c is CheckBox chk) + { + chk.ForeColor = isBright ? Color.Black : Color.White; + } + else if (c is Button btn) + { + btn.Font = _fontManager.AppFontBold; + } + + if (c is not Label && c is not MenuStrip) + { + c.Font = _fontManager.AppFont; + } + } + + private void ApplyLabelTheme(Label lbl, bool isBright) + { + if (lbl.Tag is string tag && tag.StartsWith("Header_")) + lbl.Font = _fontManager.AppFontBold; + else + lbl.Font = _fontManager.AppFont; + + if (lbl.Name == "lblResetGuide" || lbl.Name == "lblBtnModeNote") + lbl.ForeColor = Color.Gray; + else if (lbl.Name == "lblGameAppGuide") + lbl.ForeColor = AppConstants.COLOR_PAUSE; + else + lbl.ForeColor = isBright ? Color.Black : Color.LightGray; + } + + private void ApplyThemeToMenuItems(ToolStripMenuItem item, Color menuBg, bool isBright) + { + item.Font = _fontManager.MenuFont; + if (item.DropDown != null) + { + item.DropDown.BackColor = menuBg; + item.DropDown.Font = _fontManager.MenuFont; + item.DropDown.Renderer = new MyRenderer(isBright, new CustomColorTable(isBright)); + } + foreach (ToolStripItem subItem in item.DropDownItems) + { + if (subItem is ToolStripMenuItem subMenu) ApplyThemeToMenuItems(subMenu, menuBg, isBright); + else subItem.Font = _fontManager.MenuFont; + } + } + + private static void UpdatePanelBorder(Panel p, Color bg, Color border) + { + p.BackColor = bg; + p.Tag = border; + p.Invalidate(); + } + + private static void SetTitleBarDarkMode(IntPtr handle, bool dark) + { + try + { + int useDark = dark ? 1 : 0; + if (DwmSetWindowAttribute(handle, 20, ref useDark, sizeof(int)) != 0) + _ = DwmSetWindowAttribute(handle, 19, ref useDark, sizeof(int)); + } + catch { } + } + + private static IEnumerable GetAllControls(Control parent) + { + foreach (Control c in parent.Controls) + { + yield return c; + foreach (var sub in GetAllControls(c)) yield return sub; + } + } + + public static void UpdateThemeChecks(ToolStripMenuItem selected, params ToolStripMenuItem[] others) + { + selected.Checked = true; + foreach (var o in others) + o.Checked = false; + } + + } +} diff --git a/GkmStatus/src/ui/Translation.cs b/GkmStatus/src/ui/Translation.cs new file mode 100644 index 0000000..56214c5 --- /dev/null +++ b/GkmStatus/src/ui/Translation.cs @@ -0,0 +1,244 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text.Json; +using static GkmStatus.src.AppConstants; + +namespace GkmStatus.src.ui +{ + public static class I18n + { + public enum Language + { + Japanese, + English + } + + private static Language currentLanguage = Language.Japanese; + private static Dictionary currentTranslations = []; + public static Language CurrentLanguage + { + get => currentLanguage; + set => SetLanguage(value); + } + + public enum Text_List + { + App_Name, + Menu_File, + Menu_Exit, + Menu_Settings, + Menu_RunAtStartup, + Menu_StartMinimized, + Menu_AutoConnect, + Menu_CheckForUpdates, + Menu_NotifyInBackground, + Menu_NotifyOnMinimize, + Menu_MinimizeToTray, + Menu_MonitorProcess, + Menu_Language, + Menu_View, + Menu_Theme, + Menu_ThemeAuto, + Menu_ThemeLight, + Menu_ThemeDark, + Menu_ThemeOLED, + Menu_Help, + Menu_Github, + Menu_CheckUpdateNow, + Menu_About, + Menu_OpenAppLocation, + Menu_OpenConfigLocation, + Tray_Open, + Tray_ProducingIdol, + Tray_Exit, + + Header_GameName, + Header_Details, + Header_State, + Header_Timestamp, + Header_Buttons, + + Details_None, + Details_PName, + Details_PLv, + Details_Both, + Placeholder_PName, + + State_None, + State_PID, + State_Producing, + State_Idol, + State_Custom, + Placeholder_PID, + Placeholder_Custom, + State_ProducingSuffix, + State_IdolSuffix, + State_NotSet, + State_Producing_Format, + State_Idol_Format, + + Timestamp_Label, + Timestamp_Reset, + Timestamp_Guide, + + Button_Mode, + Button_ModeNone, + Button_ModeStore, + Button_ModeApp, + Button_ModeCustom, + Button_ModeNote, + Placeholder_BtnLabel, + Placeholder_BtnUrl, + Button_Warning_LabelLength, + Button_Warning_UrlJp, + + Status_Connected, + Status_Connected_Notify, + Status_Disconnected, + Status_Disconnected_Auto, + Status_Disconnected_Notify, + Status_ManualDisconnected_Notify, + Status_Connecting, + Status_Paused, + Status_Timeout, + Status_Error, + Status_InitFailed, + Status_Exception, + Status_Updated, + Tray_Status_Connected, + Tray_Status_Paused, + Tray_Status_Connecting, + + Button_Connect, + Button_Pause, + Button_Resume, + Button_Update, + Button_Disconnect, + Button_StoreLabel_Mobile, + Button_StoreLabel_DMM, + Button_AboutPresence, + + GameApp_Guide, + About_Message, + About_Author, + About_FontAttribution, + About_FontLicense, + About_Title, + Error_Browser, + CharName_JP, + CharName_EN, + Update_NotificationTitle, + Update_NotificationBody, + Update_NewAvailable, + Update_NoUpdate, + Update_RateLimit, + Notify_Minimized, + Notify_TrayIdolChanged, + Notify_TrayDetailsChanged, + Notify_TrayStateChanged, + } + + static I18n() + { + SetLanguage(currentLanguage); + } + + public static void SetLanguage(Language lang) + { + currentLanguage = lang; + currentTranslations = LoadTranslations(currentLanguage); + } + + public static string T(string key) + { + if (currentTranslations.TryGetValue(key, out var value)) + { + return value; + } + return key; // Fallback to key name + } + + public static string T(Text_List key) => T(key.ToString()); + + public static string T(string key, params object[] args) => string.Format(T(key), args); + + public static string T(Text_List key, params object[] args) => string.Format(T(key), args); + + private static Dictionary LoadTranslations(Language lang) + { + var map = new Dictionary(StringComparer.Ordinal); + var (resourceName, rootKey) = GetResourceInfo(lang); + + try + { + var asm = Assembly.GetExecutingAssembly(); + using var stream = asm.GetManifestResourceStream(resourceName); + if (stream == null) + { + Debug.WriteLine($"I18n resource not found: {resourceName}"); + return map; + } + + using var doc = JsonDocument.Parse(stream); + JsonElement entries = default; + bool found = false; + + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + if (doc.RootElement.TryGetProperty(rootKey, out var langArray) && langArray.ValueKind == JsonValueKind.Array) + { + entries = langArray; + found = true; + } + else + { + foreach (var prop in doc.RootElement.EnumerateObject()) + { + if (prop.Value.ValueKind == JsonValueKind.Array) + { + entries = prop.Value; + found = true; + break; + } + } + } + } + else if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + entries = doc.RootElement; + found = true; + } + + if (!found) + return map; + + foreach (var item in entries.EnumerateArray()) + { + if (!item.TryGetProperty("key", out var keyEl) || !item.TryGetProperty("text", out var textEl)) + continue; + + var key = keyEl.GetString(); + var text = textEl.GetString() ?? string.Empty; + + if (!string.IsNullOrWhiteSpace(key)) + map[key] = text; + } + } + catch (Exception ex) + { + Debug.WriteLine("I18n load error: " + ex.Message); + } + + return map; + } + + private static (string ResourceName, string RootKey) GetResourceInfo(Language language) + { + if (language == Language.Japanese) + return (Localization_Data_Path + ".japanese.json", "japanese"); + + return (Localization_Data_Path + ".english.json", "english"); + } + } +} diff --git a/GkmStatus/src/ui/TrayIconManager.cs b/GkmStatus/src/ui/TrayIconManager.cs new file mode 100644 index 0000000..81f7ecc --- /dev/null +++ b/GkmStatus/src/ui/TrayIconManager.cs @@ -0,0 +1,138 @@ +using GkmStatus.src.native; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace GkmStatus.src.ui +{ + public class TrayIconManager : IDisposable + { + private readonly NotifyIcon _trayIcon; + private readonly Icon _baseIcon; + private Icon? _currentStatusIcon; + private IntPtr _statusIconHandle = IntPtr.Zero; + private bool _disposed; + + private readonly ToolStripItem _detailsItem; + private readonly ToolStripItem _stateItem; + private readonly ToolStripItem _produceItem; + private readonly ToolStripItem _connectItem; + private readonly ToolStripItem _pauseItem; + private readonly ToolStripItem _disconnectItem; + private readonly ToolStripSeparator _settingsSeparator; + + public TrayIconManager(NotifyIcon trayIcon, Icon baseIcon) + { + _trayIcon = trayIcon; + _baseIcon = baseIcon; + + var menu = _trayIcon.ContextMenuStrip!; + _detailsItem = menu.Items.Cast().First(i => i.Name == "trayMenuDetails"); + _stateItem = menu.Items.Cast().First(i => i.Name == "trayMenuState"); + _produceItem = menu.Items.Cast().First(i => i.Name == "trayMenuProduce"); + _connectItem = menu.Items.Cast().First(i => i.Name == "trayMenuConnect"); + _pauseItem = menu.Items.Cast().First(i => i.Name == "trayMenuPause"); + _disconnectItem = menu.Items.Cast().First(i => i.Name == "trayMenuDisconnect"); + _settingsSeparator = menu.Items.Cast().OfType().First(s => s.Tag?.ToString() == "SepSettings"); + } + + public void UpdateStatusIcon(Color? color, string? statusText = null) + { + string appDisplayName = I18n.T(I18n.Text_List.App_Name); + _trayIcon.Text = string.IsNullOrEmpty(statusText) ? appDisplayName : $"{appDisplayName} - {statusText}"; + + CleanupIcon(); + + if (color == null) + { + _trayIcon.Icon = _baseIcon; + return; + } + + try + { + using var bitmap = _baseIcon.ToBitmap(); + using (var g = Graphics.FromImage(bitmap)) + { + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + int size = bitmap.Width / 4; + int x = bitmap.Width - size - 2; + int y = 2; + + using var brush = new SolidBrush(color.Value); + g.FillEllipse(brush, x, y, size, size); + + using var pen = new Pen(Color.White, 1); + g.DrawEllipse(pen, x, y, size, size); + } + + _statusIconHandle = bitmap.GetHicon(); + _currentStatusIcon = Icon.FromHandle(_statusIconHandle); + _trayIcon.Icon = _currentStatusIcon; + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to update tray icon: {ex.Message}"); + _trayIcon.Icon = _baseIcon; + } + } + + public void UpdateMenuState(RpcStatus status) + { + bool isConnected = status is RpcStatus.Connected or RpcStatus.Paused; + bool isPaused = status == RpcStatus.Paused; + bool showSettings = isConnected && !isPaused; + + _detailsItem.Visible = showSettings; + _stateItem.Visible = showSettings; + _produceItem.Visible = showSettings; + _settingsSeparator.Visible = showSettings; + + _connectItem.Visible = !isConnected || isPaused; + _pauseItem.Visible = isConnected && !isPaused; + _disconnectItem.Visible = isConnected; + } + + public void ShowNotification(string title, string message, string? url = null) + { + _trayIcon.Tag = url; + _trayIcon.ShowBalloonTip(3000, title, message, ToolTipIcon.Info); + } + + private void CleanupIcon() + { + if (_statusIconHandle != IntPtr.Zero) + { + Native.DestroyIcon(_statusIconHandle); + _statusIconHandle = IntPtr.Zero; + } + + _currentStatusIcon?.Dispose(); + _currentStatusIcon = null; + } + + public void SetProduceMenuEnabled(bool enabled) + { + _produceItem?.Enabled = enabled; + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + if (disposing) + { + CleanupIcon(); + _trayIcon.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} From 7f10c25b10abc3a5a746da404aac458948ad252e Mon Sep 17 00:00:00 2001 From: mumeinosato <66110797+mumeinosato@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:55:41 +0900 Subject: [PATCH 2/7] Update GkmStatus/src/ProcessWatcher.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- GkmStatus/src/ProcessWatcher.cs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/GkmStatus/src/ProcessWatcher.cs b/GkmStatus/src/ProcessWatcher.cs index b5f0870..34369b8 100644 --- a/GkmStatus/src/ProcessWatcher.cs +++ b/GkmStatus/src/ProcessWatcher.cs @@ -46,17 +46,30 @@ public bool Enabled private void OnTick(object? sender, EventArgs e) { var processes = Process.GetProcessesByName(_processName); - bool isNowRunning = processes.Length > 0; + try + { + bool isNowRunning = processes.Length > 0; - if(isNowRunning && !_wasRunning) { - IsRunning = true; - ProcessStarted?.Invoke(this, EventArgs.Empty); - }else if(!isNowRunning && _wasRunning) { - IsRunning = false; - ProcessStopped?.Invoke(this, EventArgs.Empty); - } + if (isNowRunning && !_wasRunning) + { + IsRunning = true; + ProcessStarted?.Invoke(this, EventArgs.Empty); + } + else if (!isNowRunning && _wasRunning) + { + IsRunning = false; + ProcessStopped?.Invoke(this, EventArgs.Empty); + } - _wasRunning = isNowRunning; + _wasRunning = isNowRunning; + } + finally + { + foreach (var process in processes) + { + process.Dispose(); + } + } } protected virtual void Dispose(bool disposing) From a6ac8a931e34210f803e7d3e746184cc9f3e8ead Mon Sep 17 00:00:00 2001 From: mumeinosato <66110797+mumeinosato@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:55:50 +0900 Subject: [PATCH 3/7] Update GkmStatus/src/ConfigManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- GkmStatus/src/ConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GkmStatus/src/ConfigManager.cs b/GkmStatus/src/ConfigManager.cs index e3e6d96..3c2ce23 100644 --- a/GkmStatus/src/ConfigManager.cs +++ b/GkmStatus/src/ConfigManager.cs @@ -30,7 +30,7 @@ public void Load() { if (!File.Exists(CONFIG_PATH)) { - Config = Config = CreateDefaultConfig(); + Config = CreateDefaultConfig(); return; } From 37541d68ffbf60765bcc30f4cfe881ca59fcb4ae Mon Sep 17 00:00:00 2001 From: mumeinosato <66110797+mumeinosato@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:56:09 +0900 Subject: [PATCH 4/7] Update GkmStatus/MainForm.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- GkmStatus/MainForm.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GkmStatus/MainForm.cs b/GkmStatus/MainForm.cs index 6917692..d3e9e56 100644 --- a/GkmStatus/MainForm.cs +++ b/GkmStatus/MainForm.cs @@ -265,6 +265,9 @@ private void ApplyLanguage() Native.SetPlaceholder(txtPName, I18n.T(Placeholder_PName)); Native.SetPlaceholder(txtBtn1Label, I18n.T(Placeholder_BtnLabel, 1)); + Native.SetPlaceholder(txtBtn1Url, I18n.T(Placeholder_BtnUrl, 1)); + Native.SetPlaceholder(txtBtn2Label, I18n.T(Placeholder_BtnLabel, 2)); + Native.SetPlaceholder(txtBtn2Url, I18n.T(Placeholder_BtnUrl, 2)); if (rpc.Status != RpcStatus.Disconnected) { From d5635786662907732e7065e09bacde65762cda57 Mon Sep 17 00:00:00 2001 From: mumeinosato <66110797+mumeinosato@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:56:42 +0900 Subject: [PATCH 5/7] Update GkmStatus/Resources/I18n/japanese.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- GkmStatus/Resources/I18n/japanese.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/GkmStatus/Resources/I18n/japanese.json b/GkmStatus/Resources/I18n/japanese.json index d433983..98fb60a 100644 --- a/GkmStatus/Resources/I18n/japanese.json +++ b/GkmStatus/Resources/I18n/japanese.json @@ -141,14 +141,6 @@ "text": "プロデューサー名" }, - { - "key": "Details_None", - "text": "なし" - }, - { - "key": "Details_PName", - "text": "プロデューサー名" - }, { "key": "Details_PLv", "text": "プロデューサーレベル" From fa65cc8e1f99aa6c301994a049dac38c16b7a201 Mon Sep 17 00:00:00 2001 From: mumeinosato <66110797+mumeinosato@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:58:38 +0900 Subject: [PATCH 6/7] Update GkmStatus/MainForm.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- GkmStatus/MainForm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GkmStatus/MainForm.cs b/GkmStatus/MainForm.cs index d3e9e56..8447fab 100644 --- a/GkmStatus/MainForm.cs +++ b/GkmStatus/MainForm.cs @@ -63,7 +63,7 @@ public MainForm() ApplyComponent(); ResumeLayout(); - _trayIconManager = new TrayIconManager(this.trayIcon, this.Icon!); + _trayIconManager = new TrayIconManager(this.trayIcon, this.Icon ?? SystemIcons.Application); _processWatcher = new ProcessWatcher(PROCESS_NAME); _processWatcher.ProcessStarted += (s, e) => From 6cac2ccbdc842b574704f081b46543b168538be2 Mon Sep 17 00:00:00 2001 From: mumeinosato <66110797+mumeinosato@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:59:35 +0900 Subject: [PATCH 7/7] Update GkmStatus/src/ui/ThemeManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- GkmStatus/src/ui/ThemeManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/GkmStatus/src/ui/ThemeManager.cs b/GkmStatus/src/ui/ThemeManager.cs index 12fd2a3..521ff3a 100644 --- a/GkmStatus/src/ui/ThemeManager.cs +++ b/GkmStatus/src/ui/ThemeManager.cs @@ -41,8 +41,7 @@ private static Color GetWindowsThemeColor() { try { - using var key = Registry.CurrentUser.OpenSubKey(WINDOWS_THEME_REG_KEY); - var value = key?.GetValue("AppsUseLightTheme"); + var value = Registry.GetValue(WINDOWS_THEME_REG_KEY, "AppsUseLightTheme", null); if (value is int i && i == 1) return COLOR_LIGHT_BG; }