diff --git a/CS-Jukebox/AdditionalSongsForm.cs b/CS-Jukebox/AdditionalSongsForm.cs
index 317915f..a2b1b9d 100644
--- a/CS-Jukebox/AdditionalSongsForm.cs
+++ b/CS-Jukebox/AdditionalSongsForm.cs
@@ -9,6 +9,7 @@ namespace CS_Jukebox
/// Edits the optional tracks for one game event.
public class AdditionalSongsForm : Form
{
+ private readonly MusicEventType eventType;
private readonly List songs;
private readonly ListBox songsList = new ListBox();
private readonly TextBox pathTextBox = new TextBox();
@@ -16,20 +17,22 @@ public class AdditionalSongsForm : Form
private readonly TextBox startMinutesTextBox = new TextBox();
private readonly TextBox startSecondsTextBox = new TextBox();
private readonly Jukebox previewJukebox = new Jukebox();
+ private readonly Button previewButton = new Button();
private int selectedIndex = -1;
private bool refreshingSongList;
private SongProfile previewSong;
public List Songs => songs;
- public AdditionalSongsForm(string eventName, IEnumerable currentSongs)
+ public AdditionalSongsForm(MusicEventType eventType, IEnumerable currentSongs)
{
+ this.eventType = eventType;
songs = currentSongs?
.Where(song => song != null)
.Select(song => new SongProfile(song.Path, song.Volume) { Start = song.Start, NormalizationGain = song.NormalizationGain })
.ToList() ?? new List();
- Text = eventName + " - Extra tracks";
+ Text = MusicEventTiming.GetDisplayName(eventType) + " - Extra tracks";
Width = 580;
Height = 330;
MinimizeBox = false;
@@ -38,7 +41,9 @@ public AdditionalSongsForm(string eventName, IEnumerable currentSon
BuildControls();
RefreshSongList();
- FormClosed += (sender, args) => previewJukebox.Dispose();
+ previewJukebox.PreviewCompleted += PreviewJukebox_PreviewCompleted;
+ FormClosed += AdditionalSongsForm_FormClosed;
+ ThemeManager.Apply(this);
}
private void BuildControls()
@@ -53,7 +58,8 @@ private void BuildControls()
var pathLabel = new Label { Text = "Audio file:", Left = 240, Top = 18, AutoSize = true };
pathTextBox.SetBounds(240, 40, 180, 23);
- var previewButton = new Button { Text = "▶", Left = 428, Top = 39, Width = 30, Height = 28 };
+ previewButton.Text = "▶";
+ previewButton.SetBounds(428, 39, 30, 28);
previewButton.Click += (sender, args) => TogglePreview(previewButton);
var browseButton = new Button { Text = "Browse...", Left = 478, Top = 39, Width = 78 };
browseButton.Click += (sender, args) => BrowseForSong();
@@ -180,7 +186,10 @@ private void TogglePreview(Button previewButton)
return;
}
- previewJukebox.PlayPreviewSong(song);
+ if (Properties.LimitPreviewToEventDuration)
+ previewJukebox.PlayPreviewSong(song, MusicEventTiming.GetPreviewDurationSeconds(eventType));
+ else
+ previewJukebox.PlayPreviewSong(song);
previewSong = song;
previewButton.Text = "■";
}
@@ -191,6 +200,24 @@ private void TogglePreview(Button previewButton)
}
}
+ private void PreviewJukebox_PreviewCompleted(object sender, PreviewCompletedEventArgs e)
+ {
+ previewSong = null;
+ previewButton.Text = "▶";
+
+ if (IsDisposed || Disposing) return;
+ string message = e.Reason == PreviewCompletionReason.EventEnded
+ ? "Event ended before music."
+ : "Music ended before event.";
+ MessageBox.Show(this, message, "Preview complete", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
+
+ private void AdditionalSongsForm_FormClosed(object sender, FormClosedEventArgs e)
+ {
+ previewJukebox.PreviewCompleted -= PreviewJukebox_PreviewCompleted;
+ previewJukebox.Dispose();
+ }
+
private void VolumeTrackBar_ValueChanged(object sender, EventArgs e)
{
if (previewSong == null || !previewJukebox.IsPlaybackActive() ||
diff --git a/CS-Jukebox/CS-Jukebox.csproj b/CS-Jukebox/CS-Jukebox.csproj
index fa07c6d..0f5d2c3 100644
--- a/CS-Jukebox/CS-Jukebox.csproj
+++ b/CS-Jukebox/CS-Jukebox.csproj
@@ -7,14 +7,27 @@
true
CS_Jukebox.Program
logo.ico
+ 1.3.3
+ 1.3.3.0
+ 1.3.3.0
win-x64
true
true
+
+ true
+ true
+ true
false
+
+ none
+ false
+ false
+
+
@@ -25,9 +38,8 @@
-
- Always
-
+
diff --git a/CS-Jukebox/GameLogic.cs b/CS-Jukebox/GameLogic.cs
index 8fc18af..6b34240 100644
--- a/CS-Jukebox/GameLogic.cs
+++ b/CS-Jukebox/GameLogic.cs
@@ -143,9 +143,10 @@ void OnNewGameState(GameState gs)
if (gs.Round.Phase == RoundPhase.Live && musicState != MusicState.Live && musicState != MusicState.BombPlanted)
{
jukebox.Stop();
- // Play the round-start track to completion, matching the behaviour
- // of FreezeTime and MVP which also use the two-arg overload.
- if (PlaySongIfValid(Properties.SelectedKit.PickSong(Properties.SelectedKit.startSong, Properties.SelectedKit.startSongs), false))
+ if (PlaySongIfValid(
+ Properties.SelectedKit.PickSong(Properties.SelectedKit.startSong, Properties.SelectedKit.startSongs),
+ false,
+ MusicEventTiming.RoundStartSeconds))
{
musicState = MusicState.Live;
Logger.LogEvent("TransitionToLive", $"SelectedKit={Properties.SelectedKit.Name}");
@@ -240,7 +241,7 @@ private void HandlePlayerDeath(GameState gs)
if (PlaySongIfValid(
Properties.SelectedKit.PickSong(Properties.SelectedKit.deathSong, Properties.SelectedKit.deathSongs),
false,
- 5))
+ MusicEventTiming.PlayerDeathSeconds))
{
Console.WriteLine("Local player died");
Logger.LogEvent("PlayerDeathDetected");
diff --git a/CS-Jukebox/GamePathForm.cs b/CS-Jukebox/GamePathForm.cs
index d85db52..ddab41b 100644
--- a/CS-Jukebox/GamePathForm.cs
+++ b/CS-Jukebox/GamePathForm.cs
@@ -23,6 +23,7 @@ public GamePathForm()
MinimizeBox = false;
if (!string.IsNullOrWhiteSpace(Properties.GameDir))
dirTextBox.Text = Properties.GameDir;
+ ThemeManager.Apply(this);
}
//Open folder browser dialog
diff --git a/CS-Jukebox/Jukebox.cs b/CS-Jukebox/Jukebox.cs
index 3c5ecc0..274b92c 100644
--- a/CS-Jukebox/Jukebox.cs
+++ b/CS-Jukebox/Jukebox.cs
@@ -22,6 +22,10 @@ public class Jukebox : IDisposable
private bool isPlaying = false;
private long scheduledStopAtMilliseconds;
+ private bool previewCompletionPending;
+ private long previewStopAtMilliseconds;
+
+ public event EventHandler PreviewCompleted;
public Jukebox()
{
@@ -110,7 +114,24 @@ public void PlaySong(SongProfile song, bool loop)
public void PlayPreviewSong(SongProfile song)
{
- Logger.LogEntry($"song={song?.Path}");
+ PlayPreviewSongInternal(song, null, false);
+ }
+
+ ///
+ /// Plays a preview and reports why it completed. A null duration keeps
+ /// the preview unbounded but still reports when the source file ends.
+ ///
+ public void PlayPreviewSong(SongProfile song, int? durationSeconds)
+ {
+ if (durationSeconds.HasValue && durationSeconds.Value <= 0)
+ throw new ArgumentOutOfRangeException(nameof(durationSeconds));
+
+ PlayPreviewSongInternal(song, durationSeconds, true);
+ }
+
+ private void PlayPreviewSongInternal(SongProfile song, int? durationSeconds, bool reportCompletion)
+ {
+ Logger.LogEntry($"song={song?.Path}, duration={durationSeconds?.ToString() ?? "unbounded"}, reportCompletion={reportCompletion}");
if (song == null || string.IsNullOrWhiteSpace(song.Path) || !File.Exists(song.Path))
{
Logger.Log($"WARNING: Song file path is missing or invalid: '{song?.Path}'");
@@ -121,6 +142,7 @@ public void PlayPreviewSong(SongProfile song)
lock (lockObj)
{
CancelScheduledStopInternal();
+ CancelPreviewCompletionInternal();
CancelFadeInternal();
previewMode = true;
currentSong = song;
@@ -144,12 +166,17 @@ public void PlayPreviewSong(SongProfile song)
outputDevice = new WaveOutEvent();
outputDevice.Init(waveStream);
outputDevice.Play();
+ previewCompletionPending = reportCompletion;
+ previewStopAtMilliseconds = durationSeconds.HasValue
+ ? Environment.TickCount64 + (durationSeconds.Value * 1000L)
+ : 0;
songTimer.Start();
Logger.LogExit("Preview playback started successfully");
}
catch (Exception ex)
{
Logger.LogError("PlayPreviewSong failed", ex);
+ CancelPreviewCompletionInternal();
CleanupOutput();
throw new InvalidOperationException("The selected audio file could not be played.", ex);
}
@@ -272,6 +299,7 @@ public void Stop()
lock (lockObj)
{
CancelScheduledStopInternal();
+ CancelPreviewCompletionInternal();
if (!IsPlaybackActive())
{
CleanupOutput();
@@ -347,6 +375,12 @@ private void CancelScheduledStopInternal()
scheduledStopAtMilliseconds = 0;
}
+ private void CancelPreviewCompletionInternal()
+ {
+ previewCompletionPending = false;
+ previewStopAtMilliseconds = 0;
+ }
+
private void CancelFadeInternal()
{
CancellationTokenSource cts;
@@ -364,6 +398,7 @@ public void StopImmediately()
lock (lockObj)
{
CancelScheduledStopInternal();
+ CancelPreviewCompletionInternal();
CancelFadeInternal();
CleanupOutput();
}
@@ -452,11 +487,28 @@ private void TimerTick(object sender, EventArgs e)
if (outputDevice.PlaybackState == PlaybackState.Stopped && fadeCts == null)
{
+ bool reportMusicEnded = previewCompletionPending;
CancelScheduledStopInternal();
+ CancelPreviewCompletionInternal();
lock (lockObj)
{
CleanupOutput();
}
+ if (reportMusicEnded)
+ OnPreviewCompleted(PreviewCompletionReason.MusicEnded);
+ return;
+ }
+
+ if (previewCompletionPending && previewStopAtMilliseconds > 0 &&
+ Environment.TickCount64 >= previewStopAtMilliseconds)
+ {
+ CancelScheduledStopInternal();
+ CancelPreviewCompletionInternal();
+ lock (lockObj)
+ {
+ CleanupOutput();
+ }
+ OnPreviewCompleted(PreviewCompletionReason.EventEnded);
return;
}
@@ -469,6 +521,18 @@ private void TimerTick(object sender, EventArgs e)
UpdateVolume();
}
+ private void OnPreviewCompleted(PreviewCompletionReason reason)
+ {
+ try
+ {
+ PreviewCompleted?.Invoke(this, new PreviewCompletedEventArgs(reason));
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("PreviewCompleted handler failed", ex);
+ }
+ }
+
private static bool IsGameFocused()
{
try
diff --git a/CS-Jukebox/MainForm.Designer.cs b/CS-Jukebox/MainForm.Designer.cs
index 6cd2a16..7d9640d 100644
--- a/CS-Jukebox/MainForm.Designer.cs
+++ b/CS-Jukebox/MainForm.Designer.cs
@@ -39,6 +39,7 @@ private void InitializeComponent()
this.editButton = new System.Windows.Forms.Button();
this.notifyIcon = new System.Windows.Forms.NotifyIcon(this.components);
this.autoCheckBox = new System.Windows.Forms.CheckBox();
+ this.darkThemeCheckBox = new System.Windows.Forms.CheckBox();
((System.ComponentModel.ISupportInitialize)(this.trackBar1)).BeginInit();
this.SuspendLayout();
//
@@ -131,19 +132,31 @@ private void InitializeComponent()
// autoCheckBox
//
this.autoCheckBox.AutoSize = true;
- this.autoCheckBox.Location = new System.Drawing.Point(139, 107);
+ this.autoCheckBox.Location = new System.Drawing.Point(139, 133);
this.autoCheckBox.Name = "autoCheckBox";
this.autoCheckBox.Size = new System.Drawing.Size(138, 19);
- this.autoCheckBox.TabIndex = 7;
+ this.autoCheckBox.TabIndex = 10;
this.autoCheckBox.Text = "Autostart CS Jukebox";
this.autoCheckBox.UseVisualStyleBackColor = true;
this.autoCheckBox.CheckedChanged += new System.EventHandler(this.autoCheckBox_CheckedChanged);
- //
+ //
+ // darkThemeCheckBox
+ //
+ this.darkThemeCheckBox.AutoSize = true;
+ this.darkThemeCheckBox.Location = new System.Drawing.Point(139, 106);
+ this.darkThemeCheckBox.Name = "darkThemeCheckBox";
+ this.darkThemeCheckBox.Size = new System.Drawing.Size(88, 19);
+ this.darkThemeCheckBox.TabIndex = 7;
+ this.darkThemeCheckBox.Text = "Dark theme";
+ this.darkThemeCheckBox.UseVisualStyleBackColor = true;
+ this.darkThemeCheckBox.CheckedChanged += new System.EventHandler(this.darkThemeCheckBox_CheckedChanged);
+ //
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.ClientSize = new System.Drawing.Size(285, 135);
+ this.ClientSize = new System.Drawing.Size(285, 162);
+ this.Controls.Add(this.darkThemeCheckBox);
this.Controls.Add(this.autoCheckBox);
this.Controls.Add(this.importButton);
this.Controls.Add(this.exportButton);
@@ -175,6 +188,7 @@ private void InitializeComponent()
private System.Windows.Forms.Button editButton;
private System.Windows.Forms.NotifyIcon notifyIcon;
private System.Windows.Forms.CheckBox autoCheckBox;
+ private System.Windows.Forms.CheckBox darkThemeCheckBox;
}
}
diff --git a/CS-Jukebox/MainForm.cs b/CS-Jukebox/MainForm.cs
index d0bb24e..30a4f3a 100644
--- a/CS-Jukebox/MainForm.cs
+++ b/CS-Jukebox/MainForm.cs
@@ -18,6 +18,7 @@ public partial class MainForm : Form
static extern bool AllocConsole();
private GameLogic logic;
+ private bool initializingTheme;
public MainForm()
{
@@ -26,9 +27,11 @@ public MainForm()
//AllocConsole(); //Enable console
MaximizeBox = false;
- // Use default system theme (no custom theming applied)
-
Properties.Load();
+ initializingTheme = true;
+ darkThemeCheckBox.Checked = Properties.DarkTheme;
+ initializingTheme = false;
+ ThemeManager.Apply(this);
// If GameDir is missing or invalid, attempt auto-detection of CS2 install.
bool gameDirValid = false;
@@ -559,6 +562,15 @@ private void autoCheckBox_CheckedChanged(object sender, EventArgs e)
RegisterInStartup(autoCheckBox.Checked);
}
+ private void darkThemeCheckBox_CheckedChanged(object sender, EventArgs e)
+ {
+ if (initializingTheme) return;
+
+ Properties.DarkTheme = darkThemeCheckBox.Checked;
+ ThemeManager.ApplyToOpenForms();
+ Properties.SaveProperties();
+ }
+
private async void exportButton_Click(object sender, EventArgs e)
{
if (Properties.SelectedKit == null)
diff --git a/CS-Jukebox/MusicEventTiming.cs b/CS-Jukebox/MusicEventTiming.cs
new file mode 100644
index 0000000..8e91011
--- /dev/null
+++ b/CS-Jukebox/MusicEventTiming.cs
@@ -0,0 +1,66 @@
+namespace CS_Jukebox
+{
+ public enum MusicEventType
+ {
+ MainMenu,
+ FreezeTime,
+ RoundStart,
+ BombPlanted,
+ BombTenSeconds,
+ RoundTenSeconds,
+ Mvp,
+ RoundWon,
+ RoundLost,
+ PlayerDeath
+ }
+
+ ///
+ /// Fixed preview windows documented by the project. These values describe
+ /// the length of the event preview after the configured Start At offset.
+ ///
+ public static class MusicEventTiming
+ {
+ public const int FreezeTimeSeconds = 15;
+ public const int RoundStartSeconds = 5;
+ public const int BombPlantedSeconds = 30;
+ public const int TenSecondWarningSeconds = 10;
+ public const int RoundResultSeconds = 7;
+ public const int PlayerDeathSeconds = 5;
+
+ public static int? GetPreviewDurationSeconds(MusicEventType eventType)
+ {
+ return eventType switch
+ {
+ MusicEventType.FreezeTime => FreezeTimeSeconds,
+ MusicEventType.RoundStart => RoundStartSeconds,
+ MusicEventType.BombPlanted => BombPlantedSeconds,
+ MusicEventType.BombTenSeconds => TenSecondWarningSeconds,
+ MusicEventType.RoundTenSeconds => TenSecondWarningSeconds,
+ MusicEventType.Mvp => RoundResultSeconds,
+ MusicEventType.RoundWon => RoundResultSeconds,
+ MusicEventType.RoundLost => RoundResultSeconds,
+ MusicEventType.PlayerDeath => PlayerDeathSeconds,
+ MusicEventType.MainMenu => null,
+ _ => null
+ };
+ }
+
+ public static string GetDisplayName(MusicEventType eventType)
+ {
+ return eventType switch
+ {
+ MusicEventType.MainMenu => "Main Menu",
+ MusicEventType.FreezeTime => "Freeze Time",
+ MusicEventType.RoundStart => "Round Start",
+ MusicEventType.BombPlanted => "Bomb Planted",
+ MusicEventType.BombTenSeconds => "Bomb: 10 seconds",
+ MusicEventType.RoundTenSeconds => "Round: 10 seconds",
+ MusicEventType.Mvp => "MVP",
+ MusicEventType.RoundWon => "Round Won",
+ MusicEventType.RoundLost => "Round Lost",
+ MusicEventType.PlayerDeath => "Player Death",
+ _ => eventType.ToString()
+ };
+ }
+ }
+}
diff --git a/CS-Jukebox/MusicSelector.cs b/CS-Jukebox/MusicSelector.cs
index f829583..c2dbeba 100644
--- a/CS-Jukebox/MusicSelector.cs
+++ b/CS-Jukebox/MusicSelector.cs
@@ -13,6 +13,7 @@ public partial class MusicSelector : Form
private Jukebox previewJukebox;
private SongProfile previewSong;
private TrackBar previewVolumeTrackBar;
+ private MusicEventType? previewEventType;
private readonly Dictionary startTimeControls = new();
private readonly GroupBox deathGroup = new GroupBox();
private readonly Label deathStartLabel = new Label();
@@ -22,12 +23,14 @@ public partial class MusicSelector : Form
private readonly Button deathPreviewButton = new Button();
private readonly TextBox deathTextBox = new TextBox();
private readonly Button saveAsCopyButton = new Button();
+ private readonly CheckBox previewDurationCheckBox = new CheckBox();
public MusicSelector(MusicKit newKit, bool? createKit)
{
InitializeComponent();
CreateDeathScenarioControls();
CreateStartTimeControls();
+ CreatePreviewDurationControl();
ConfigureEditorLayout();
MaximizeBox = false;
@@ -48,10 +51,13 @@ public MusicSelector(MusicKit newKit, bool? createKit)
saveAsCopyButton.Size = new System.Drawing.Size(110, 27);
saveAsCopyButton.Click += saveAsCopyButton_Click;
Controls.Add(saveAsCopyButton);
+ saveAsCopyButton.Visible = !createMode;
// Jukebox used for previewing individual tracks inside the editor
previewJukebox = new Jukebox();
+ previewJukebox.PreviewCompleted += PreviewJukebox_PreviewCompleted;
FormClosed += MusicSelector_FormClosed;
+ ThemeManager.Apply(this);
}
private void MusicSelector_Load(object sender, EventArgs e)
@@ -96,7 +102,7 @@ private void CreateDeathScenarioControls()
deathPreviewButton.Name = "deathPreviewButton";
deathPreviewButton.Text = "▶";
deathPreviewButton.Click += (sender, args) =>
- TogglePreview(deathTextBox, deathTrackBar, deathStartTextBox, deathPreviewButton);
+ TogglePreview(deathTextBox, deathTrackBar, deathStartTextBox, deathPreviewButton, MusicEventType.PlayerDeath);
deathTextBox.Name = "deathTextBox";
@@ -108,6 +114,16 @@ private void CreateDeathScenarioControls()
Controls.Add(deathGroup);
}
+ private void CreatePreviewDurationControl()
+ {
+ previewDurationCheckBox.Name = "previewDurationCheckBox";
+ previewDurationCheckBox.Text = "Limit preview to event duration";
+ previewDurationCheckBox.AutoSize = true;
+ previewDurationCheckBox.Checked = Properties.LimitPreviewToEventDuration;
+ previewDurationCheckBox.CheckedChanged += PreviewDurationCheckBox_CheckedChanged;
+ Controls.Add(previewDurationCheckBox);
+ }
+
private void AddStartTimeControls(GroupBox group, Label label, TextBox legacyTextBox)
{
legacyTextBox.Visible = false;
@@ -144,6 +160,7 @@ private void ConfigureEditorLayout()
nameTextBox.Location = new System.Drawing.Point(62, 11);
nameTextBox.Size = new System.Drawing.Size(200, 23);
deleteButton.Location = new System.Drawing.Point(272, 10);
+ previewDurationCheckBox.Location = new System.Drawing.Point(370, 13);
saveButton.Text = "Apply Changes";
saveButton.Size = new System.Drawing.Size(110, 27);
saveButton.Location = new System.Drawing.Point(12, 625);
@@ -205,9 +222,37 @@ private void EnsurePreviewButtonsVisible()
private void MusicSelector_FormClosed(object sender, FormClosedEventArgs e)
{
+ if (previewJukebox != null)
+ previewJukebox.PreviewCompleted -= PreviewJukebox_PreviewCompleted;
previewJukebox?.Dispose();
}
+ private void PreviewDurationCheckBox_CheckedChanged(object sender, EventArgs e)
+ {
+ Properties.LimitPreviewToEventDuration = previewDurationCheckBox.Checked;
+ Properties.SaveProperties();
+
+ previewJukebox?.StopImmediately();
+ previewSong = null;
+ previewVolumeTrackBar = null;
+ previewEventType = null;
+ ResetPreviewButtonTexts();
+ }
+
+ private void PreviewJukebox_PreviewCompleted(object sender, PreviewCompletedEventArgs e)
+ {
+ previewSong = null;
+ previewVolumeTrackBar = null;
+ previewEventType = null;
+ ResetPreviewButtonTexts();
+
+ if (IsDisposed || Disposing) return;
+ string message = e.Reason == PreviewCompletionReason.EventEnded
+ ? "Event ended."
+ : "Music ended.";
+ MessageBox.Show(this, message, "Preview complete", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
+
private void ConfigurePreviewVolumeSynchronization()
{
foreach (TrackBar trackBar in new[]
@@ -233,19 +278,19 @@ private void PreviewVolumeTrackBar_ValueChanged(object sender, EventArgs e)
private void AddExtraSongsButtons()
{
- AddExtraSongsButton(freezeGroup, "Freeze Time", () => currentKit.freezeSongs, songs => currentKit.freezeSongs = songs);
- AddExtraSongsButton(startGroup, "Round Start", () => currentKit.startSongs, songs => currentKit.startSongs = songs);
- AddExtraSongsButton(bombGroup, "Bomb Planted", () => currentKit.bombSongs, songs => currentKit.bombSongs = songs);
- AddExtraSongsButton(wonGroup, "Round Won", () => currentKit.winSongs, songs => currentKit.winSongs = songs);
- AddExtraSongsButton(lostGroup, "Round Lost", () => currentKit.loseSongs, songs => currentKit.loseSongs = songs);
- AddExtraSongsButton(MVPGroup, "MVP", () => currentKit.MVPSongs, songs => currentKit.MVPSongs = songs);
- AddExtraSongsButton(bombTenSecBox1, "Bomb: 10 seconds", () => currentKit.bombTenSecSongs, songs => currentKit.bombTenSecSongs = songs);
- AddExtraSongsButton(roundTenSecBox, "Round: 10 seconds", () => currentKit.roundTenSecSongs, songs => currentKit.roundTenSecSongs = songs);
- AddExtraSongsButton(mainMenuGroupBox, "Main Menu", () => currentKit.mainMenuSongs, songs => currentKit.mainMenuSongs = songs);
- AddExtraSongsButton(deathGroup, "Player Death", () => currentKit.deathSongs, songs => currentKit.deathSongs = songs);
+ AddExtraSongsButton(freezeGroup, MusicEventType.FreezeTime, () => currentKit.freezeSongs, songs => currentKit.freezeSongs = songs);
+ AddExtraSongsButton(startGroup, MusicEventType.RoundStart, () => currentKit.startSongs, songs => currentKit.startSongs = songs);
+ AddExtraSongsButton(bombGroup, MusicEventType.BombPlanted, () => currentKit.bombSongs, songs => currentKit.bombSongs = songs);
+ AddExtraSongsButton(wonGroup, MusicEventType.RoundWon, () => currentKit.winSongs, songs => currentKit.winSongs = songs);
+ AddExtraSongsButton(lostGroup, MusicEventType.RoundLost, () => currentKit.loseSongs, songs => currentKit.loseSongs = songs);
+ AddExtraSongsButton(MVPGroup, MusicEventType.Mvp, () => currentKit.MVPSongs, songs => currentKit.MVPSongs = songs);
+ AddExtraSongsButton(bombTenSecBox1, MusicEventType.BombTenSeconds, () => currentKit.bombTenSecSongs, songs => currentKit.bombTenSecSongs = songs);
+ AddExtraSongsButton(roundTenSecBox, MusicEventType.RoundTenSeconds, () => currentKit.roundTenSecSongs, songs => currentKit.roundTenSecSongs = songs);
+ AddExtraSongsButton(mainMenuGroupBox, MusicEventType.MainMenu, () => currentKit.mainMenuSongs, songs => currentKit.mainMenuSongs = songs);
+ AddExtraSongsButton(deathGroup, MusicEventType.PlayerDeath, () => currentKit.deathSongs, songs => currentKit.deathSongs = songs);
}
- private void AddExtraSongsButton(GroupBox group, string eventName,
+ private void AddExtraSongsButton(GroupBox group, MusicEventType eventType,
Func> getSongs, Action> setSongs)
{
var button = new Button
@@ -263,8 +308,9 @@ private void AddExtraSongsButton(GroupBox group, string eventName,
previewJukebox.StopImmediately();
previewSong = null;
previewVolumeTrackBar = null;
+ previewEventType = null;
ResetPreviewButtonTexts();
- using var editor = new AdditionalSongsForm(eventName, getSongs());
+ using var editor = new AdditionalSongsForm(eventType, getSongs());
if (editor.ShowDialog(this) == DialogResult.OK)
{
setSongs(editor.Songs);
@@ -502,7 +548,8 @@ private void menuButton_Click(object sender, EventArgs e)
}
// Preview button handlers
- private void TogglePreview(TextBox textBox, TrackBar volumeTrackBar, TextBox startTextBox, Button previewButton)
+ private void TogglePreview(TextBox textBox, TrackBar volumeTrackBar, TextBox startTextBox,
+ Button previewButton, MusicEventType eventType)
{
string path = textBox.Text;
if (string.IsNullOrWhiteSpace(path))
@@ -517,19 +564,24 @@ private void TogglePreview(TextBox textBox, TrackBar volumeTrackBar, TextBox sta
// If these exact parameters are already playing, stop preview.
if (previewSong != null && previewSong.Path == song.Path && previewSong.Volume == song.Volume &&
- previewSong.Start == song.Start && previewJukebox.IsPlaybackActive())
+ previewSong.Start == song.Start && previewEventType == eventType && previewJukebox.IsPlaybackActive())
{
previewJukebox.Stop();
previewSong = null;
previewVolumeTrackBar = null;
+ previewEventType = null;
previewButton.Text = "▶";
return;
}
// Preview exactly what will play in-game: volume and offset.
- previewJukebox.PlayPreviewSong(song);
+ if (Properties.LimitPreviewToEventDuration)
+ previewJukebox.PlayPreviewSong(song, MusicEventTiming.GetPreviewDurationSeconds(eventType));
+ else
+ previewJukebox.PlayPreviewSong(song);
previewSong = song;
previewVolumeTrackBar = volumeTrackBar;
+ previewEventType = eventType;
ResetPreviewButtonTexts();
@@ -556,15 +608,15 @@ private void ResetPreviewButtonTexts()
}
}
- private void freezePreviewButton_Click(object sender, EventArgs e) => TogglePreview(freezeTextBox, freezeTrackBar, freezeStartTextBox, freezePreviewButton);
- private void startPreviewButton_Click(object sender, EventArgs e) => TogglePreview(startTextBox, startTrackBar, startStartTextBox, startPreviewButton);
- private void bombPreviewButton_Click(object sender, EventArgs e) => TogglePreview(bombTextBox, bombTrackBar, bombStartTextBox, bombPreviewButton);
- private void wonPreviewButton_Click(object sender, EventArgs e) => TogglePreview(wonTextBox, wonTrackBar, wonStartTextBox, wonPreviewButton);
- private void lostPreviewButton_Click(object sender, EventArgs e) => TogglePreview(lostTextBox, lostTrackBar, lostStartTextBox, lostPreviewButton);
- private void MVPPreviewButton_Click(object sender, EventArgs e) => TogglePreview(MVPTextBox, MVPTrackBar, MVPStartTextBox, MVPPreviewButton);
- private void bombTenSecPreviewButton_Click(object sender, EventArgs e) => TogglePreview(bombTenSecTextBox, bombTenSecTrackBar, bombTenSecStartBox, bombTenSecPreviewButton);
- private void roundTenSecPreviewButton_Click(object sender, EventArgs e) => TogglePreview(roundTenSecTextBox, roundTenSecTrackBar, roundTenSecStartBox, roundTenSecPreviewButton);
- private void menuPreviewButton_Click(object sender, EventArgs e) => TogglePreview(menuTextBox, menuTrackBar, menuStartTextBox, menuPreviewButton);
+ private void freezePreviewButton_Click(object sender, EventArgs e) => TogglePreview(freezeTextBox, freezeTrackBar, freezeStartTextBox, freezePreviewButton, MusicEventType.FreezeTime);
+ private void startPreviewButton_Click(object sender, EventArgs e) => TogglePreview(startTextBox, startTrackBar, startStartTextBox, startPreviewButton, MusicEventType.RoundStart);
+ private void bombPreviewButton_Click(object sender, EventArgs e) => TogglePreview(bombTextBox, bombTrackBar, bombStartTextBox, bombPreviewButton, MusicEventType.BombPlanted);
+ private void wonPreviewButton_Click(object sender, EventArgs e) => TogglePreview(wonTextBox, wonTrackBar, wonStartTextBox, wonPreviewButton, MusicEventType.RoundWon);
+ private void lostPreviewButton_Click(object sender, EventArgs e) => TogglePreview(lostTextBox, lostTrackBar, lostStartTextBox, lostPreviewButton, MusicEventType.RoundLost);
+ private void MVPPreviewButton_Click(object sender, EventArgs e) => TogglePreview(MVPTextBox, MVPTrackBar, MVPStartTextBox, MVPPreviewButton, MusicEventType.Mvp);
+ private void bombTenSecPreviewButton_Click(object sender, EventArgs e) => TogglePreview(bombTenSecTextBox, bombTenSecTrackBar, bombTenSecStartBox, bombTenSecPreviewButton, MusicEventType.BombTenSeconds);
+ private void roundTenSecPreviewButton_Click(object sender, EventArgs e) => TogglePreview(roundTenSecTextBox, roundTenSecTrackBar, roundTenSecStartBox, roundTenSecPreviewButton, MusicEventType.RoundTenSeconds);
+ private void menuPreviewButton_Click(object sender, EventArgs e) => TogglePreview(menuTextBox, menuTrackBar, menuStartTextBox, menuPreviewButton, MusicEventType.MainMenu);
private void freezeStartTextbox_KeyPress(object sender, KeyPressEventArgs e)
{
diff --git a/CS-Jukebox/PreviewCompletedEventArgs.cs b/CS-Jukebox/PreviewCompletedEventArgs.cs
new file mode 100644
index 0000000..75c8ce4
--- /dev/null
+++ b/CS-Jukebox/PreviewCompletedEventArgs.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace CS_Jukebox
+{
+ public enum PreviewCompletionReason
+ {
+ MusicEnded,
+ EventEnded
+ }
+
+ public sealed class PreviewCompletedEventArgs : EventArgs
+ {
+ public PreviewCompletedEventArgs(PreviewCompletionReason reason)
+ {
+ Reason = reason;
+ }
+
+ public PreviewCompletionReason Reason { get; }
+ }
+}
diff --git a/CS-Jukebox/Properties.cs b/CS-Jukebox/Properties.cs
index 3a18fd5..241da90 100644
--- a/CS-Jukebox/Properties.cs
+++ b/CS-Jukebox/Properties.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.IO;
+using System.Reflection;
using System.Windows.Forms;
using Newtonsoft.Json;
@@ -17,6 +18,8 @@ static class Properties
public static string GameDir = null;
public static int MasterVolume = 70;
+ public static bool LimitPreviewToEventDuration = false;
+ public static bool DarkTheme = false;
public static MusicKit SelectedKit
{
get { return selectedKit; }
@@ -26,7 +29,8 @@ public static MusicKit SelectedKit
public static List MusicKits = null;
private static string startDir;
- private static string appDir;
+ private static bool dataDirectoryWritable = true;
+ private const string ConfigResourceName = "CS_Jukebox.gamestate_integration_jukebox.cfg";
public static string KitsDirectory => Path.Combine(startDir ?? GetAppDirectory(), MusicKitsPath);
@@ -50,12 +54,16 @@ public static void Save()
//Converts settings to json file then saves it
public static void SaveProperties()
{
+ if (!dataDirectoryWritable) return;
+
string dir = Path.Combine(startDir, PropertiesFilePath);
PropertiesFile propFile = new PropertiesFile();
propFile.GameDir = GameDir;
propFile.SelectedKitName = SelectedKitName;
propFile.MasterVolume = MasterVolume;
+ propFile.LimitPreviewToEventDuration = LimitPreviewToEventDuration;
+ propFile.DarkTheme = DarkTheme;
string jsonFile = JsonConvert.SerializeObject(propFile);
try
@@ -71,8 +79,7 @@ public static void SaveProperties()
//Reads properties file then deserializes it
public static void LoadProperties()
{
- appDir = GetAppDirectory();
- startDir = GetDataDirectory(appDir);
+ startDir = GetDataDirectory(GetAppDirectory());
Console.WriteLine("App Directory: " + startDir);
string dir = Path.Combine(startDir, PropertiesFilePath);
@@ -84,10 +91,14 @@ public static void LoadProperties()
GameDir = propFile.GameDir;
SelectedKitName = propFile.SelectedKitName;
MasterVolume = Math.Clamp(propFile.MasterVolume, 0, 100);
+ LimitPreviewToEventDuration = propFile.LimitPreviewToEventDuration;
+ DarkTheme = propFile.DarkTheme;
}
catch (FileNotFoundException)
{
MasterVolume = 70;
+ LimitPreviewToEventDuration = false;
+ DarkTheme = false;
}
catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is JsonException)
{
@@ -95,6 +106,8 @@ public static void LoadProperties()
GameDir = null;
SelectedKitName = null;
MasterVolume = 70;
+ LimitPreviewToEventDuration = false;
+ DarkTheme = false;
}
}
@@ -105,34 +118,29 @@ private static string GetAppDirectory()
private static string GetDataDirectory(string applicationDirectory)
{
- if (CanWriteToDirectory(applicationDirectory)) return applicationDirectory;
-
- string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
- if (string.IsNullOrWhiteSpace(localAppData)) return applicationDirectory;
-
- string fallbackDirectory = Path.Combine(localAppData, "CS-Jukebox");
- try
- {
- Directory.CreateDirectory(fallbackDirectory);
- }
- catch (Exception e)
- {
- ReportError("Storage directory is unavailable",
- "CS Jukebox cannot prepare a writable settings directory.\n\n" + e.Message);
- return applicationDirectory;
- }
-
- try
+ dataDirectoryWritable = CanWriteToDirectory(applicationDirectory);
+ if (!dataDirectoryWritable)
{
- MigrateUserData(applicationDirectory, fallbackDirectory);
+ ReportError(
+ "Application folder is read-only",
+ "CS Jukebox stores properties.json and the kits folder beside CS-Jukebox.exe.\n\n" +
+ "Move CS-Jukebox.exe to a writable folder, such as Documents, then restart the application.");
}
- catch (Exception e)
+ else
{
- ReportError("Existing settings could not be migrated",
- "CS Jukebox will use a new writable settings directory.\n\n" + e.Message);
+ try
+ {
+ ImportLegacyUserData(applicationDirectory);
+ }
+ catch (Exception e)
+ {
+ ReportError(
+ "Existing settings could not be imported",
+ "CS Jukebox will continue with portable storage beside the executable.\n\n" + e.Message);
+ }
}
- return fallbackDirectory;
+ return applicationDirectory;
}
private static bool CanWriteToDirectory(string directory)
@@ -152,22 +160,40 @@ private static bool CanWriteToDirectory(string directory)
}
}
- private static void MigrateUserData(string sourceDirectory, string destinationDirectory)
+ private static void ImportLegacyUserData(string destinationDirectory)
{
- string sourceProperties = Path.Combine(sourceDirectory, PropertiesFilePath);
- string destinationProperties = Path.Combine(destinationDirectory, PropertiesFilePath);
- if (File.Exists(sourceProperties) && !File.Exists(destinationProperties))
- File.Copy(sourceProperties, destinationProperties);
+ string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ if (string.IsNullOrWhiteSpace(localAppData)) return;
+
+ string legacyDirectory = Path.Combine(localAppData, "CS-Jukebox");
+ if (!Directory.Exists(legacyDirectory) ||
+ string.Equals(
+ Path.GetFullPath(legacyDirectory).TrimEnd(Path.DirectorySeparatorChar),
+ Path.GetFullPath(destinationDirectory).TrimEnd(Path.DirectorySeparatorChar),
+ StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
- string sourceKits = Path.Combine(sourceDirectory, MusicKitsPath);
- string destinationKits = Path.Combine(destinationDirectory, MusicKitsPath);
- if (!Directory.Exists(sourceKits)) return;
+ string legacyProperties = Path.Combine(legacyDirectory, PropertiesFilePath);
+ string portableProperties = Path.Combine(destinationDirectory, PropertiesFilePath);
+ string portableKitsDirectory = Path.Combine(destinationDirectory, MusicKitsPath);
- foreach (string sourceFile in Directory.GetFiles(sourceKits, "*.json", SearchOption.TopDirectoryOnly))
+ // Import only into a completely new portable data directory. This
+ // prevents deleted kits from reappearing from legacy storage later.
+ if (File.Exists(portableProperties) || Directory.Exists(portableKitsDirectory)) return;
+
+ if (File.Exists(legacyProperties) && !File.Exists(portableProperties))
+ File.Copy(legacyProperties, portableProperties);
+
+ string legacyKitsDirectory = Path.Combine(legacyDirectory, MusicKitsPath);
+ if (!Directory.Exists(legacyKitsDirectory)) return;
+
+ foreach (string legacyKit in Directory.GetFiles(legacyKitsDirectory, "*.json", SearchOption.TopDirectoryOnly))
{
- string destinationFile = Path.Combine(destinationKits, Path.GetFileName(sourceFile));
- Directory.CreateDirectory(destinationKits);
- if (!File.Exists(destinationFile)) File.Copy(sourceFile, destinationFile);
+ Directory.CreateDirectory(portableKitsDirectory);
+ string portableKit = Path.Combine(portableKitsDirectory, Path.GetFileName(legacyKit));
+ if (!File.Exists(portableKit)) File.Copy(legacyKit, portableKit);
}
}
@@ -179,7 +205,6 @@ public static void CreateConfig()
string configFileName = Properties.ConfigName;
string cfgDir = GameInstallLocator.GetConfigDirectory(Properties.GameDir);
string configPath = Path.Combine(cfgDir, configFileName);
- string configSrc = Path.Combine(appDir ?? GetAppDirectory(), configFileName);
try
{
@@ -189,7 +214,11 @@ public static void CreateConfig()
return;
}
- File.Copy(configSrc, configPath, true);
+ using Stream configStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ConfigResourceName);
+ if (configStream == null)
+ throw new InvalidOperationException("The embedded GSI configuration resource is missing.");
+
+ WriteStreamAtomically(configPath, configStream);
}
catch (Exception e)
{
@@ -199,6 +228,8 @@ public static void CreateConfig()
public static void SaveKits()
{
+ if (!dataDirectoryWritable) return;
+
string dir = KitsDirectory;
var saveErrors = new List();
try
@@ -233,6 +264,8 @@ public static void LoadKits()
{
string dir = KitsDirectory;
MusicKits = new List();
+ if (!dataDirectoryWritable) return;
+
string[] kitFiles;
try
@@ -289,6 +322,7 @@ public static void LoadKits()
//Deletes the json file for a kit but not the kit itself
public static void DeleteKitFile(string kitName)
{
+ if (!dataDirectoryWritable) return;
if (!TryValidateKitFileName(kitName, out _)) return;
string dir = KitsDirectory;
string kitDir = Path.Combine(dir, kitName + ".json");
@@ -375,12 +409,33 @@ private static void WriteAllTextAtomically(string path, string contents)
}
}
+ private static void WriteStreamAtomically(string path, Stream source)
+ {
+ string directory = Path.GetDirectoryName(Path.GetFullPath(path));
+ Directory.CreateDirectory(directory);
+ string temporaryPath = Path.Combine(directory,
+ "." + Path.GetFileName(path) + "." + Guid.NewGuid().ToString("N") + ".tmp");
+
+ try
+ {
+ using (var destination = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+ source.CopyTo(destination);
+ File.Move(temporaryPath, path, true);
+ }
+ finally
+ {
+ try { File.Delete(temporaryPath); } catch { }
+ }
+ }
+
//Inner class for properties parameters
private class PropertiesFile
{
public string GameDir;
public string SelectedKitName;
public int MasterVolume;
+ public bool LimitPreviewToEventDuration;
+ public bool DarkTheme;
}
}
}
diff --git a/CS-Jukebox/ThemeManager.cs b/CS-Jukebox/ThemeManager.cs
new file mode 100644
index 0000000..3bf9605
--- /dev/null
+++ b/CS-Jukebox/ThemeManager.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Drawing;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+
+namespace CS_Jukebox
+{
+ internal static class ThemeManager
+ {
+ private static readonly Color DarkBackground = Color.FromArgb(31, 35, 42);
+ private static readonly Color DarkSurface = Color.FromArgb(42, 47, 56);
+ private static readonly Color DarkControl = Color.FromArgb(52, 58, 69);
+ private static readonly Color DarkInput = Color.FromArgb(36, 41, 49);
+ private static readonly Color DarkText = Color.FromArgb(232, 235, 241);
+ private static readonly Color DarkMutedText = Color.FromArgb(184, 192, 204);
+ private static readonly Color DarkBorder = Color.FromArgb(82, 91, 106);
+ private static readonly Color DarkHover = Color.FromArgb(63, 71, 84);
+ private static readonly Color DarkPressed = Color.FromArgb(72, 99, 142);
+ private static readonly Color DarkAccent = Color.FromArgb(122, 162, 247);
+ private static readonly Color DarkError = Color.FromArgb(255, 125, 135);
+
+ private const int DwmUseImmersiveDarkMode = 20;
+ private const int DwmUseImmersiveDarkModeBefore20H1 = 19;
+
+ [DllImport("dwmapi.dll")]
+ private static extern int DwmSetWindowAttribute(
+ IntPtr windowHandle,
+ int attribute,
+ ref int attributeValue,
+ int attributeSize);
+
+ public static void Apply(Form form)
+ {
+ if (form == null || form.IsDisposed) return;
+
+ ApplyControl(form, Properties.DarkTheme);
+ form.HandleCreated -= Form_HandleCreated;
+ form.HandleCreated += Form_HandleCreated;
+ if (form.IsHandleCreated) ApplyTitleBar(form);
+ form.Invalidate(true);
+ }
+
+ public static void ApplyToOpenForms()
+ {
+ var forms = new Form[Application.OpenForms.Count];
+ for (int index = 0; index < forms.Length; index++)
+ forms[index] = Application.OpenForms[index];
+ foreach (Form form in forms) Apply(form);
+ }
+
+ private static void Form_HandleCreated(object sender, EventArgs e)
+ {
+ if (sender is Form form) ApplyTitleBar(form);
+ }
+
+ private static void ApplyControl(Control control, bool dark)
+ {
+ switch (control)
+ {
+ case Form form:
+ form.BackColor = dark ? DarkBackground : SystemColors.Control;
+ form.ForeColor = dark ? DarkText : SystemColors.ControlText;
+ break;
+
+ case GroupBox groupBox:
+ groupBox.BackColor = dark ? DarkSurface : SystemColors.Control;
+ groupBox.ForeColor = dark ? DarkText : SystemColors.ControlText;
+ break;
+
+ case Button button:
+ ApplyButton(button, dark);
+ break;
+
+ case TextBoxBase textBox:
+ textBox.BackColor = dark ? DarkInput : SystemColors.Window;
+ textBox.ForeColor = dark ? DarkText : SystemColors.WindowText;
+ textBox.BorderStyle = dark ? BorderStyle.FixedSingle : BorderStyle.Fixed3D;
+ break;
+
+ case ComboBox comboBox:
+ comboBox.BackColor = dark ? DarkInput : SystemColors.Window;
+ comboBox.ForeColor = dark ? DarkText : SystemColors.WindowText;
+ comboBox.FlatStyle = dark ? FlatStyle.Flat : FlatStyle.Standard;
+ break;
+
+ case ListBox listBox:
+ listBox.BackColor = dark ? DarkInput : SystemColors.Window;
+ listBox.ForeColor = dark ? DarkText : SystemColors.WindowText;
+ listBox.BorderStyle = dark ? BorderStyle.FixedSingle : BorderStyle.Fixed3D;
+ break;
+
+ case TrackBar trackBar:
+ trackBar.BackColor = dark
+ ? trackBar.Parent?.BackColor ?? DarkBackground
+ : SystemColors.Control;
+ trackBar.ForeColor = dark ? DarkAccent : SystemColors.ControlText;
+ break;
+
+ case CheckBox checkBox:
+ checkBox.BackColor = dark
+ ? checkBox.Parent?.BackColor ?? DarkBackground
+ : SystemColors.Control;
+ checkBox.ForeColor = dark ? DarkText : SystemColors.ControlText;
+ checkBox.UseVisualStyleBackColor = !dark;
+ break;
+
+ case LinkLabel linkLabel:
+ linkLabel.BackColor = dark
+ ? linkLabel.Parent?.BackColor ?? DarkBackground
+ : SystemColors.Control;
+ linkLabel.ForeColor = dark ? DarkAccent : SystemColors.ControlText;
+ linkLabel.LinkColor = dark ? DarkAccent : SystemColors.HotTrack;
+ linkLabel.ActiveLinkColor = dark ? DarkText : Color.Red;
+ break;
+
+ case Label label:
+ label.BackColor = dark
+ ? label.Parent?.BackColor ?? DarkBackground
+ : SystemColors.Control;
+ label.ForeColor = string.Equals(label.Name, "errorLabel", StringComparison.OrdinalIgnoreCase)
+ ? dark ? DarkError : Color.Red
+ : dark ? DarkMutedText : SystemColors.ControlText;
+ break;
+
+ default:
+ if (control is Panel or UserControl)
+ {
+ control.BackColor = dark ? DarkSurface : SystemColors.Control;
+ control.ForeColor = dark ? DarkText : SystemColors.ControlText;
+ }
+ break;
+ }
+
+ foreach (Control child in control.Controls)
+ ApplyControl(child, dark);
+ }
+
+ private static void ApplyButton(Button button, bool dark)
+ {
+ button.UseVisualStyleBackColor = !dark;
+ button.FlatStyle = dark ? FlatStyle.Flat : FlatStyle.Standard;
+ button.BackColor = dark ? DarkControl : SystemColors.Control;
+ button.ForeColor = dark ? DarkText : SystemColors.ControlText;
+
+ if (dark)
+ {
+ button.FlatAppearance.BorderColor = DarkBorder;
+ button.FlatAppearance.BorderSize = 1;
+ button.FlatAppearance.MouseOverBackColor = DarkHover;
+ button.FlatAppearance.MouseDownBackColor = DarkPressed;
+ }
+ else
+ {
+ button.FlatAppearance.BorderColor = Color.Empty;
+ button.FlatAppearance.MouseOverBackColor = Color.Empty;
+ button.FlatAppearance.MouseDownBackColor = Color.Empty;
+ }
+ }
+
+ private static void ApplyTitleBar(Form form)
+ {
+ if (!OperatingSystem.IsWindows() || !form.IsHandleCreated) return;
+
+ int enabled = Properties.DarkTheme ? 1 : 0;
+ try
+ {
+ int result = DwmSetWindowAttribute(
+ form.Handle,
+ DwmUseImmersiveDarkMode,
+ ref enabled,
+ sizeof(int));
+
+ if (result != 0)
+ {
+ DwmSetWindowAttribute(
+ form.Handle,
+ DwmUseImmersiveDarkModeBefore20H1,
+ ref enabled,
+ sizeof(int));
+ }
+ }
+ catch (DllNotFoundException)
+ {
+ // Older Windows versions do not expose DWM dark title bars.
+ }
+ catch (EntryPointNotFoundException)
+ {
+ // Keep the themed client area even when the title-bar API is unavailable.
+ }
+ }
+ }
+}
diff --git a/CSGSI/CSGSI.csproj b/CSGSI/CSGSI.csproj
index be1fd28..01384f0 100644
--- a/CSGSI/CSGSI.csproj
+++ b/CSGSI/CSGSI.csproj
@@ -20,7 +20,8 @@
TRACE
- pdbonly
+ none
+ false
true
@@ -68,4 +69,4 @@
-->
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index 9e907fd..c5bb5ae 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ Download community-created kits or share your own on our official subreddit:
* **Independent Audio Engine:** Powered by [NAudio](https://github.com/naudio/NAudio) for low-latency playback without relying on legacy Windows Media Player components.
* **Smart Steam Discovery:** Automatically scans all connected Steam libraries (`libraryfolders.vdf`) across your drives to locate CS2 and configure the integration file.
* **Intuitive Kit Dashboard:** Easily create, manage, switch, and test custom audio kits using `.mp3` and `.wav` formats.
+* **Comfortable Dark Theme:** Switch the complete interface to a soft blue-graphite palette from the main window; the preference is remembered automatically.
---
## 🚀 Getting Started
@@ -61,13 +62,14 @@ Download community-created kits or share your own on our official subreddit:
1. Download the latest compiled release archive:
👉 **[Download CS-Jukebox V2 (Latest Release)](https://github.com/TYFALY/CS-Jukebox-V2/releases/latest)** *(Windows only)*
-2. Extract the downloaded archive to any folder on your PC.
+2. Place `CS-Jukebox.exe` in any writable folder on your PC.
---
## 📦 Installation & Setup
-1. Launch `CS-Jukebox.exe`.
+1. Launch `CS-Jukebox.exe`. (To avoid problems, place the application in a separate, empty folder.)
+ The application creates `properties.json`, `debug_log.txt`, and the `kits` folder beside the executable. Keep these items together when moving an existing portable installation.
2. The application will automatically scan your Steam libraries and detect your CS2 installation directory.
> [!TIP]
> If automatic detection fails, click **Browse** and manually select your CS2 `game` folder:
@@ -86,17 +88,18 @@ Download community-created kits or share your own on our official subreddit:
> [!NOTE]
> Tracks exceeding the ideal duration will automatically cut off or fade smoothly when the next gameplay event triggers.
-
-| Event / Track | Minimum Length | Ideal Duration | Behavior & Engine Context |
-| :--- | :---: | :---: | :--- |
-| **Main Menu** | Loopable | Ambient / Seamless | Loops continuously while in the main menu or lobby. |
-| **Freeze Time** | 10 sec | 10–15 sec | Plays during the pre-round buy phase (15s timer). |
-| **Round Start** | 5 sec | 5–10 sec | Plays at round start; automatically fades out after ~5s. |
-| **Bomb Planted** | 30 sec | 30 sec | Plays on bomb plant (Premier bomb timer is 40s total). |
-| **10-Sec Warning** | 10 sec | **Exactly 10 sec** | Plays when exactly 10s remain on round or bomb timer. |
-| **MVP Anthem** | **7 sec** | **7 sec** | Plays when you earn round MVP; fades into the next freeze time. |
-| **Round Won / Lost** | **7 sec** | **7 sec** | Fallback round-end track played when you are not the MVP. |
-| **Player Death** | 3 sec | 3–5 sec | Secondary audio cue triggered immediately when you die. |
+> In the Music Selector, enable **Limit preview to event duration** to simulate the fixed preview window below. **Start At** changes the source offset, not the length of that window. The option also applies to previews in **Extra tracks**.
+
+| Event / Track | Minimum Length | Ideal Duration | Limited Preview | Behavior & Engine Context |
+| :--- | :---: | :---: | :---: | :--- |
+| **Main Menu** | Loopable | Ambient / Seamless | Until file ends | Loops continuously while in the main menu or lobby. |
+| **Freeze Time** | 10 sec | 10–15 sec | **15 sec** | Plays during the pre-round buy phase (15s timer). |
+| **Round Start** | 5 sec | 5–10 sec | **5 sec** | Plays at round start; automatically fades out after ~5s. |
+| **Bomb Planted** | 30 sec | 30 sec | **30 sec** | Plays on bomb plant (Premier bomb timer is 40s total). |
+| **10-Sec Warning** | 10 sec | **Exactly 10 sec** | **10 sec** | Plays when exactly 10s remain on round or bomb timer. |
+| **MVP Anthem** | **7 sec** | **7 sec** | **7 sec** | Plays when you earn round MVP; fades into the next freeze time. |
+| **Round Won / Lost** | **7 sec** | **7 sec** | **7 sec** | Fallback round-end track played when you are not the MVP. |
+| **Player Death** | 3 sec | 3–5 sec | **5 sec** | Secondary audio cue triggered immediately when you die. |
### How to Create & Assign Kits
@@ -138,18 +141,20 @@ Make sure you have the following installed:
git clone https://github.com/TYFALY/CS-Jukebox-V2.git
cd CS-Jukebox-V2
-# 2. Build the project in Release configuration
-dotnet build -c Release
+# 2. Publish the compressed, self-contained single-file release
+dotnet publish ./CS-Jukebox/CS-Jukebox.csproj -c Release
```
### Output Location
-After a successful build, the executable will be available at:
+After a successful publish, the only distributed file will be available at:
```text
-bin/Release/net8.0-windows/CS-Jukebox.exe
+CS-Jukebox/bin/Release/net8.0-windows/win-x64/publish/CS-Jukebox.exe
```
+The GSI configuration is embedded in the executable and installed into the CS2 configuration directory automatically. Runtime settings and music kits remain portable beside `CS-Jukebox.exe`.
+
---
## ❓ Troubleshooting
@@ -168,7 +173,7 @@ CS-Jukebox scans your Steam directories via `libraryfolders.vdf`. If automatic d
1. Ensure `CS-Jukebox.exe` is running while playing.
2. Verify that the GSI config file was created at:
- `...\Counter-Strike Global Offensive\game\csgo\cfg\gamestate_integration_csjukebox.cfg`
+ `...\Counter-Strike Global Offensive\game\csgo\cfg\gamestate_integration_jukebox.cfg`
3. Restart Counter-Strike 2 so the game engine loads the integration file.
4. Verify your audio tracks are valid, uncorrupted `.mp3` or `.wav` files.