Skip to content

Commit 8892ab6

Browse files
committed
Add new PNG resources for QuotaBarWindows
- Added anthropic.png to Resources - Added gemini.png to Resources - Added github.png to Resources - Added openai.png to Resources These images are essential for the visual representation of the quota bar feature.
1 parent 35f7153 commit 8892ab6

14 files changed

Lines changed: 454 additions & 337 deletions

QuotaBarWindows/App.xaml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,30 @@
103103
</Setter>
104104
</Style>
105105

106-
<!-- ScrollBar Style (slim, dark) -->
106+
<!-- Light Button Style (for popup footer) -->
107+
<Style x:Key="LightButtonStyle" TargetType="Button">
108+
<Setter Property="Background" Value="Transparent"/>
109+
<Setter Property="BorderThickness" Value="0"/>
110+
<Setter Property="Cursor" Value="Hand"/>
111+
<Setter Property="Padding" Value="8,5"/>
112+
<Setter Property="Template">
113+
<Setter.Value>
114+
<ControlTemplate TargetType="Button">
115+
<Border x:Name="border" Background="{TemplateBinding Background}"
116+
CornerRadius="6" Padding="{TemplateBinding Padding}">
117+
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
118+
</Border>
119+
<ControlTemplate.Triggers>
120+
<Trigger Property="IsMouseOver" Value="True">
121+
<Setter TargetName="border" Property="Background" Value="#E5E5EA"/>
122+
</Trigger>
123+
</ControlTemplate.Triggers>
124+
</ControlTemplate>
125+
</Setter.Value>
126+
</Setter>
127+
</Style>
128+
129+
<!-- ScrollBar Style (slim) -->
107130
<Style TargetType="ScrollBar">
108131
<Setter Property="Width" Value="6"/>
109132
<Setter Property="Background" Value="Transparent"/>

QuotaBarWindows/App.xaml.cs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,20 @@ protected override void OnStartup(StartupEventArgs e)
3232

3333
private System.Drawing.Icon LoadIcon()
3434
{
35-
// Try to load custom icon; fall back to a system icon
36-
var iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "icon.ico");
37-
if (System.IO.File.Exists(iconPath))
38-
return new System.Drawing.Icon(iconPath);
35+
// Load from WPF pack resource (works in single-file publish too)
36+
try
37+
{
38+
var uri = new Uri("pack://application:,,,/Resources/icon.ico");
39+
var info = GetResourceStream(uri);
40+
if (info?.Stream != null)
41+
return new System.Drawing.Icon(info.Stream);
42+
}
43+
catch { }
44+
45+
// Fallback: try alongside the exe
46+
var path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "icon.ico");
47+
if (System.IO.File.Exists(path))
48+
return new System.Drawing.Icon(path);
3949

4050
return System.Drawing.SystemIcons.Application;
4151
}
@@ -84,16 +94,15 @@ private void PositionPopupAboveTray()
8494
if (_popup == null) return;
8595

8696
var screen = SystemParameters.WorkArea;
87-
var popupWidth = 340.0;
88-
var popupHeight = 500.0;
97+
const double popupWidth = 340.0;
8998

90-
double left = screen.Right - popupWidth - 12;
91-
double top = screen.Bottom - popupHeight - 12;
92-
93-
_popup.Left = left;
94-
_popup.Top = top;
9599
_popup.Width = popupWidth;
96-
_popup.Height = popupHeight;
100+
// Let height be auto, then position after measure
101+
_popup.UpdateLayout();
102+
double popupHeight = _popup.ActualHeight > 0 ? _popup.ActualHeight : 480;
103+
104+
_popup.Left = screen.Right - popupWidth - 12;
105+
_popup.Top = screen.Bottom - popupHeight - 12;
97106
}
98107

99108
private void ShowContextMenu()

QuotaBarWindows/Helpers/OAuthHelper.cs

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,9 @@
22
using System.Net;
33
using System.Security.Cryptography;
44
using System.Text;
5-
using System.Web;
65

76
namespace QuotaBarWindows.Helpers;
87

9-
/// <summary>
10-
/// Handles OAuth 2.0 PKCE flow: opens browser, listens for redirect, returns auth code.
11-
/// </summary>
128
public static class OAuthHelper
139
{
1410
public static string GenerateCodeVerifier()
@@ -29,38 +25,50 @@ private static string Base64UrlEncode(byte[] bytes)
2925
public static string BuildAuthUrl(string baseUrl, string clientId, string redirectUri,
3026
string scope, string codeChallenge, string state)
3127
{
32-
var query = HttpUtility.ParseQueryString(string.Empty);
33-
query["response_type"] = "code";
34-
query["client_id"] = clientId;
35-
query["redirect_uri"] = redirectUri;
36-
query["scope"] = scope;
37-
query["code_challenge"] = codeChallenge;
38-
query["code_challenge_method"] = "S256";
39-
query["state"] = state;
40-
return $"{baseUrl}?{query}";
28+
static string Encode(string s) => Uri.EscapeDataString(s);
29+
return $"{baseUrl}?response_type=code&client_id={Encode(clientId)}" +
30+
$"&redirect_uri={Encode(redirectUri)}&scope={Encode(scope)}" +
31+
$"&code_challenge={Encode(codeChallenge)}&code_challenge_method=S256" +
32+
$"&state={Encode(state)}";
4133
}
4234

4335
/// <summary>
44-
/// Opens the browser to the auth URL and waits for the redirect callback.
45-
/// Returns the authorization code, or null if timed out / cancelled.
36+
/// Parses a query string (e.g. "?code=abc&state=xyz") and returns a key->value dictionary.
37+
/// </summary>
38+
public static Dictionary<string, string> ParseQuery(string query)
39+
{
40+
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
41+
var q = query.TrimStart('?');
42+
foreach (var part in q.Split('&', StringSplitOptions.RemoveEmptyEntries))
43+
{
44+
var idx = part.IndexOf('=');
45+
if (idx < 0) continue;
46+
var key = Uri.UnescapeDataString(part[..idx]);
47+
var val = Uri.UnescapeDataString(part[(idx + 1)..]);
48+
result[key] = val;
49+
}
50+
return result;
51+
}
52+
53+
/// <summary>
54+
/// Opens a local HTTP listener, waits for the OAuth redirect callback.
55+
/// Returns the authorization code or null on timeout/cancel.
4656
/// </summary>
4757
public static async Task<string?> WaitForCallbackAsync(string redirectUri, string expectedState,
4858
CancellationToken ct = default)
4959
{
5060
var uri = new Uri(redirectUri);
5161
var prefix = $"http://localhost:{uri.Port}{uri.AbsolutePath}";
52-
if (!prefix.EndsWith('/')) prefix += '/'; // HttpListener needs trailing slash
62+
if (!prefix.EndsWith('/')) prefix += '/';
5363

5464
using var listener = new HttpListener();
5565
listener.Prefixes.Add(prefix);
5666

57-
try
58-
{
59-
listener.Start();
60-
}
67+
try { listener.Start(); }
6168
catch (Exception ex)
6269
{
63-
throw new InvalidOperationException($"Could not start local OAuth listener on {prefix}: {ex.Message}");
70+
throw new InvalidOperationException(
71+
$"Could not start local OAuth listener on {prefix}: {ex.Message}");
6472
}
6573

6674
try
@@ -69,39 +77,32 @@ public static string BuildAuthUrl(string baseUrl, string clientId, string redire
6977
cts.CancelAfter(TimeSpan.FromMinutes(5));
7078

7179
var contextTask = listener.GetContextAsync();
72-
var cancelTask = Task.Delay(Timeout.Infinite, cts.Token);
80+
await Task.WhenAny(contextTask, Task.Delay(Timeout.Infinite, cts.Token));
81+
if (!contextTask.IsCompletedSuccessfully) return null;
7382

74-
var completed = await Task.WhenAny(contextTask, cancelTask);
75-
if (completed == cancelTask) return null;
83+
var context = contextTask.Result;
84+
var query = ParseQuery(context.Request.Url!.Query);
85+
query.TryGetValue("code", out var code);
86+
query.TryGetValue("state", out var state);
7687

77-
var context = await contextTask;
78-
var query = HttpUtility.ParseQueryString(context.Request.Url!.Query);
79-
var code = query["code"];
80-
var state = query["state"];
81-
82-
// Respond to the browser
8388
const string html = """
84-
<html><body style="font-family:system-ui;text-align:center;padding:40px;background:#1C1C1E;color:#fff">
85-
<h2>QuotaBar</h2><p>Authorization complete! You can close this tab.</p>
89+
<html><body style="font-family:system-ui;text-align:center;padding:40px;background:#F5F5F7">
90+
<h2 style="color:#1C1C1E">QuotaBar</h2>
91+
<p style="color:#8E8E93">Authorization complete! You can close this tab.</p>
8692
</body></html>
8793
""";
88-
var responseBytes = Encoding.UTF8.GetBytes(html);
94+
var bytes = Encoding.UTF8.GetBytes(html);
8995
context.Response.ContentType = "text/html";
90-
context.Response.ContentLength64 = responseBytes.Length;
91-
await context.Response.OutputStream.WriteAsync(responseBytes, ct);
96+
context.Response.ContentLength64 = bytes.Length;
97+
await context.Response.OutputStream.WriteAsync(bytes, ct);
9298
context.Response.Close();
9399

94100
if (state != expectedState) return null;
95101
return code;
96102
}
97-
finally
98-
{
99-
listener.Stop();
100-
}
103+
finally { listener.Stop(); }
101104
}
102105

103106
public static void OpenBrowser(string url)
104-
{
105-
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
106-
}
107+
=> Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
107108
}

QuotaBarWindows/QuotaBarWindows.csproj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
<AssemblyName>QuotaBar</AssemblyName>
1111
<RootNamespace>QuotaBarWindows</RootNamespace>
1212
<ApplicationManifest>app.manifest</ApplicationManifest>
13+
<Version>1.0.0</Version>
14+
<FileVersion>1.0.0.0</FileVersion>
15+
<AssemblyVersion>1.0.0.0</AssemblyVersion>
16+
<Company>QuotaBar</Company>
17+
<Product>QuotaBar</Product>
18+
<Description>AI Quota Tracker for Windows</Description>
19+
<Copyright>Copyright © 2025</Copyright>
1320
</PropertyGroup>
1421

1522
<ItemGroup>
@@ -18,6 +25,10 @@
1825

1926
<ItemGroup>
2027
<Resource Include="Resources\icon.ico" />
28+
<Resource Include="Resources\anthropic.png" />
29+
<Resource Include="Resources\openai.png" />
30+
<Resource Include="Resources\github.png" />
31+
<Resource Include="Resources\gemini.png" />
2132
</ItemGroup>
2233

2334
</Project>
2.68 KB
Loading
3.48 KB
Loading
3.94 KB
Loading
3.47 KB
Loading

0 commit comments

Comments
 (0)