From 8cb6602b594ebd56833873a61df77b19e8ad0e1d Mon Sep 17 00:00:00 2001 From: SuperDooper86 Date: Thu, 10 Sep 2026 08:59:09 +0200 Subject: [PATCH] Move ClaudeChecker builds and updates to Gitea --- .gitea/workflows/build.yml | 29 ++ .gitea/workflows/release-windows.yml | 32 ++ .gitea/workflows/release.yml | 29 ++ .github/workflows/release-beta.yml | 81 --- .github/workflows/release.yml | 105 ---- .gitignore | 7 + CONTRIBUTING.md | 2 +- ClaudeChecker/Info.plist | 4 +- ClaudeChecker/UpdateManager.swift | 6 +- README.md | 37 +- RELEASE_NOTES.md | 8 +- scripts/build-macos.sh | 12 + scripts/build-windows.ps1 | 10 + scripts/publish-release.py | 76 +++ version-beta.json | 2 +- version-windows-beta.json | 10 +- version.json | 2 +- windows/App.xaml | 198 +++++++ windows/App.xaml.cs | 151 ++++++ windows/AppSettings.cs | 49 ++ windows/Assets/icon.ico | Bin 0 -> 38587 bytes windows/Assets/icon.png | Bin 0 -> 20505 bytes windows/ClaudeCheckerWindows.csproj | 32 ++ windows/Controls/GaugeControl.cs | 84 +++ windows/Controls/SparklineControl.cs | 60 +++ windows/GlobalUsings.cs | 9 + windows/LoginWindow.xaml | 42 ++ windows/LoginWindow.xaml.cs | 68 +++ windows/Models.cs | 71 +++ windows/PopupWindow.xaml | 295 +++++++++++ windows/PopupWindow.xaml.cs | 750 +++++++++++++++++++++++++++ windows/Properties/Settings.settings | 23 + windows/README.md | 50 ++ windows/RELEASE_NOTES.md | 4 + windows/ThemeManager.cs | 81 +++ windows/UpdateManager.cs | 229 ++++++++ windows/UsageViewModel.cs | 609 ++++++++++++++++++++++ windows/WebViewFetchWindow.cs | 159 ++++++ windows/installer.iss | 41 ++ 39 files changed, 3246 insertions(+), 211 deletions(-) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitea/workflows/release-windows.yml create mode 100644 .gitea/workflows/release.yml delete mode 100644 .github/workflows/release-beta.yml delete mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 scripts/build-macos.sh create mode 100644 scripts/build-windows.ps1 create mode 100644 scripts/publish-release.py create mode 100644 windows/App.xaml create mode 100644 windows/App.xaml.cs create mode 100644 windows/AppSettings.cs create mode 100644 windows/Assets/icon.ico create mode 100644 windows/Assets/icon.png create mode 100644 windows/ClaudeCheckerWindows.csproj create mode 100644 windows/Controls/GaugeControl.cs create mode 100644 windows/Controls/SparklineControl.cs create mode 100644 windows/GlobalUsings.cs create mode 100644 windows/LoginWindow.xaml create mode 100644 windows/LoginWindow.xaml.cs create mode 100644 windows/Models.cs create mode 100644 windows/PopupWindow.xaml create mode 100644 windows/PopupWindow.xaml.cs create mode 100644 windows/Properties/Settings.settings create mode 100644 windows/README.md create mode 100644 windows/RELEASE_NOTES.md create mode 100644 windows/ThemeManager.cs create mode 100644 windows/UpdateManager.cs create mode 100644 windows/UsageViewModel.cs create mode 100644 windows/WebViewFetchWindow.cs create mode 100644 windows/installer.iss diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..0ceca45 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,29 @@ +name: Build +on: + push: + branches: [main] + workflow_dispatch: +jobs: + macos: + runs-on: claudechecker-macos-arm64 + steps: + - uses: actions/checkout@v4 + - name: Build universal macOS app + run: bash scripts/build-macos.sh + - uses: actions/upload-artifact@v3 + with: + name: ClaudeChecker-macOS + path: ClaudeChecker.zip + windows: + runs-on: claudechecker-windows-x64 + defaults: + run: + shell: powershell + steps: + - uses: actions/checkout@v4 + - name: Build Windows installer + run: ./scripts/build-windows.ps1 + - uses: actions/upload-artifact@v3 + with: + name: ClaudeChecker-Windows + path: ClaudeChecker-Installer.exe diff --git a/.gitea/workflows/release-windows.yml b/.gitea/workflows/release-windows.yml new file mode 100644 index 0000000..07d8653 --- /dev/null +++ b/.gitea/workflows/release-windows.yml @@ -0,0 +1,32 @@ +name: Release Windows +on: + workflow_dispatch: + inputs: + version: + description: 'Version, e.g. 0.0.1-beta.64' + required: true + type: string + channel: + description: Release channel + required: true + default: windows-beta + type: choice + options: [windows-beta, windows-stable] +jobs: + release: + if: github.ref == 'refs/heads/main' + runs-on: claudechecker-windows-x64 + defaults: + run: + shell: powershell + env: + VERSION: ${{ inputs.version }} + CHANNEL: ${{ inputs.channel }} + steps: + - uses: actions/checkout@v4 + - name: Build Windows installer + run: ./scripts/build-windows.ps1 -Version $env:VERSION + - name: Publish release and update feed + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python scripts/publish-release.py diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..2ff2767 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release macOS +on: + workflow_dispatch: + inputs: + version: + description: 'Version, e.g. 1.3.3 or 1.3.4-beta.1' + required: true + type: string + channel: + description: Release channel + required: true + default: stable + type: choice + options: [stable, beta] +jobs: + release: + if: github.ref == 'refs/heads/main' + runs-on: claudechecker-macos-arm64 + env: + VERSION: ${{ inputs.version }} + CHANNEL: ${{ inputs.channel }} + steps: + - uses: actions/checkout@v4 + - name: Build universal macOS app + run: bash scripts/build-macos.sh + - name: Publish release and update feed + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python3 scripts/publish-release.py diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml deleted file mode 100644 index c1f8850..0000000 --- a/.github/workflows/release-beta.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Release Beta - -on: - push: - tags: - - 'v*-beta*' - -jobs: - release-beta: - runs-on: macos-latest - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Get version from tag - id: version - run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - - - name: Build - run: | - xcodebuild \ - -project ClaudeChecker.xcodeproj \ - -scheme ClaudeChecker \ - -configuration Release \ - -derivedDataPath build \ - clean build - - - name: Zip app - run: | - ditto -c -k --keepParent \ - build/Build/Products/Release/ClaudeChecker.app \ - ClaudeChecker.zip - - - name: Create GitHub pre-release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create "$GITHUB_REF_NAME" ClaudeChecker.zip \ - --title "$GITHUB_REF_NAME" \ - --prerelease \ - --notes "$(cat RELEASE_NOTES.md 2>/dev/null || echo 'Beta release ${{ steps.version.outputs.version }}')" - - - name: Update version-beta.json - run: | - notes=$(cat RELEASE_NOTES.md 2>/dev/null || echo "Beta release ${{ steps.version.outputs.version }}") - jq -n \ - --arg version "${{ steps.version.outputs.version }}" \ - --arg url "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/ClaudeChecker.zip" \ - --arg notes "$notes" \ - '{version: $version, url: $url, notes: $notes}' > version-beta.json - - - name: Update beta badge in README - run: | - shields_version=$(echo "${{ steps.version.outputs.version }}" | sed 's/-/--/g') - tag="${{ github.ref_name }}" - export BADGE="[![macOS Beta](https://img.shields.io/badge/macOS_Beta-${shields_version}-orange?style=flat)](https://github.com/${{ github.repository }}/releases/tag/${tag}) " - python3 -c " - import os - badge = os.environ['BADGE'] - lines = open('README.md').readlines() - out = [] - for line in lines: - out.append(badge + chr(10) if '' in line else line) - open('README.md', 'w').writelines(out) - " - - - name: Commit version-beta.json and README - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add version-beta.json README.md - git commit -m "Beta release ${{ github.ref_name }}" - for i in 1 2 3 4 5; do - git pull --rebase origin main && git push origin HEAD:main && break - echo "Push attempt $i failed, retrying..."; sleep 3 - done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 4764cb5..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*' - -jobs: - release: - if: ${{ !contains(github.ref_name, '-') }} - runs-on: macos-latest - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Get version from tag - id: version - run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - - - name: Build - run: | - xcodebuild \ - -project ClaudeChecker.xcodeproj \ - -scheme ClaudeChecker \ - -configuration Release \ - -derivedDataPath build \ - clean build - - - name: Zip app - run: | - ditto -c -k --keepParent \ - build/Build/Products/Release/ClaudeChecker.app \ - ClaudeChecker.zip - - - name: Compute SHA256 - id: sha - run: echo "sha256=$(shasum -a 256 ClaudeChecker.zip | awk '{print $1}')" >> $GITHUB_OUTPUT - - - name: Create GitHub release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create "$GITHUB_REF_NAME" ClaudeChecker.zip \ - --title "$GITHUB_REF_NAME" \ - --notes "$(cat RELEASE_NOTES.md 2>/dev/null || echo 'Release ${{ steps.version.outputs.version }}')" - - - name: Update version.json - run: | - notes=$(cat RELEASE_NOTES.md 2>/dev/null || echo "Release ${{ steps.version.outputs.version }}") - jq -n \ - --arg version "${{ steps.version.outputs.version }}" \ - --arg url "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/ClaudeChecker.zip" \ - --arg notes "$notes" \ - '{version: $version, url: $url, notes: $notes}' > version.json - - - name: Update Homebrew cask - run: | - sed -i '' \ - -e 's/version "[^"]*"/version "${{ steps.version.outputs.version }}"/' \ - -e 's/sha256 "[^"]*"/sha256 "${{ steps.sha.outputs.sha256 }}"/' \ - ../homebrew-tap/Casks/claudechecker.rb 2>/dev/null || true - - - name: Update README badge - env: - NEW_VERSION: ${{ steps.version.outputs.version }} - run: | - python3 -c " - import os, re - v = os.environ['NEW_VERSION'] - content = open('README.md').read() - content = re.sub(r'macOS_Stable-[^-]+-orange', 'macOS_Stable-' + v + '-orange', content) - lines = content.splitlines(keepends=True) - out = ['' + chr(10) if '' in line else line for line in lines] - open('README.md', 'w').writelines(out) - " - - - name: Commit version.json and README - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add version.json README.md - git commit -m "Release ${{ github.ref_name }}" - git pull --rebase origin main - git push origin HEAD:main - - - name: Update Homebrew tap - env: - TAP_TOKEN: ${{ secrets.TAP_TOKEN }} - run: | - git clone https://x-access-token:${{ secrets.TAP_TOKEN }}@github.com/${{ github.repository_owner }}/homebrew-tap /tmp/homebrew-tap - sed -i '' \ - -e 's/version "[^"]*"/version "${{ steps.version.outputs.version }}"/' \ - -e 's/sha256 "[^"]*"/sha256 "${{ steps.sha.outputs.sha256 }}"/' \ - /tmp/homebrew-tap/Casks/claudechecker.rb - cd /tmp/homebrew-tap - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add Casks/claudechecker.rb - git commit -m "Update claudechecker to ${{ github.ref_name }}" - git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1919a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/build/ +/publish/ +/ClaudeChecker.zip +/ClaudeChecker-Installer.exe +/windows/bin/ +/windows/obj/ +__pycache__/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bca53a4..a7f8b6f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for your interest in ClaudeChecker! ## Reporting bugs & requesting features -Please use the [issue templates](https://github.com/superdooper86/claudechecker/issues/new/choose) — they keep reports consistent and easy to act on. +Please use the [issue templates](https://git.jamesbone.net/coder/claudechecker/issues/new/choose) — they keep reports consistent and easy to act on. ## Pull requests diff --git a/ClaudeChecker/Info.plist b/ClaudeChecker/Info.plist index 6d4400b..9a3c846 100644 --- a/ClaudeChecker/Info.plist +++ b/ClaudeChecker/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.3-beta.5 + 1.3.3 CFBundleVersion - 81 + 82 LSMinimumSystemVersion 13.0 LSUIElement diff --git a/ClaudeChecker/UpdateManager.swift b/ClaudeChecker/UpdateManager.swift index c2fd7c7..1255f36 100644 --- a/ClaudeChecker/UpdateManager.swift +++ b/ClaudeChecker/UpdateManager.swift @@ -30,8 +30,8 @@ enum UpdateError: LocalizedError { @MainActor class UpdateManager: ObservableObject { - static let versionURL = "https://raw.githubusercontent.com/superdooper86/claudechecker/refs/heads/main/version.json" - static let betaVersionURL = "https://raw.githubusercontent.com/superdooper86/claudechecker/refs/heads/main/version-beta.json" + static let versionURL = "https://git.jamesbone.net/coder/claudechecker/raw/branch/main/version.json" + static let betaVersionURL = "https://git.jamesbone.net/coder/claudechecker/raw/branch/main/version-beta.json" @Published var updateAvailable = false @Published var latestVersion = "" @@ -118,7 +118,7 @@ class UpdateManager: ObservableObject { } private func fetchVersion(from urlString: String) async -> VersionInfo? { - // Add timestamp to bust GitHub's 5-minute CDN cache + // Add timestamp to bust cached update metadata let cacheBusted = urlString + "?t=\(Int(Date().timeIntervalSince1970))" guard let url = URL(string: cacheBusted) else { print("[UpdateManager] Invalid URL: \(urlString)") diff --git a/README.md b/README.md index dfc4228..9990e93 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-ClaudeChecker icon +ClaudeChecker icon # ClaudeChecker @@ -11,9 +11,9 @@ [![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org) [![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
-[![macOS Stable](https://img.shields.io/badge/macOS_Stable-1.3.2-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases) -[![macOS Beta](https://img.shields.io/badge/macOS_Beta-1.3.3--beta.5-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.3.3-beta.5) -[![Win Beta](https://img.shields.io/badge/Win_Beta-0.0.1--beta.63-blue?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/win-v0.0.1-beta.63) +[![macOS Stable](https://img.shields.io/badge/macOS_Stable-1.3.2-orange?style=flat)](https://git.jamesbone.net/coder/claudechecker/releases) +[![macOS Beta](https://img.shields.io/badge/macOS_Beta-1.3.3--beta.5-orange?style=flat)](https://git.jamesbone.net/coder/claudechecker/releases/tag/v1.3.3-beta.5) +[![Win Beta](https://img.shields.io/badge/Win_Beta-0.0.1--beta.63-blue?style=flat)](https://git.jamesbone.net/coder/claudechecker/releases/tag/win-v0.0.1-beta.63)
@@ -130,7 +130,7 @@ brew install --cask claudechecker ``` ### Download -1. Go to [Releases](https://github.com/superdooper86/claudechecker/releases) +1. Go to [Releases](https://git.jamesbone.net/coder/claudechecker/releases) 2. Download `ClaudeChecker.zip` from the latest release 3. Unzip and drag `ClaudeChecker.app` to `/Applications` 4. Open from Applications — it will appear in your menubar @@ -166,7 +166,7 @@ ClaudeChecker reads from the following `claude.ai` API endpoints (authenticated ## Privacy -- No data leaves your machine except to `claude.ai` (your own session) and `raw.githubusercontent.com` (version check) +- No data leaves your machine except to `claude.ai` (your own session) and `git.jamesbone.net` (version check) - Session stored in macOS `WKWebsiteDataStore.default()` — same as Safari - No analytics, no tracking, no ads @@ -181,3 +181,28 @@ ClaudeChecker reads from the following `claude.ai` API endpoints (authenticated
ClaudeChecker is not affiliated with or endorsed by Anthropic.
+ +## Building and releasing + +The canonical repository is https://git.jamesbone.net/coder/claudechecker. +Both macOS and Windows source live on `main`; the original Windows feature +branch and all historical tags/releases are retained. + +Gitea **Build** validates every push to `main` on repository-scoped host runners: +`claudechecker-macos-arm64` (Mac mini, Xcode) and `claudechecker-windows-x64` +(Windows host, .NET 8, Inno Setup). Each run uploads its app/installer artifact. + +Use **Release macOS** or **Release Windows**, select `main`, and supply a version +and channel. Stable versions use `X.Y.Z`; beta versions use `X.Y.Z-beta.N`. +The workflow builds that main revision, creates the release and uploads the asset +before advancing the corresponding `version*.json` feed on main. Releases are +never overwritten. macOS builds retain the existing ad-hoc signing model; this +project does not currently perform Developer ID signing or notarization. + +Existing installs still check the old GitHub `version*.json` URLs. Those files +are kept as a migration bridge advertising the first Gitea-hosted update. Keep +the GitHub repository accessible for those clients; subsequent updates use Gitea. +GitHub workflows are disabled after the Gitea builds are verified. + +Local build commands are `bash scripts/build-macos.sh` and, on Windows, +`./scripts/build-windows.ps1` with `INNO_SETUP_COMPILER` pointing to `ISCC.exe`. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c0da116..cfb8136 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,8 +1,4 @@ ## What's new in v1.3.3 -### Diagnostics improvements -- Org IDs are now redacted when copying diagnostics text (privacy) -- Removed stale Cookie Store section (always showed 0) -- All fields (Org ID, lastActiveOrg, Last error, Time) now included in copied output -- Last error now shows "(none)" instead of blank when there are no errors -- Privacy note updated to reflect what is and isn't redacted +- Updates and release downloads now come from git.jamesbone.net. +- macOS and Windows builds are managed by Gitea Actions from main. diff --git a/scripts/build-macos.sh b/scripts/build-macos.sh new file mode 100644 index 0000000..97a368b --- /dev/null +++ b/scripts/build-macos.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail +if [[ -n "${VERSION:-}" ]]; then + [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-beta\.[0-9]+)?$ ]] || exit 2 + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" ClaudeChecker/Info.plist +fi +xcodebuild -project ClaudeChecker.xcodeproj -scheme ClaudeChecker \ + -configuration Release -derivedDataPath build \ + ARCHS="arm64 x86_64" ONLY_ACTIVE_ARCH=NO CODE_SIGN_IDENTITY=- clean build +codesign --verify --deep --strict build/Build/Products/Release/ClaudeChecker.app +lipo -verify_arch arm64 x86_64 build/Build/Products/Release/ClaudeChecker.app/Contents/MacOS/ClaudeChecker +ditto -c -k --keepParent build/Build/Products/Release/ClaudeChecker.app ClaudeChecker.zip diff --git a/scripts/build-windows.ps1 b/scripts/build-windows.ps1 new file mode 100644 index 0000000..4205efd --- /dev/null +++ b/scripts/build-windows.ps1 @@ -0,0 +1,10 @@ +param([string]$Version = '0.0.1-beta.64') +$ErrorActionPreference = 'Stop' +if ($Version -notmatch '^\d+\.\d+\.\d+(-beta\.\d+)?$') { throw 'Invalid version' } +dotnet publish windows/ClaudeCheckerWindows.csproj -c Release -r win-x64 --self-contained true "-p:Version=$Version" -o publish +if ($LASTEXITCODE -ne 0) { throw 'dotnet publish failed' } +Copy-Item -Recurse -Force windows/Assets publish/Assets +& $env:INNO_SETUP_COMPILER "/DAppVersion=$Version" windows/installer.iss +if ($LASTEXITCODE -ne 0) { throw 'Inno Setup failed' } +if (!(Test-Path ClaudeChecker-Installer.exe)) { throw 'Installer missing' } +Get-FileHash ClaudeChecker-Installer.exe -Algorithm SHA256 diff --git a/scripts/publish-release.py b/scripts/publish-release.py new file mode 100644 index 0000000..ba57983 --- /dev/null +++ b/scripts/publish-release.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Publish a completed main build, then advance its update channel.""" +import base64 +import json +import os +from pathlib import Path +import re +import subprocess +import urllib.error +import urllib.parse +import urllib.request + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, *args, **kwargs): + return None + + +def main(): + version = os.environ['VERSION'] + channel = os.environ['CHANNEL'] + if channel not in ('stable', 'beta', 'windows-beta', 'windows-stable'): + raise ValueError('Unknown release channel') + if not re.fullmatch(r'\d+\.\d+\.\d+(-beta\.\d+)?', version): + raise ValueError('Invalid release version') + if ('beta' in channel) != ('-beta.' in version): + raise ValueError('Version must match release channel') + if os.environ.get('GITHUB_REF') != 'refs/heads/main': + raise ValueError('Releases must build from main') + revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip() + windows = channel.startswith('windows-') + tag = ('win-v' if windows else 'v') + version + artifact = Path('ClaudeChecker-Installer.exe' if windows else 'ClaudeChecker.zip') + notes = Path('windows/RELEASE_NOTES.md' if windows else 'RELEASE_NOTES.md').read_text() + feed = {'stable': 'version.json', 'beta': 'version-beta.json', + 'windows-beta': 'version-windows-beta.json', 'windows-stable': 'version-windows.json'}[channel] + server = os.environ['GITHUB_SERVER_URL'].rstrip('/') + repo = os.environ['GITHUB_REPOSITORY'] + opener = urllib.request.build_opener(NoRedirect) + + def api(path, method='GET', data=None, binary=False): + body = data if binary else (json.dumps(data).encode() if data is not None else None) + request = urllib.request.Request(f'{server}/api/v1/repos/{repo}{path}', data=body, method=method, + headers={'Authorization': 'token ' + os.environ['GITEA_TOKEN'], + 'Content-Type': 'application/octet-stream' if binary else 'application/json'}) + with opener.open(request, timeout=600) as response: + return json.load(response) + + # Never replace an existing release or advertise a partially uploaded asset. + release = api('/releases', 'POST', {'tag_name': tag, 'target_commitish': revision, + 'name': tag, 'body': notes + '\n\nBuilt from main: ' + revision, + 'draft': True, 'prerelease': 'beta' in channel}) + asset = api(f"/releases/{release['id']}/assets?name={urllib.parse.quote(artifact.name)}", + 'POST', artifact.read_bytes(), binary=True) + if asset['size'] != artifact.stat().st_size: + raise ValueError('Uploaded asset size mismatch') + api(f"/releases/{release['id']}", 'PATCH', {'draft': False}) + content = json.dumps({'version': version, + 'url': f'https://git.jamesbone.net/{repo}/releases/download/{tag}/{artifact.name}', + 'notes': notes}, indent=2) + '\n' + try: + current = api(f'/contents/{feed}?ref=main') + except urllib.error.HTTPError as error: + if error.code != 404: + raise + current = None + payload = {'branch': 'main', 'message': f'Update {channel} feed to {tag}', + 'content': base64.b64encode(content.encode()).decode()} + if current: + payload['sha'] = current['sha'] + api(f'/contents/{feed}', 'PUT' if current else 'POST', payload) + print(f'Published {tag} and {feed} from {revision}') + + +if __name__ == '__main__': + main() diff --git a/version-beta.json b/version-beta.json index ff24bbf..be8d04d 100644 --- a/version-beta.json +++ b/version-beta.json @@ -1,5 +1,5 @@ { "version": "1.3.3-beta.5", - "url": "https://github.com/superdooper86/claudechecker/releases/download/v1.3.3-beta.5/ClaudeChecker.zip", + "url": "https://git.jamesbone.net/coder/claudechecker/releases/download/v1.3.3-beta.5/ClaudeChecker.zip", "notes": "## What's new in v1.3.2\n\n### Reliability\n- Data now loads automatically on every app launch without requiring manual re-authentication. The background WebView is anchored in a hidden window so macOS no longer throttles its JavaScript execution." } diff --git a/version-windows-beta.json b/version-windows-beta.json index e31edd7..36d06fe 100644 --- a/version-windows-beta.json +++ b/version-windows-beta.json @@ -1,5 +1,5 @@ -{ - "version": "0.0.1-beta.63", - "url": "https://github.com/superdooper86/claudechecker/releases/download/win-v0.0.1-beta.63/ClaudeChecker-Installer.exe", - "notes": "## What's new in beta.62\r\n\r\n### Installer\r\n- App now ships as a proper Windows installer (no more zip extract)\r\n- Installs to user AppData — no admin rights required\r\n- Auto-update now downloads and silently runs the new installer, then relaunches\r\n- Desktop shortcut creation is optional during install\r\n\r\n### Bug fixes\r\n- Fixed app crash on launch (startup DllNotFoundException caused by single-file publish; all DLLs now installed alongside the exe)\r\n- Fixed clicking Copy in Diagnostics crashing the app (CLIPBRD_E_CANT_OPEN)\r\n- Fixed Sign In — Done button now always clickable; session detection no longer depends on specific cookie names that may have changed\r\n\r\n### Diagnostics\r\n- Org IDs are now redacted when copying diagnostics text\r\n- Removed stale Cookie Store section (always showed 0)\r\n- Fixed missing fields in copied diagnostics output\r\n- Privacy note added below diagnostics header" -} +{ + "version": "0.0.1-beta.63", + "url": "https://git.jamesbone.net/coder/claudechecker/releases/download/win-v0.0.1-beta.63/ClaudeChecker-Installer.exe", + "notes": "## What's new in beta.62\r\n\r\n### Installer\r\n- App now ships as a proper Windows installer (no more zip extract)\r\n- Installs to user AppData — no admin rights required\r\n- Auto-update now downloads and silently runs the new installer, then relaunches\r\n- Desktop shortcut creation is optional during install\r\n\r\n### Bug fixes\r\n- Fixed app crash on launch (startup DllNotFoundException caused by single-file publish; all DLLs now installed alongside the exe)\r\n- Fixed clicking Copy in Diagnostics crashing the app (CLIPBRD_E_CANT_OPEN)\r\n- Fixed Sign In — Done button now always clickable; session detection no longer depends on specific cookie names that may have changed\r\n\r\n### Diagnostics\r\n- Org IDs are now redacted when copying diagnostics text\r\n- Removed stale Cookie Store section (always showed 0)\r\n- Fixed missing fields in copied diagnostics output\r\n- Privacy note added below diagnostics header" +} diff --git a/version.json b/version.json index 03110f0..dc0fa85 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { "version": "1.3.2", - "url": "https://github.com/superdooper86/claudechecker/releases/download/v1.3.2/ClaudeChecker.zip", + "url": "https://git.jamesbone.net/coder/claudechecker/releases/download/v1.3.2/ClaudeChecker.zip", "notes": "## What's new in v1.3.2\n\n### Reliability\n- Data now loads automatically on every app launch without requiring manual re-authentication. The background WebView is anchored in a hidden window so macOS no longer throttles its JavaScript execution." } diff --git a/windows/App.xaml b/windows/App.xaml new file mode 100644 index 0000000..8d9107c --- /dev/null +++ b/windows/App.xaml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/windows/App.xaml.cs b/windows/App.xaml.cs new file mode 100644 index 0000000..2fc1f22 --- /dev/null +++ b/windows/App.xaml.cs @@ -0,0 +1,151 @@ +using System; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using Forms = System.Windows.Forms; + +namespace ClaudeCheckerWindows; + +public partial class App : Application +{ + public static UsageViewModel ViewModel { get; } = new(); + public static UpdateManager Updater { get; } = new(); + // Persistent hidden WebView2 — shares the same user data folder as LoginWindow + // so its cookies (including cf_clearance) are always live. Used on every refresh + // for endpoints that need a real browser session (overage, prepaid). + public static WebViewFetchWindow BackgroundBrowser { get; } = new(); + + private Forms.NotifyIcon? _tray; + private PopupWindow? _popup; + private DispatcherTimer? _timer; + + protected override void OnStartup(StartupEventArgs e) + { + var logPath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClaudeChecker", "startup_crash.txt"); + + AppDomain.CurrentDomain.UnhandledException += (_, ex) => + System.IO.File.WriteAllText(logPath, ex.ExceptionObject?.ToString() ?? "unknown"); + + DispatcherUnhandledException += (_, ex) => + { + System.IO.File.WriteAllText(logPath, ex.Exception?.ToString() ?? "unknown"); + ex.Handled = false; + }; + + base.OnStartup(e); + ThemeManager.Initialize(); + SetupTray(); + ShowPopup(); + ScheduleTimer(ViewModel.RefreshInterval); + + // Show background browser window on the UI thread before Task.Run + BackgroundBrowser.Show(); + + _ = Task.Run(async () => + { + await ViewModel.LoadFromCacheAsync(); + // Initialize the persistent background browser (navigates to claude.ai once) + await Application.Current.Dispatcher.InvokeAsync( + () => BackgroundBrowser.InitAsync()).Task.Unwrap(); + await ViewModel.RefreshAsync(); + await Updater.CheckForUpdatesAsync(); + }); + } + + private void SetupTray() + { + _tray = new Forms.NotifyIcon + { + Text = "ClaudeChecker", + Visible = true, + Icon = LoadIcon(), + }; + + _tray.MouseClick += (_, e) => + { + if (e.Button == Forms.MouseButtons.Left) + Dispatcher.InvokeAsync(TogglePopup); + }; + + _tray.ContextMenuStrip = BuildContextMenu(); + + ViewModel.PropertyChanged += (_, _) => UpdateTrayText(); + Updater.PropertyChanged += (_, _) => UpdateTrayText(); + } + + private static System.Drawing.Icon LoadIcon() + { + try { return new System.Drawing.Icon("Assets/icon.ico"); } + catch { return System.Drawing.SystemIcons.Application; } + } + + private Forms.ContextMenuStrip BuildContextMenu() + { + var menu = new Forms.ContextMenuStrip(); + menu.Items.Add("Show ClaudeChecker", null, (_, _) => Dispatcher.InvokeAsync(ShowPopup)); + menu.Items.Add(new Forms.ToolStripSeparator()); + menu.Items.Add("Quit", null, (_, _) => Dispatcher.InvokeAsync(Quit)); + return menu; + } + + private void TogglePopup() + { + if (_popup == null || !_popup.IsVisible) + ShowPopup(); + else + _popup.Hide(); + } + + private void ShowPopup() + { + if (_popup == null) + { + _popup = new PopupWindow(); + } + _popup.Show(); + _popup.Activate(); + } + + public void ScheduleTimer(int seconds) + { + _timer?.Stop(); + _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(seconds) }; + _timer.Tick += async (_, _) => + { + await ViewModel.RefreshAsync(); + await Updater.CheckForUpdatesAsync(); + }; + _timer.Start(); + } + + private void UpdateTrayText() + { + if (_tray == null) return; + var limits = ViewModel.Limits; + if (ViewModel.ShowInTaskbar && limits.Count >= 2) + { + var fh = limits.Find(l => l.Window == WindowKind.FiveHour); + var sd = limits.Find(l => l.Window == WindowKind.SevenDay); + if (fh != null && sd != null && fh.IsLive) + { + _tray.Text = $"ClaudeChecker {(int)fh.UsedPercent}% {(int)sd.UsedPercent}%"; + return; + } + } + _tray.Text = "ClaudeChecker"; + } + + private void Quit() + { + _tray?.Dispose(); + Shutdown(); + } + + protected override void OnExit(ExitEventArgs e) + { + _tray?.Dispose(); + base.OnExit(e); + } +} diff --git a/windows/AppSettings.cs b/windows/AppSettings.cs new file mode 100644 index 0000000..bbe4d88 --- /dev/null +++ b/windows/AppSettings.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace ClaudeCheckerWindows; + +public class AppSettings +{ + // Auth signal — non-empty means the user has completed sign-in at least once. + // The WebView2 user-data folder holds the live session; this is only for startup. + public string CookieStore { get; set; } = ""; + + // Accumulated sparkline data — built up over multiple refreshes. + public string BurnHistory { get; set; } = ""; + + // User preferences + public int RefreshInterval { get; set; } = 120; + public bool ShowInTaskbar { get; set; } = true; + public bool BetaChannel { get; set; } = false; + + private static readonly string FilePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClaudeChecker", "settings.json"); + + private static AppSettings? _instance; + public static AppSettings Default => _instance ??= Load(); + + private static AppSettings Load() + { + try + { + if (File.Exists(FilePath)) + return JsonSerializer.Deserialize(File.ReadAllText(FilePath)) ?? new(); + } + catch { } + return new(); + } + + public void Save() + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(FilePath)!); + File.WriteAllText(FilePath, JsonSerializer.Serialize(this, + new JsonSerializerOptions { WriteIndented = true })); + } + catch { } + } +} diff --git a/windows/Assets/icon.ico b/windows/Assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..dab4b7cfd76e67f1431d47381fa5a2cccaa17a75 GIT binary patch literal 38587 zcmagFb8sfn_67RIU+jr(dty5i+qP}nHYT=h+qNgp#CG1?d*A!-RsBv?SNE>n2UWf5 z?6r390|0;kAOT27fPak;a19ConEvO9@PBp`BmkfS1^_TJ{?Cqp0RZ-(0RRDk|Jgqf z0RRg$0Knh>e>MpT0O0rU`d=diV95gj1!4d|guJXcJPh`~-T-(>2@%DA_y5d@1pGHX zfZ+by0001OkrWYBcF(p+k}+2iM;rAsGu;v2`K7D76i%?mqDqwtxh3%-uo=<>;b3xW zRH&}q-dsuFaXJw~+;%D}h%DM4f)msxA6*=>;Hp+ul#a^K%{+^IkkjcuUtv>?E)uoio@Icc<(_ubo-CL)5&m&GRx}fCri|opSSwcnEwys5Lbp$;_fR!|-B+ ztRbWwor<^quA??qfnaO+A<1Ck^Y6GOETuwZ)pC);1>AIfDNpYsiMx1yX-|WtPafLA zDnp`lbi?Q7efo~d3+=G+w8xw4g&_Jb>78Ctk4s4HRMF+2_gEogmLoRPQN*|e18K*)zp3_X%G z3AL>$$QSGW9anA$vvy#@xg8C>y{K0)F{_gJ=-%N9rSLoeAYV;YiLTQkT}Q% zs%K>B4at#?Jj_n4WT%tNP3$_p)d3n8F*_=t7Js|A$H-0^v!YVb1FK7!iZ8m)U7>QM zS5=pf0WIV8(cmJq=81Mz$QG8MI$M*yM&!_10@!z?0u59J(F^weA}b&mx*rztiy_AOj>tWkqU)^aKBY;7<6@ zP5589J9&CO005AR|AYImnw7P(DB73l@lNL^-R9+!V0p6yOuwc;eUBwLbOCWhM0rU} zZCM`wIg4eWf*)|+Phcnyeni|GNm!)pV-HHOE|Az#Q&6-*KGH{$&c>$oz2|u*on><+ z@oJ*-T+-If%k9|v;32FPGebL?lgW{Urx=Cb$K!rU@#=&Pc#|$ zr~(2joVfDmpJ}gmF{bs?G%&<%_b8J{cKDe|l$ziJDN5v@_a%B}&^RT2W(O&dc$yB_xGgH<8&Lt()R*pIbUG*GQ_&FVW7%hN;Z$9Rt4 z0N$D2hh=2Q96*6xpv_EtR#Bs!n?4&>Fq5>N`XbjS2@I+OCe^`1FOh4PW3H`-WIGPN zh*&E6r!3X;6~Zuzr3c#p($CrwnP+Cer2x!WVbbJz;nW9dKoDEcKHi=BW~>;4JrTzN za8R7j8ul;bS zQon6a+x!#Mt-tR?sT)_}o^Ft)1n^6?y)j@gXoE6PgJt}?x9tarx|hbD2DRLgr?$L5 zwXW5;=Nr9nvh#h@zW~S#My%1&drkcaVe-yOY^GcH;9Kh86mIPLd9ScTUVdTHLubk{ zq0n1*eM;)ldmlKAHj->osVje4>2UsyLNirG<&R_$qmN1~0Pcj>Rui=DQW@`eX_)^9;w^m8seX{@eu3^W6n>1Y2ni;ju5(i!jPdzqk?Z#QN6U^&?fOHGS?f`Dau3@tw0sFysEX8DQA8bDqCDun1`77AiQp)Ve$ps5r_ zquL$QQ{uz%@aIz^+d7Pbod6I1n(+Hvv<{|3kW&)RLl~6-S$OA2Wh3FMq#H?N-H1~P zN_PK9m%#Bv<%|W>k{8Zv53P&LB>jHP(!Dfw?}lITm_Vk{7nU zm_vgrr`r7M&e{1Ei*8lE?l*&}j%%tWn;P-al~(;%FvbQa*^{|3gE(hgQCSZ%Lb%V^ zMLkQ7oj{8^l0m=PZ2w#spE}2U@{4(Pz0hcJkkOG_#1*0~AL113!MPM>9n^(xGTgw{ zI!x^}Q|Ov%Bv7B}cO=VC$yMjE2QkX>CbQcw-R;y61{`fQakW(>JB9J^Rn%uC;XfrI z=4v=^nNR1*bJViN2ft~Zy|v@!5TTe+;&&9VKJQEI_8B_i0z6d0ADxL-7Rz5zX>;QO zg5DoBD1;Z)Ya?*J9+5WKt5@Bis8x-+`{w#GHp#m&c4$RpoAN#v-I$(3;5*s`q|oTv z;B&b7#ZD!ZDkh4FZpskOF+yb(Ea4~J3C2jH`(wo7wc3%CFlZMk4weFRFkfmEaNKJK zo*49!_08JQi+N+`8H;k3zMbwOV~s;M5>&+_sU)TP9fWTjHxnYSEX^7-2E_C6{fNde zpPFC@;e%KoDQE|l)>sJ_qEqX(O``LsubDzj=FB-fggPWB z&dA{gh^OlJAHH#Czd_+I`tIx@A%8B*iSS(nxw1T>5GP-mI zPhR_B?2KEeQUw>tWh7t&AD9P@LW)QjOnPBT@Vw*1^}QYjUbH@hUjHp?`gFzn{QKI^ z?1Tjzh9TSor_ai4L_wUPl*5R~m=W=Q+k4UF z4&y-t-KJx@#td1E+f?@)(jPs47z%3(7RUowfDg5TRR3+p7PxxcL5JFucJr&vz|0wa zNQ~iy254@RPFR;4WB`Rha6}!l*nm{}S!hd-)dnIst&wyfB%@d=KT8Nx5F?7}fb^Qi zLBS-s)L9P{<*y0`(v>jkNES-}Vor>tcEu|?z926$(e(_(-@(ZLd6%;&^IATp$nxF< zc{qK9cG){9Gnr1MMj1h`Y=ilCtv_Lwu@{sH;r1-2I#u~Kq828{BRNF25hY=Amiby< zN^p~s{o)xM2RVSK51!i8-`Tp)<&i@%H0wv(5eOzh3-kI2HZvcrJ?JGVs(vce?+MEn zs~J&!Z#-|61O1S~Iw>$ucP@P4juT)a6yo06PEPpsW{mNPZ<#a7z+W(S-a%F4F&IYr zyFOl$QT?F@kFvH?m|)A^M1lh@1jT;=bCJr&26BKB&*#Tb^7<>24_&9j%``m6LL2f~ z5EOHyIzHvylKf4V$Gf&l+cH+RI>FSbgtCqciI`qH(!gDxB-#Aq6%I#7H}dUDoW2`D zU{W{;^JF_m*gjo=hH6A2Q!_A^9qdSnejeZPf_39(mH7MQxBucYbRlc{cn)Z{LOLkg z$wEldh4^rGZQ;`~SzRFV5cktT-!H5A&nGKvRx*a+?pPms$R6R2_*k%`HS9c~C^FwS zadgiydGJVXlyep0wBI`kLhlm~TpSdVn4lC~1>gWTF;NK^-TO#^$#%`PHV*z&^T~o> zXW<`B5VTJN!a~c-r-Z~e9Jo$hM1vAzZ_^gx+xL)XeSEY*xg&vgsiiCQ>(kL6MNf^5 z?E`o2_P12vD_Gh4zA$BFR$xtu7;Ehto+2I{t5Z33gE>`E8R%eT)s{WmaAo^tOBDGy7CvWX%keKKF^4zm#1g--_lnAB4I z076rA+91aqB8qJX?4y*QW55W^C!V)oLz=FF_Xs$*ax?hsa>GSuS@C!NoIpusu4BSS@Gspp>^JaQC={x^bflc#d6=r zTwJRKX}VG6h&6(*hZ<|W2E03+*Yu=(8#VCI20|lV4*G+OH{c}U((UR-q*WADd4HLW z!e`P;YY}X1$D~;$C_a=Y6%i%LSOq9nU{hp`&Dp8Qz_-4P{Evh$pHmGN{+PtOV#`eW zRiLK)!U%+ce8haay3P06B$rqfJQ`LrS=BiYCN-7j*9RW}uF8m3yfYelxmkvCB;SnB z#iD7iijKEM+^DUZpD3u4B3uOQ;mUHw^u0DRI?Q+9#FdToS7+6^O~Y{L;4#WwoJ(1R zW^!%C{zhyiQDlcOEkcQ;PD$98TBjaj-KE0f1yLmoj-6&OR;TE$>y!QU!&M2|eL_6{ znERJJlh2j^J5;NU*yM(|g3E0RX78S?1XaD&@*u~(?bnLTEy69-{5UOjnJ=SJS-9guUCT5f;y5+**^sA$UZW5f~Nynh(raQifOVa z=bqYV_}|8W3QZ%7o$CJ9?yeuxa`dybME!H^26!X1>cJaXKdf$KCa*%JMqGNaq7&z& zVqlfRkRw{VECm$CDf8HsF-2N-HgAovh*f~T69l6HKoR+$HVBD2Ip!MAm~e53Q>2mA z`VtW>C%GcSQhUkhgJNhDO_!JpaSBz-KAXIqW z)}(Kn-3NFapG@$4v#RBpk`W{_TFr*P@2IGa!}`osR8AWZaOs;pJmW^wa#&CcU$3S5VZa~RT;8W*!67lQheAp=r)R4~ z8oEWpl&{vUfs*#MaunjTom%=YQjIia1LS#;0>7jpuTKv%>ZrvBuPlT15zjsx4SDM( zD*t^^{6z82c&?=ycu2T>5!is1$G9^WPy@0liV!4?Sz34u1io+AnqNA9ohl99KFYN2 zaMYKZ>)sFQ4w%wOMha}lYYiX<*LLe5nRiMvC_>jrrWcTcYmEhr(Ryf@(h+Ed!7F>3 zfv7)riv9lDdXj*;Fz%Vp~6?FTh|# zt6U|_^M_ev#2IJGRZVmH=nDNFNuFx?*N}7TIF?7Vx!o)h^Ty#cPrIyi;#WH#b#?YC@?&SplfzYlV*#d%YxW8?B!l)Tvd zR&YQa68V4_CNzz~%E{0-#`i@)_JrPmU+XJ_UAGi$_;YCE|3O3r{`m#|OGGOy9CiQz zgzf(k(Mv5@XO-oQuP45fEjL?Rpd=8QH6D0}g0&Q@46apeGG|dQDvtNpT!)rhCnuv| zMtYPjNLr1%u_QLibt85ovaqlXCP+O+69Pm=K~TMAObO+V7}}%v>YeAQ>YM*LR83w& zD6jr|LbLs({rUO&`!ko05kMuNKMX^pTH}1`{>;wjmoaCbbi@IRCobcbOs$B-MNf<( zpNdu>DpjwS=9txRd%jiLa*=OkISdn#LX)CzUdrbNQ;r^QNcK-QnB?}#QZ{cnEbHTJYZFtecEEM?@K@=fLY|1L888)K)si*n9)$pin4aTg0xlp`O{dZ+OD^)i) zE6s|RJfuqv6_jZ)Ft8yd&>u+%uCFaB0PT~iIM^QrNpo&aFv&H!v7=I4&v&q6npY!w z-Av?3{Au9Ij73lQ-G9)@_;Y%KZc0)0Y*`MJEcLe@BN4@a#k zzUUepU#N$wI34rXW(g7_BTP6e`{_K~sKTTC3`RJ###ACazW?*phvHi3OA72xgBYwk z@qkb|5XXSD%2^jgs79+JnG?j$QKfILR2=JL*knH}=>LOGEy;zm!>Z5f%xck>>sFJs z;e^1BK{C?1B!&KI=B4_o?rl^n4K(dS>>tsy^!O};BpzhVT^>4FKTq#`W1_R!YQ{^l@zVxaNoOJiDflOjj6LgauCrVRh%EU^vc5tCUd&4nj)n!1M1ifYMb0 zvg^w3cOwHh6CuZl0u=TDT0=$RSm!1vQ#pz^OMaoMe3>74?vrHNtP8gC+QeWpGG}K}IF|rHMe*DWO}8-q67I)dAq~!!GKzxv<_e z$Pl(wym^&iMIpK=4@`!JWLTh!AJXqG(3fB2B=S7S`*#+=AF{768>IjNb){$-b}n(OJE~%>Dhzh#J?XOHZ7fP8#G|ZeV38 zYMo3fTE0b1J$FBBn^;10A!rdK9d3Z15X7D7tZ;t{EHbv*gqvnb#PNh);q8%GJfA8Q zNph9~EL$L4JW8F7`Zzeuk1tO0?I)QR;#Y5~jV=EYI-I%N8S?bY>G;gU6hkNBtJ!MJ z>_2zf1WVUZXeM_*vYlaKO#~F zEy3%(?a)x|R+M}~C4S5%d;U`f0cZqe@Tl7iq+&KKn1u7CJDjI4a*6Nz`&g$jd9K~- z9^MjPFHCtT^=SaE?&ZFV)_@9@$zbH8;3UmJuvTvb^y>DMTBjBNK)=d2u(J|9kaQn+ zB*lyuLUON=aYUxCD$H+U4{x-8zgcdtP&{qWk`so6Cscphhq+UHJH5_v*aC;@LQ0!AtsLolIvn zQ1R?O^tqL~@1z|q-=V@Au+TlZW9Pr{T&oao!frC&zo1d=Ua0^2Ry9K}TlofxEaRK= z`T5^;yZ_&x`PC+iHP zjR~mnK9e{n#b9eWkAy4mFLp*~E!-4bwDh{d{=KEF_&!mPD32-Y$c+dt8I&9moRs&T zWR~tY(mf4@D(}JnZBSCEtCZy_FY_{k~bh>J7a=zPJ{%)cZl37_*!m zu03~-)&7&p;K@~xfp)kUv3h{4b$fQNv*P){jV5h>Z&tOhh4xU73&%rkw=YpTeMS>B z*+>}nIL~VrL=$(j3Bi-mTTnDtfRbfem-4%#Ht!?WZV)5ZKeNRJXLaxNjUs>j zhvFGpW@~Tk0z?#(!EMgz#%)QK$L|<$AdEW-)5@p(nenmvftA&sHyp++zk*w0Q9~NM z&TH!zI?L6!86<%XXCyDbHvsWmIGSl}Hbd_|VI0@-B>9vkh8DIbF?xBCn zy2`v?rt9r?h1~gK3ucGKn&yH=KdZ)(_7MUn8^!k=h`b zL>@OpGkyn<=mIt0(V57EHmwIo_Sgsv23K)Xyg~PaO!#$jbQuEz66txuXWgb5EiJxaZD-)~oJlBwi zGFf1HXU6F=&DzCW4!EC(9+E$@vdhzipIhTbxBksn=?lHc}P*tPKxx~6N}(+S&hi|0Vo&yOI~<@-rW$b+#`ydSeXem zz=4d%SWgMhP{qrjg}omby01=-!3Tr!Nk+cw%Fpu!uwE3I{v}uVnqnrfjSCaRKSQV_ zX8Gt^=(PJjy!FPpVf6bNh=nKUG| zEl`z@-C{GyUjo<(yp>qN4}MVKHYS8rC+`X&oBIU$%CWRLRKbaSNuu90#a8Nh?DoB( zyS5b@{3S=SyfQ>Y6XA3l(`xvqpjW_G>{C(u&J(mcYgPCef)I@J%ifj2SoC4)5z-sTfCLiL*$X;y5V z*CHE8X(pK79Bcn~^g{p~(v|f`yT_CbHCw`I$4~uu)I3+rfJuT;u%^q3;Pu7+ zkccGJWC2BO{S5Qn_82*cUz6LAz+LtiQ9{TC;yJG_QjiaqJ^?s=lsc69gOpt&DgOy( ze<(;4yz2Gq1;~z!jGT+BJ>hAiI%Fdwwo@vgY+6c&1eECPvAKS; zNNO}ix2}I%^j?3Up>)SPNO}o0_r~AIA{&SHoDOLdH#+gMqUeyKW=9fICX2G8v}kPR z5)eH~YG)NcOE46kFZ_hq zqmik)&qx>{YnhvSvsS-x=g)m8b)310mFR=mN8vjt!7j@=!%Nt|5OuD*L4vjj*cE5_ zVTU9n`Xup6+|i^K4WjW7{r27Y(#);II;x>1C|C@yOU6kAxgK@JEuW#z8h-$9CpPe= zPYY-D%G~pDw3mXB@#ia&{2QDFS}Z_^f7q~M1~7=e_rgv30#X!^*`YA8ZdeoPgYA>e zlZ48^LdB|2kmcKwOcOq-4?nzNo}rCs255u12#W`PHcX86rr>{ys6#gzUx4B$rsfn_ z;_;D*sADzuL3%8ow?1+8rRIrabd8!Oqy7^T!LJp|c9%yg&k~PzOFHA~ee+c?#;5H` zBI$zdfXlps98{`F53bd}^VuxJ(S`p16(7MK|MXJk*cHmQLNf`e&$t`BjBH%YfRC^g z70v0q%cAQOx8&|}ZuN_GvTITzlX1fL;)&m4rJzqnOX_^g67nb|k=tf=4eHMU3H5ik zSbyIt?X!4}IbW^~H8XBYyBaz6>V`(P}xZn`RoDQqx)3~fsCV)?7%j`G5 zu0dsNTUi{=|DlTg|M~s@tBMEt3?=~p;`9Gk75hM?<1RNJ``M|uWM{J$Mh^g?gM;Nk zBM6BM>h|mX$u@7K>!iK=8sfq#l{H(NYUcu5rP(? z#ge$gDmvbCv{ZO`VNlV$#C-U~q@R`5)Rz6O#!s|Ejf&LFrsPedF^Sf;+DqVYNp$~Ne0Ohxj(JIp( z4gGVHaf+s!epy_9)Hz82Bf390?^-`vV>emnGl#^W8eLE>&i91+WUA0{9Bn=rUjgl_ zM&EE}trWw@xea1n!%zqPId)8g^KYVLcH=kb4SdHx8C^=V91&pC)u>M(j`EN@{Da&` zdfK5}u>tgDj&)dWbXu$HK|h*HI0tf^^R_`F4gEFwP*_rUtB^W745dT+3@+MC?M0G= zh)|;N4mWMWd61PY4l(G2EkFYQZdIuHyr zkdeq5NHP$~NOHlYd053VvU9#!%2N=?k12g>Jf4*;>5y9tM*@|l-GHe7WjLi2^<^eL zvY&vZKLQsrooiRs<8oeT8^S+_CWi?ixhAbVVcAv>W`y0QC$mlO#l4>zAmCKTzj_(0 zIaIqutm?2ipT)Z=E*lTby(RUrWeoW@El_x+0s+CzTg$QS=w1V3_flx)q$75qVP2M$;{IEpYs*DVa+7OPQ!YN~OHdn^ zswI#{f}8I{MZA}FFc6vkSm)Tn*!}qYEd$;*p`q*$O?c_;E=TlQ>cU}@N>zC^J6L}<<&46By{TzC-Y$0=amidcDb{Nl|5S`B2^ozREmn#3Q< zMIoA%ha~|>rl7r4fU4!NnjaddHxEQ&mi`mt)GA8hDCHlGH@QDW)RTrs38j>U!wpY- z9*ivq_Zte(t6OQSZp=JS6lbpt6Wc)m!g*AH(Il>dmz;6BJVDlcF-ppbi{B%~A!H7UA}=gpxc*v+PeWfb#IYz((yqok)es z>)28@{V_lmGzSXAjrPfajhln#kAN{2B8S@}1#E93ZPbI@ZgkHDO$(5cl0_A?_p!<_ z2NM=k$DCb>TA$HjMA<@co&6QT72|+!d+_!=K_f?DOWKR76V9UJ+u`(U-4mjP<3#h(Q;E%%NYP^2 zk;n?k2QF`Mm#ELe_2w<7@Itwq8ZYVyimvmF%coHqR69~GQN~HnSF8I1W-$nx3^=3T zu+9G!BTeSaN5^}>#e?#N8uK8i4GSdeIBm{*AE-e#bK?BBvSuxw;{4|;D1#>A<{ydV@7Gb#Cf_S5U?$caD@pqGxFn)3^ zX2|;bSnY~Ve)*Jr9fGm?d=a5|x%C}A1-7#!Au~sUoDwA}l#Uw8dp!ydqwk8oLhSmp zGwgEpAy2m?LG8du(qap?d9r7+SBp)qxP&h+3xH?ogJTnowHc8vv_H1HK6Kk}i7Db_ z;_#FLqy(9k0#OcgnBYQ4MoCI{k=z%SWwBX`SV-34-uXU}puU(0JYCSW`55v#2&1Q{ zI>+E3_p~*a#Yo{&^#XO%rUi1(R{OKED86Y$83M%eBmHUo-5(1-#`v%^@6V+EIbk?crFAnPeJn`i=Qu$ zEmL)>@&QDXdLndB3Jl{=OS`wPsw_h;f|zSm@=l|LnrXSeaKnvMOhtQkx}cq5ECz*W~^MKN?D zSZcaJdOkq4*f@Vjk*&Mdst4BUy`U$Te9BWQdL(A-6)^*_OvN|=#k-7folZX^ArW}1 z+G=-ukX33wpcCLHt2I1|XgZb3Qkt0q@nif`9>uRK?MC{3eT!m8LG7}%$7ucjphX@LDNTGj4Xo^w8LEGcG~tLb`D zZ#R%H356*UxN#AbFNGw=(>E{qB|rJhjz&0bMHekyZ(JFIUdZXctxH;Q`hd|jSP1#M zX+O~;_-6r?WMOB+?UtCer>(*{x0~>?3*>@;JJG57R8l^`Yz3=Mv zkK}Gc*L%S>ZGmFt{$<)jU1tMEGC_@yGh7hwoj67c7qK6D?v zqr;6Wy${MT0qITKk0Dgor_iLU_^ijAG+Ji%8#{o^u3zRVe$;E?^ADoa5v5ca0?BMN6Gz^2_z}|+^9JYnVyI;F3d*W1)Db%EB(1aj9(Kyo)Mfvkt>^2~v+?j<-brz!u=X+Y2JL#FH6&ZXP*!_5arc#oNi$#vl6rh&&&-u6?N7+Y)^ zwl~paVJTg(SVe;ibX;cuGjSiFZlMR)e(U>y$F1=-K7rmuPhE=7+`6mJT27sQ&Q&t( zveXV3LBpI!iX_*G{eyg>ku6!7D}rvc_BwJ8w9zvwb!^-th&Y`12{$xI^fDqhTDCqb zakChuQ^aHw<*UI$1pcPy4dHE6N{ofjQZB(K3D-v$O`6;#1ovXs+5|~&jgGX|g6~4` z0bi5dRW-aDVQ(+wQg}U$XgKxlj@dp;)ok3MDZl`LGxF?kznbu;OV z?&JQknDyu1Yb5n2x3(@__YdC-k57%{m`p%x*cLC1@s`_0v-!&*Z+f!8p9O^>Xcds? z$~h9EWVv31owL3pE$zs}@>N1Cgi@*V4k4Wn@T6OF z43|sk5_?iCvL|o&5&6%!)jXwET9^0Z!Wxl9eU@jX1rq^MZm{+Ly$`Cz)gtXu`AUQGf1Z!==k_Xns+VOIwO{ z*8%h;o891M{!SeDe|r+c>(i2Q=5Jb(vMP^2*`deNOVDpe3mjADV4 zxYS`NNOxJj35F)sUMCQdW!N2ohBJTbs2K#@u;%meIoOsymUE%A9r&)(yQUIVt9{I9 zgTG$2`h$aRg1hzjnz}vBlWztEo`(wUADreNC)~j0RZXApSUUT075#S6RPjDpAQVp|wb1mL?jOeiFg^SEx1guO`@ zp9}X4GOq;)Yw!Ge{%gN49U*7WlviBVIQZJfSgp+afZ2}+6pSC4S$RdgH07O~qgN)d zH%D?;0uNCk_6m~O(N8WHY{;k^Z}!O8ts)M-#Er z#8mijRP&E2(dCWDR(Wg=UOYK~3JW;Gwr_DQ4&LvWD3i@us|K50;`&$X)Hp`Qjf)Cm zQWl;Xmx<5vOrD0+ASbt;3Eak{pv&BOiq-lQEq3 zuOZts7g>CyLPcL^8&neK30@d61N^?&q1zAF3k-7CPRYyj9RU@s-+`ZYyin89LgU4n zV}*XIMZV~*IQi3+$hMXOvIY(l=Cr(i*qQuZy^EH4N^kuiyf&8> z_-=kYTXq9R6uy@+fEQcf(+BlNpc#^m#W2DxrV9F^_M%V3Wtrsbf^wwBh+r(voUju4 z5044a%r*Y7gVbvrPPn17o&eELsM6tEDh^W_f;Z3o%{5_<1br~=!*?a`Lk>DLE`Ioo zGOZpYp9m1^v^nv{m^XN_!%6Hi*77PawmBlO`213eKa_k{*-dWxy`~ z&%aVo<$r5nLZPb#diX#B4nRILvlM!L$t+X5g8EJf1eMVHjuLu0@y$W6)gmUcuEdAO z>hBt5&sVV2w7QR3ls}Ky8LeC=5}Fzg&O}Iu$13I7Fk_k>i8{i5o2cl6-8>V3D(O0n z(wXA1_*(>aZjVvX$ay6ZwY9?J=VV#$@;5&8vV{?XyY=>CdVcz)VX;ca@?Y^#C45oQ z` zrg*=jZa?QyGJ;K;@3dhL3(rbz$CtL@QZwnb6%WE0yu%5IAs73?O6^}R&w9@CsTG;F z(wfR!>#-;Gm9`6e6fz8jcb;fkoS~i4+MJ&ljLkH1ZUxc zVtHAi3BW#mobcQnB>1lC!{#Kgk=G1H-J+0kSj6bg;zy2|@)@{a&N%O-H?s>M8FF#*dmzu|a6eBR->H*>Y z?S)o<`FDSut_i+dDG$;Hy$A{ixn`XNt8ZcrFqH)x%qSP7;rMO>3{?lLf5=lC5zL?g2Jf zjeMg49rehPVLOA9zxRqLv-Li#ezCX=(?_gHQ>V@a+_FuqfDh0CH^l|>2_)|{x)02oOMiRWkWevMQ+daNWNE}_}}g*{YaJ^@lP<0=k4)I%GIVhg@yoTOY?g$ zb}S5w9b?utP+_?>UwiV4(ClE<6{w2URa2wV4u-FYwV9X(&f;JFmXD8=K0ZWfyrel^OYT0VpW)A;X#aag`4bnKl?Kig5vtw}A>CQw zbYBPkKS;q?!{TZlDQ71+AKcOA#l(9z@>Ofa5e6E1Xu$9Tlb2F?iFvVMc3B)Riu+7*);PrWZrn#hg*^eb#020=f+Gw?5+&Wm zEm6zmFYC9z2j&~Sa`bC9em+|p3pGI-eXb+{Mcfu5UXG(`Yae6Vjtyz+3;NA0CK7N0 zaIoZd4dAa>{_BkSfD+mhKedFIAUiJMJcH9 zT7_U_NwvYY@q8Ct^4W>Bx0n<3JuujpN`!?`s-Tp0W@e!Re)1{lt59tCp{datq8&MX zWlqdf^t4FjYkr~rxl-Y62DocMBhyw|F-k=84p*=Ch?yLIlsZa=84c%%t9H5J1)jIuRC!}T`7P%YTKEDBIl@^8 zF0s&P6%We>jk4MC9$EfYs{^-4} zsv@^}24!8(2Bc{0QxX!9PY*VK_CMm&gv$@i)olT6r?H%rDR^DgWO_{rCYSuWLDWXD zSimm@`F}YjN$r5laQ%}>Il>HSO)7;={8L^}P%*v-3QFNw(deHxz|GDu?2gBO>3Xq2 zM?J6|H_DlhTY|Zhz%v{2>?8&C$FIq78y%YJD`}dbzb)^O_;SR^xxk-=H(Klm+1I8yCRUcGwIJvs_aZjWYb zUz-H|{|{g9936=hbpdv)j&0kvIdLYOiIa&Zn%K58nb@``wkNi2+vsfnzHj&J?%8wp z-|jl^RaIBL*Y)nLTUBZhHeesH7>eKS^>B*G6y{O z9N+R=wkK~&EhX} zE%1A)@f8vqxw4d~%b>G2ia~YWKKC5;=W;Wlr#^SS%L3b}O^*qA+F9;uui`(qOuEwq zhFoMdXGNSZhvMXdxd(uJGn&F#vpfzWF<%^L4h*10J38jOw8c^UVJXzrDOJ;BzK{QSEx-9zov}rdJ`` zKBE#s^d{ewXi&E#*fIUs){k@gjZ@0gmzkcq6g*=^9YOo+w7Vn128*df%xwKeIo@%< z{gx4earuC3nP7Tj1w72fV18p(>K-Dm6oHhX&t|L0!VS3uZUir_bJAdDum#LRsYD1c z0#9=i&zzpH4Q{3|dw%bxl8EMnJNW#pf+TJB*ZLh4lf)=f^&;iBuHk2tvE zHDNZA;062QYpE0wbjvR1ZbfXhcr%dH$5ZqE!(i(Rt=4lY$`Fx-g{if3icD$#FXT0u zOk@479Diz%|nB18D0rdV7w(TyJWB6+fOb0mC zEfG1@dWKTE2_}lnAqNAA&d~K5P5)oKnUc``?kL0L@uxCvIMN=j2a6K<^@QSC#N^jV zc*4H$T8eS1=LQR>J`CC1SS?O3mmwfNo6dh2x#py=6=KV7TOQ|-r?0dlF9mt9IF67DakJN-F1XRt&%sEd&pT1rmdh98vc=cyAP z)BYb={8@`fREj{V%l(V;%d<0u0C^Ulx%F@4Iq&rVUhU|<7MJJW|!sehv${$_gPPUxm3EC_QnPn3eVajN-E#)}I-(7jcwUqyoWY+rz-pKA3m@}mvT>@mr+#@H3qx^+vm=&m{nOV5K8g&h zbFP&tB&?jjvJk%}g&0!q@Cy)x^XcC8dj~q|R9Ii-NWKhhuD5cv<|-sUmqPGABQ1fh zXXUdZ7_mYbG1}ObcRJ+qZfqOZlC4H#^;?-elLe^c*GInOa5aT~Ylugl`{`UZDlfV1 zqgneTE81>!wuAFS`y^GHDk2k~QYQQ!NylX=2|~hR54j(&%0J6CP?MmKi5tf9++7Q? zQ6-R*iy?Z+F}wn+BgWB}TPZp}qLk%`-+U~oc9?4z%SN>j>Wvur@Kgcuji>tU6}GE9AIW#3Tdi)c zvK3h?f4J7BkP=-jF2~o?TCd!??*2TP;`QjX6GHh+8jE2Bzx3Dj%*%}F}D z*qybxwG)`hY}`{s)S;uDo4iO8p>@X>J%XkudkUqjS8H`g2{AB-nYyUwtkO!4G)A@V_Qnn|-va}@2U>s_8X?q3UW-|z2Xj}b(g z8N_*mS{FJuwVg>{-iOC5X25}lh>4`q*I14}FFRV%(JKCvVqd=k zxIui)qS|Me>88j1%zCTFzkukTffO-w^wLyax;>FpNySM_w&moq6Ne^z>&+eD6WVY;6FfwMb zneJp7yL9z6M$m>Nphd~graV8?8QJy=Wf}V;NACOet?ZV&CQ-BJ-`DANt6_0r&un>q{^i8h@k6cka*-D2vsyO7GBV!ZRM=GjrljU0qu|^gM~bg7 zUJu)rZW%m|oWTh=74y!V1uJ}O%rWpR^Zl1U&5LL1x3{zqxY#1zD@4%+A1WsYlyz{3 za;){vsU84UggAjZQQbnj?}{$gV!Ezjb!sjj>TrGxZnidir&ZT~-ddg3Hx-JP3I1Ns z-So6Yi0Fl#Y%OC@9J@OX&sfyz-}U>udlOl;k02E6a$L16R)YsXv@8ttzN)4-tYCXo zNgr*roaG{add9#=g9^>mg@`Lo!{N1^{B(IS#Xb{oVn`X`M^~q9JDIGI?=(2LiI?r} zqBiEMj&**brfQRpWP~xuGWk(Mh4I*Frqcawj!=fI#KUE^0seN&Y2iU6r){IgW{G$B zcX7C{{H=oAJ^TRnF3>TIPfCwM8NE5&6weaYU*5i~HVhI+cQHvg{%Xb$R&_O*O=?uK z?HvQ-2=4#YdVqUZFV^5_mrVJ`#dn$BuI9z0&k{q;E&^WW41pUw%rapFgor4>(Ry*L zR8BDi`xvPAPn@QAOkj{sv+_Ny-nbJZt^x?TLOaU)VC^YcRH(1SkJ~(bK1_B|A<
7=#; zoktEcl3)pQ6Y?zMo+8vnxxA_G+s9JV57IilVPJG3Pfkd5%PQapM6ICcg=b7MEGCzs zywoJ`r^FxIuoa^pg?kN%?@~D4O5ohz@c;;ylFKRjhf|h*dDc6-?ticr&@rJ4D#XvMg18K9VTx!(wmc6T+iu_|Lt zP8Ot;pkheGt%zA)q%C3MA5``~ZnZ~ZY7Dayj`TO{lI4-KYnq#dTddP%HM`K232^9N z?oT}wifR&dz<8I#arNJ&Tbt+ulpzw#S?T8I=?X*AMd*Z2 zR$M}SJf1UJ;Sx4Zvyoxj`^?0fr2(oFTuh@E0GqA=QPghr9F$xAVkH(p^=tq$ua%yvnP$@3GT60DR_jkyBuq+^;OD?46$5(*8{d^Z-O~^U)}9b* zGJf(~Oy}awq@x*GwTkZljH#9*)!OdzIA*q7%7^po`u|Fb`tY~f+3a+P`cY zlU#qS`6q!Q-9J`x_N4zc5plLV^w+HXtJ;k3c*#IO;wuJ*B!<;GjvnphoYMJ*p#4aL zP@PYDpFFZI4+tE47i|5j+00ivWRzYlSp8lPcn~3f9Y>5owtZ)41W~p(tbJ=)2+t3JT;wgMANaM1qge_iSA2|pX~xH)6= zt7pTTM|^VUcKMoU?xVLq;sB0U5XiTHq+l!JxYUU|tehI7q`qZ%hL29LW(fv(H52}i zGXsaM{uO5>&08jxf=>*L{(KUDC*@4qOUBFwg;F(t0hrgfeJU!dSHKT|loMLJR2}~i zy#_hipePkpC>$Q2qk)6rj*j)$bCo6lIOU=`Fu>M}uWR#>g@}SjL>~TD2=!HLFSzhE zE_k3e0FPW|T^ma}%}ghg0HzQEyi8!psxglu-Xhyy!FvJ787`jx;REg)Og=l;1@8$O z3fxG7p(M)6Xsf_h?^J6{Yd_i7D)LG6eEE5w@O!*4j6w*!Xa~+FDXrJN_{6qO{ubK+ z-6lz$wNBv&H$jgnYS1^8E6@Sa1p$O7vt|e$7WJu2rs#c=GIHA6`m7j`K@N&-dk0Mk z4UMSDHlYdAD1!|d>dRPYRjE-qY_V6K2jujJuVovm+GPB^vt=*j^DWZO7-O(#Hz43$ zsI(j}CQ*Iz3Tn0T(#%Rb?PHGj+sxl1Q4n|=JP2-Ci2LdOKGzUpomLi}SVX!He z-oM()C>?eZi%l8^YmgnWOcVnOXBGgYi{DbJsBB1#{JYd-L`Vbd_aDSM29cSNur{h; zvY9_Z>a?L252JFU1@hJaK!Ftd*eN}vqfl6y^KJ=_s$4PoT_!G5SkHM{s+R&NO~(Ff zG-5E6fM_Wo^Mgw`7dP3=Dx@F@L|iDk5s?yYT<*!Sue+(OQ3M?^P-!VQ(O8u`+-=vB zS&u7ombw1knM{npA0*=iK@(*8k1df+QL?gBO*iBMnaqBjZ~sqph#D8c9&U}8!HGMY zJs_{6Q>$+}w~^u1BZK z0=kS#CF%WwxgAysEqXy?z>j&)M?bn;ZOvNjADlGd95ecnY1B<9d3;GE*m`9#j4ojJ z;OXox8x1WJJ&=F%d!TAY2Wb27m#v>!b=Np^`G)1()C*n21uy#t9)kyLG{O{gB=7p&LWCdW(J6HKav@N!_Ib6!RSUZQFqcAU(3MqDC)lq#DU4Wfl%VE*jQQQOH6 zDofpXp8A!Y8-U!>OlzyfZf3!q@0@t79|gwXiLhXNrATzSzMv7WmLw7{gzd!j%f~j> zun}U}LU>h})RiQG|7I}!X6p$TWrP$3#Ngifiq9NwuEULdv|&A@CmCwC5nB~~zvIH- zY4-OSINh3L8!SD;t(z~io039l02L}#w^|Hj!KU1Upyu4B#UMfibPbJ8-(fVlnp|yi zWHEbxJ>6pe4A`HX5rq@EaXBbgjW6&@IZ?CUkhV#=m0Ko>b8Y=OORT07I!oj{*zX&D zHHA%oHw;@HB`)21BR?qy1NHhRtq&bSoOKQVH|LokQtCB<|+(q;!KbP_pH zAMH!H<-HE?_#z04cuM+-3K?pg!#D%>JApO2t+fmXK`1&>(46lYR}Zr~7J6B9{~?b? zq|)?nMl5%yY<09KYjo!gdZydgyf>(8+*q-lk(vCF@L09ZE@k>c4eDI4$w|Vl+NaD_ zhC*9RlN&tcjt2r|dyw$%Flt{8Q1Z!h@G-X^ZRE%9iv)rH9W3T11FzC@GJ@nop5+Nc z^vAPxbSe*Ax>u)Qh0n0{qA5IXS3za6nxh!Oiu$B|WfiuIEQ(lEBiUCkM9p?l|J6l* zz|XMf8$KpVdLU$jQr&k>w?9e;=u(?oRaDBI#mjj}aS{+3<)?GGRstG1q+McYq2-kM zo`YV*!Zsp6LwFBU+PoI$Og1Wu9NXce__hac==P|hP>!0NFL+aP?S5h`LJW0nSvw(Z(zJ+GvM8=D$AQ4k?^J-r- z%Hr{#h8F{a9*fpFojWu>s9DOd?z8^*wvvr?B;2GOP-pw%TpE}Dyy&od#g_Nvw`J*w znor@s3?t*+l0b#C_7{FGL6?LqV@86JA z+3e|MW-Shodva2i5-vN?g6NynFW;z*6#z;l%G=8d-4b(atb(v=c#sR+&=@OZ$YMOx z)`+l-Bq2o}+wJeBsjhcf`4TF$Pc2cM)@}~7Mks1u*%XmVo~|U$x=Ro-W7ej9AQBygZMfY!UK&|CKvEWv?N8BECM7}$O;kW zfwr>^@OuB3wdQc%97o~mKpR*Nxv+;VK9%hy-48blaQ>U_7z<{o_?%-!8-7qHF|}g& z=3KpNzq7>J+lT~Ff+e^{F5SS10NS$!n`ELa!lL}LWrp~lH_*4R9@mNhHd7EneoZ2& z;yRWo#O;LSP4V< zNf}OG+QfUvCEl{}f;9{=a4#ki@Y+awS5o?TozUeEnb9vMiEkhgMO?8N<>HJBadG)` ze4KoUW8ZhQEj`bu-A$RNJ4C)|rovhQW)VFSXYjn|j!a+EyZgh-8i+ZH%O5%4o&YQTsPKCg{*FlILa|ggbEFcR)Qs$)wkebS zM*iz#3tV;nP%;HT!|x|euXDD?kKJzLD-SpWdBW4p-uAzvy4;(*(9ZVp7HT~Ry0#vZasrvjn}K5OlJ=m1zA`Q-Xr{qFS$-PJ&+&l-(f9F(IME- z{V~9QG479$Zd$(=%dr#i|09ni#a4$%fG<1W0*9Zl@KJiG{l!t~{ zf4anF6g7k%j3#onY=y}&(qM$I0)Ng+B^lgVDRy4sJbQz(2`?IL#hGx16P9vhC!$1U z$!6(R<0drF9Px~G@ci$42>zZ#!rzx7;643oXKQ_z+4*JO?LSK~L1lZORlq&3v2*{H zF{5##d!252?Jm5nk;o9F*r}7UGg3^gi1d0s#=cvwVl+Ejfu6gG#H+fzky`O6FQt0X z!*lNS?sYv+9JY1!6CrfYjh6`s2+(q!ZBqzc+wR=A21|@ysp)Wxx=eVrq&UZsBX|6^ z?~4B2?FH%T!~Kr6j5&}}uhogVSzY~udN({VdNp&5?PSh|&9@ihDL$5EFxwq!A_jT+ z>AV2G`-VxPjG@oC-i(k0)*n(KIYO>)ApFdJ_&(zk+JjQ&(V^xl>3r6)*lKs+v03fo z^^T3Pxvv5;+H*c^rJUVJ%xoAaF)DO27WP^HOR?in9Z%4yx}}|Vi`6ZT9fufH3#sM%LQCHgzfPzN%W+Iod@ z*2yP+e7C-&_Ox-L?V?-@M319DXBui*`^1{l-hhs)e#*tp$kF6g?R2AM@Vl+KUdZvs zf$t;P?d>JE1z>50pj$yPW<{@{f;eR+)3^3E-?3iGra;rAenG3+IRT40FdKX5P;5V> zR{&5KZ&rVKfdfp0+4w0&yAy^31A_`v{e-mxfitQ;7;ibR5aasxy=Qmr3u%&=l8XRU z->qsKkyQGvjb#HdUCk{P=t1`VU=z(NKK?zswx_R#Tp#=|y;?->w{O?;B$ED+V$S|lpShvZ4agtj z!Mn_SA5m8Y2iq)69ZzTQI9P3*NNomKnPp=E{w_J1BU{^{`u3c}N zo48EZ`nRMvYCuTj+0b{-)GuKadP5c;-vUqi_^SI=8O zk`6{=+m8Uepi$<=w4UYmPZTFn&`GxDW9x2W34@!-)!tTE$swAA$BQz4sTVm=+}?g! z(mSWH?D&Cvs^0Oo&OXm-OPT50-O}|-M>R%**M{m%N_qa@I`qc~sB6^@BY021suAk* z-$AA5w{>l{qrIHVCeF$NFO|WWi>vMDxz7->3-nTukWLm=QP`$7GdWq)BNtLYUQY!@ z+kuC-5q_%gnJ^u`Xx$zkwk5xM+(-8_dE@IIalMITWu1+KIYcQAQPc4;L+c(Z+o!sl zYYk;e;;ISaW9K7etnkYfooegIg@W+Q4wAH5e6+MtG+vk^uP;f++#y12Gm)Twxu^(I z=V841L)t7ckqMqNESdDRJ4J+A^LkWk^VSu-zz_6eMa_p9U;8KfY^e_MLMNhQ2`gSzA!oTLDX-+H`#4-;wdb2&>V|x4~UBKxvW7h*j z*t@IB$du74&m-vY3zyIvW^ie(>h0V_&qb`|5iwSS2?z2f@5Ggc~`ebd)PelOr7W0;N8>`-KO7 zI-tL*)r>}J0?&0?is_@iVaQTJ&cfgVe%agZ4IYEvgSy(}$(=R0X6XaGbVn-(pZ#T} zo=o?Za{4QEuR^ExPWa9Rwzt;^9e2yF$WwuWrqgfR9K=|#{-GM*XW++YPjuWrxA^y` zW|tF2w-VR7&yGH0MuseQ9>N5;z}5LzL`Ce2qZTIYA_`1;z?c@GZ& z5tH-E!>mvS`v-VMdi<3a6(Fj<)b;q0P z{-QZ;rTD%0Y^lod<^Y#MRS5C%23E`kS}8OQ;y9J4V3`O(dN`W(gA=i2@+AA+&S|*X z5QN3d`q4-3_vR!ocdtoUA7R-S{8S%jdQ;LJuBL7g`Q|yBYCd4XmT&C-Pv^eZMQ|G^ z#;_v+%T5xJ`~3SU*LmkaxlN_y|JNC6=pP8QeQgcAI(Qj< zKe)s77mJPk*DL^{u=!`P=zGU@2iX`_fji50bB-J1@YvV(s6?e2p@u1$+E42Lhu`5Q?W zri259Qfsku&i7J|x!F?EC&^4oB5Sb;5JyFWUfe!lAVmyU%X20UO48J@fEtdcJqARe zgU`SVKhkp1V-1)`2-su@v{n@)>t6Wrw_8#2?jO-}tts3~Q_~uTVV$BcxbB1%QY9%G z*9phM*N;1tRA`Ct%@S!`xO4WO95gK7Sj<(NaVvf`RZg-eovz)r8|FWT(C(-N=fO(& zcz&SpK^ZkQ!JiZd@+-70-4yWO8S%Z2dX{pB<$aw70}}Zuy~pW|Kq)1Zt$G!sOI#2z zq(C8y%vlAil#Wa3v|p3f@wl}F&FfQhzZa5r@I<7`7cLeeS5qXdr?f&e0)f|i()MEF zR=f@8{`oKk-Pg7=-tNEY)nqH*)0RpS!J!*S^7~Io{xW|d%j4e5 zyF{EKi~*OI57=W8^Ns$kjBIanZl=5MgTxH|!l`W1`*sx-y#XoN;&a7EQp0%|qblmT zr*ENqzPteCpE;D~I&}IjjQ3)>YvNfFEyHrT)*o>JbHl;M12byxi_uC%p43g1)z$(r6 zy)VyNn}G`5H>4EJQ?ow~Ca(~Pd*i_6wp7>Q6m;$Tbt@oH3rg&d`i%qQZA}YA*VmGN zv>Dl?_^@MKYA?@EMm!o2m)Eig5$pbf0V2iWi+7fra^ugZZgs~2_GEaYXDuQPn)haE z4|%yf!m=lU)Yq(f5mHo4huV)vkc)o00PS^3`%}H7O^2|%9cRpe0xwkicj4umPE{Y} zN-+rv+`%Kg%2$F^xvQ0HrdAvz(e~ob*zWdwgUI_0Zxh4CA_!fud%ZJjQ$PjdM zk|bYHP3`;H7RJ84;g9<2SK@4O%>(7kGLeKf$^u+A&r!u_(6Vj#fT`;Gk$HbFAiT7@ z&xkf3UO6)&&_jXvEM0m@1>T=C_Yqa4AJYmFGx1BA#c!|&wqp3+SSb#cA8qD77wxG| zd)DOes|8klc0~+IDXTa4adk&)egPk6r2j6*+AzatDqCPjpHC>@C#8LQz|RO>n8o)( zDt7LT!DGk$OgX!Jg@|>j@~wf4A+>|JL+ONw!SIRF$Y7>SW8sX@>JNMx)pc0e^2No( zQH8O#=#6E`ro;yRKvX6b4o3BB!I@wc3Wbaz?>e(3@;yUNIX*I|C<7(;shP5!aM~DI z8qoFW;=#HA5M5rSzb7T&6r2n1pAtc&#(}weE=;c(kPcv9P&>4> zfhr#F-&d*W-B+jqm)nJPALfQfL_fK#4PT4!C`sW%jKl+Y@%tklL?v$Szu~ca{!D3r z%i1X*!c5T*`ieMr+9Fa_Frs6kfBk&bd+fccN4@P1Jn^dVM8g|)XFq>{Mr z(i;IAmfkNZ*UumIt17#FiO6*J9vt(?}kRB3#7PFs^z66w7(1gQ3^kTGWe9M z+84sMOTHQ?b3Jr+=xLL?c!QnsWqTc6%Y#wOO;pz|!1rtjGh2(HyPxyqS!9 z(i@1JK(`sG%KJweTAM-nBuku@Ha~lsXhCpcevLSrL%*;0vH3JhFGn|gJ!Y@cQB>}% zh*qyT$(63ww_YG!JD~LE*k6*{6Yg;V`CO^U;;8;ylNdBf^y&$s#^uj^^_uSy<~fpo z!Y@5>H&scP#*{Ee`$!|0j`6k*m{=muTx8Nh(RtS+VBCb@@L1wiINnZ5^0BhC4$k{%{CTfcRZkYU8B)S6GEqjS3P zzQb}Gc{0aUn~u8V^v?8?_6}ExIJv(4Koaxt<3ta0+xdgn;H|kBN~(rFh^K7p0dr$( z&)1KCa+l2S7m+PNcee_!PVBc*)pCCI3H3YtT^QrMb`8o0xg^%~4RtB+#Vsx$9>I}W zAjU$tfL1u=@`mhv(}u6@fV-JA7pZ?Id+OQj0ABAGHWTHJ`A}#iP3|09Ox=<%L3zf| zSn6xRrP!Rr!`W0X2Qr-jf3J4F;C17rFua+8)3}CW*Tqx8cclM|#Phw0j|L|SEJ8ax z)A8Z~orqqm3co(OzUbAJyOc-Kw4D9qaQiWl+I8C&JM(xl+}eBlZ)r37@W}w~H1_ee z)S;+3$WI7=xYVp-K(AXv-!H{s7=qjH9%tNLqfu@&DMxB(#Y@VLGvZJOGT?S`8KbMnBT8Iu30u>b~~US>bkQ4t*;d zEwvd~6h4gE*N)#v0#Fl1!0x)4TbuLOzcoCc!CNg?A*=g@Dv^_Wd{uA`;S%_^K@`Wzd0NPRDsAd|P6WLv@}s#E|*C*Pzb> zAMteDG#IB}3yoOz1OMMSK^oKoVA^ho2RXXyRAxU0FOC1{s!6;QN~i zNBIC5gcGC(4f5tVX~fObCnd|-;N{gPa(+#X?8sG!61u0_ z@YdT>#xDh*Q}n&t3R8+dC{c)jS?oXoxAqNi`T)R4W%kCHyIsa_lsIY zv{2=C8U#=&iq+p-o09sIJV#d|b z`qVnI%J(VXL+ZY=7EE2`ki6Gv5hCy1x1OCW_}7o^bhsum(W|?VP}h5rWy_TYVyze2 z0j6Fjm919hnjc)_=%&5aapFssxyxi=S?_pUqtM7)j2nI9IH%2WhY74;)JuuICMAk1 zc^q96_n>eE<2IcZ?EgKCLV!;>t%P!yD=oH>xKzlPq_*zRF*TxL@iVa0c)y^0d_)5) zh;^oluCV$xQzjA&Qj}H;yaZgd$*{YE^}Q_XkA`dGkMj9$4Wq?feL>P3;ua!804$4!-|cor9ov|rlrOsz_FuxBu$f=5-`ib&&wLu=9!JT>Ux%u; zy(*-7Zd6^L*yT+mnQbJ&X+%&&xw@MWt=2hhLS;K<`+hf zs2tIW-3Z$X%!mI{?Jk63c4(IBv-W|s_um8HKL=evl4%ZCO|d#yM>T+2Vnq*w&1PWF_}B{)3SO!^tn!+ zj-KE@Y?Q>T9C6~gH=zp7f(uGLX?YfZWv<=RJee353OK?IpJmUx+9=oo&P7PkT z?+?GbmsuDpMiMSp_*yv7^;Ox6v=Hv4R36vw#&t>5BlM;r&2$_LB4JRUa6*Z(DCy0h zW%9`Ge`b03WyPJM*#OJLxKe1@)^`>YIM*-=OT>P2=VJ|W9wM8z(_)rp?|FTU|S`}ki!5^|%ZhB4IuiA=+V$=vQwzqr)eeLZlNO!`qp)T_mIM-X+C@uz&{X~6EM5)5MP@G>lI!CD zE`!kCQ}iPuD(nKfoiQW|6xxaVc8f+E>M#R}%yyp^lg$UFxNbP~TQ^Q)PL%w-VIqv$KKd%ATzF0dDaLqCYu zVO&3OPJ>)94G?_qQ!;CG!x=`K1UI$cx-Dq^%M*p46OjMD)P#ib9RrPXU zrD1NX9Lwh?XRj{DHX*C7I+>ZTCUa za3lIXAniOdm|x4qidfx((9m^l_KYO!swy#_=@xtDYUd;0u_0Xf;k82~QiJ=OwH;%R zwG+ekfqS`q2TY9kpzK)pKP=Gt?wD%>n|Uh0p{PX>%C~T{Pqw&D8tSmPj7Gq#6OKY#ORDaJsb*kr` ze(JB6Kf?2SDhgN9rlsDO^jvnY#DOz3?gQ}E*(W!sG|L*;#}B*rVOK`G$Q3(xFB~k{ zpk4Os;K-hs+WsmcrfmK-$myl*+MlI%uQg-0=1ZMrn;@Ru35)H!<5?&^!Hz6r4|+@PMc#z(?dywW8yD_y@39h|dZ z2MsGpm9{`1mYK74wwEgB5CBRb7CrM#lW1A7zP|ZNxSd}xJ zTS8mk;Elgl*ymXiT$IddEmn@J;IATap;lX~k8I^^7HWxjg!Ks9m@c{@>{RlA`P<8tudMld}Iica1 zN%N2fhlrQ}#EKLEY!vt(8F}m2nE((UK513CtQf!F5WlEJdkk$5z&`2(W8gujtuC?HF7L zeyiA$$U{qVjx=e5o2YdAXa-Hf_7k`)x}LNh@Bm34*XoztA|?N^s`4)?b}l<(W^fj zAir}4n;*~TU*w}Mr;lSt6~6g{n!B9uH&4JuAAYh48JjPIx%UB>s-q0L)c$yEeL|F+ z<(m2B&dS9Y&~#|dmB|-*oCdobkvd)ZbcjviH?FfEM;H2)k6=*UqUA3XF)UR-LQ{nR z!mZmPss>ftfnxuV_`V%w<(9HJPurzZC~a^HNnpfNi8W!X9kDequcdbFhVpQ5ZEN%GFv*nbYhHq*`g+quLL#v*P29}-}42qbzGi(<*lHK&f1~B5Y!;{_`3azogenAg=xPDFO?2P`7L{$H3)=4fG))= z8s>A=9f0Y?+8=CV*SkE(D4=` zcnHmuhss7V!RoN%`K{Lo&C{6Oq!=bHhkvnsOBk>g`c;PsmfkgNzZcd22k;sTvP1HZ zW`2~9pHOJWN)-ErlVO1TpyozZ%;o7SSg-K(h-C0&DF;(pNMjgPI37pCDaZ$(skX>+ z8d88u5W#fqj8Ef5BwgeNMzQ_I=^6$SKck~f_W^lKWoReZQh`5IrAx2oUb;&PC4=BjpRzXmqhPO zsp2jM=UIJ;VG4#aDUJW&D=X$!)uF&;zp_e+cp?e3RyFFhNGtSby%!`JP)Vwnin$s)L1}9H8 z#mpPO_0gKkMT#cY_rg84dJJajvwZFG2H*j9g<3I4;-0zJxR1<;#Q)5jv%G4CF%d10 zV7`_jTz}Oz!!<3OqDc!pRF|#0l07pT%vTkX0&SL67anPPi{rhyZujhMNm3}wSK~80 ztfOmpfuH6xFgR3tm5M(ZQZpXH3Utb57%~vZi$0Ap_7RfBIlATy@X{ z2CRf?O_wKkJMc3Z!jsiVQ9!kz$;C7^lFiQUZ_eV|F*xkgC2%`%&{p~(1OC`dOWU}& zEddJuOe9pcEo&3!gMlfk4lw=Fy>3oCb!H;yy98JSno zCy@>R+t|3|uiy+boKY{UGTCO~s^RV*YMWuaL=+-%QSU>ngDDUo(%8|BZj1bI5yEiP z!wu|;Xb85D6oBQpSi;WCrN{P7$TrooBq>3RuZCKzf$gE+Rn4#Y8l70UT>sp%{5}mr14Ms)U92 zaiB4{tVJ{%NbXPD(MgC8U7GfvT0^Qr#h2$FPKc7Nt}+4VT94NAn6Q)teBd$g$*{64 z?If3aXCHkDYH&B-A!)bQx=H*Sntn?^skT}r?~UlQQyTM@`#9DJXoy2B&|RE+Z=`7G zmOMszczzIQVBU~od+I`+J4CgVjn(i_JAdO4hlOYmn}11=&7zMv^HkPYy(nRrBHEv0 zyH)Y z5RyYkDw2XA(n?5341=U}NC*ryfOL&g0}Kk1N=gW#q%ufL41CAmckf-_T6f)b|KI1` zbz-f3-sgEY`dLhpj=kyO?S17hHAVIE-hqh}K-e(d3gnN>c*Iw274YbK^aW2Lr1R+0 z9V|`9fxo$?z2clg)N#+r@U4D>aEAz(%y>1gz$c|#WvK0@&EUKLnr3VJQ5TK3ZW>cl1&Dv)mit&`=RjJU5;w)R8g2Ay{1L}EHu zn8%{yp+abRxfJ(0^^fkP6-V_IZ5`htK`ly#w*hs6^s%WRzC;Ix?-N zr#dIx*rKEuOBPb}Z7x#7?1-F3y*2`&2?=Omv&@FLc?3Ec#|F z9_~Il2zh_=WKIgtL)`H*P2U~7QSh@@OfKg!;jpRXKghq?SV&91;ZNG$8_+)ZrDR!&BoxgD73x7|I z&llRPp`1HPjY{K~royYfCSp(mD-t#yBnP@v;rL@cm`)5i;uUF9tl?R*{lmD$7Qr1B z?=<#~dS+YqSMWxs-Md{jlcIU-SKvJQ`(i_)pdG>IGb9(RDRA}aO4~yQvHU;E-DMtH zUGeDOV=O2aPWKPYg~WYlRvH!55!MP>eP*pTZdxguJ%V>FiS2*H#h$un$9LK*x>&}| z%lu|xagck|m-#mGDTTcPGaqG&ui5ZnK95qR;A09^8-9qDZI?klGnzg_STRt_Z48cU zrrXc4jc8wW7o$%?O_&bM_g41sxFbkW=b9n4EWl5rlx^s*j6jN~jK2d#k6Ouk{ZZO# zLq;-!_nl{N+aSndO9wRuzU5)gA}BPD!D*BZ%?Vk=7iD3QQVLm87Cd-Q4c5%K(3XOc z(Xw)y&SpMm<#rlR<2siU14R`fJaSEhm|bM4%Ty04*I~Pqfr;&6LP%2W>obyXKa9lM zA$-fab@$fj%WwDq@=E_E3j2K@$1H>sW6^m1zECMYs;r8Yl8?P&Du2xVDH8(-rl+6cK(3tL;CkIn^GUjk z6Dc%tu{zE9|s>pCU$d5D(VB{DZlF5 z-yVz1Y#79aylDj8MA2Zzv`RKGq!|cp(;WAyjl`gz6Xpj;h>cf$D8pATw!wq8>svE< z2PblY%VI1IZpk-@Mt6zs(AHpGt-Ctla7sxa-`-WL{rv5g?rBw{5+b4Ht?>6{UvHZ= z&8z*x@!=F3b%%k;sv+cMlK#LsJ+Y4e+PTlPur4~nYeeMT6Uwqj=%7>1&Yi$I_P(@* zvR#Z)rT2*PpSh35G2!4xASzC%NnvjfcIMBGEJd-JIKDX3n<4#wXgNcFhD6Iti*3l> zM7f;8g&U{m;Gq18N%7SsUZ+!Z8Z8@P905ZwXHOr#b*pW?`yotn|p^glxdSx zvhpAwD`9y>4z~}K1+a_~`BCkpY);=}O zDub1@B46g+CsFeOcwBVB#MpPV-?8k=(-k?nKqPL=nwv~3CFi!U^x1}ry@7RffEB%> zP-#lsr$qg=37&5k#Y&d>5P+=T^9wN!s*heRpQi_)0k`I_84CmVjJd^eYp;;Y*%nnc&iJ z4JoFPk|~GbsgAh1Co(!YOu0@BQV*a9p92dD>5?Si;EQjsP-d*L8zrI>#k~GZ!8Bb4 z1L8TYv%N>8Rmp~vEsP@dwn*Ge`uQ(XM{d%N=Y?hR7;FY`Sj9A`(5ZbvMDk+s;vU)X zZRmxXQ;LTn2Q~rau_L_ud9>vx%xy;!_ZFUU*>u9hlzu|m!3UG=PCK()_yUhwM;zq= z#^?V2;W-g^aCeVSd&C_U_YW8;bl3huv zaqzO3EHO3_pkwZTc;ftM8JXO9uqOEH_`p^u%Y}{I&1VbdB?#OoFR&C&49UeYtU$J+ zW`6r@y6w+c!lOhztMb%RbavubtzT<4QuABm zGjSW-1%bicks1-fUkH-18i*XQq`z5t? z-AfhU&+{(7-k#7H=_uNru9|Gndk#7Iv$lO67Pu!I5Fu#Mz7nxetHBR^OS;H68Y3fu zdo_9*U|m`lKjd27lF5#EM?c7VohVkb_=_&wR6v&f##(=^v31d2?ly6Drx^B}&*zP%?ua z?@%dvo=U65ibh1=`Q`f?7&U_79{P%Hei->y+j7?P51F!oJ};nU)7-`J_#h$XVP%8YC7+`VuGez)+D0FK}b8Ty-t z^TP8M>v1htlZCaJ?V{mn;2fPS9dOhNTG^NEz0K~Gw5vGNJ@x=p;corGwg$Pme7LqX zqd~{M#J%PYrjjE?)7uqI&n?%fib77y+R~_afah6PyM%~il;lH|2ih$sgmFoA_pLV= z$v&4)`KtOy_kDrO9+ZN9M|0Iq^`h2eb?JMq2H{Og;Vnwv_PTTlj}5!{AEW2}Za)>f z1B3aC_KQSO0sAA4&{&gp{+I*fyRB*3k1VkUw(k3$54_^B!c7iwK$ez+(;?3OhV#}{Mf9x2A5$v0 z0~P>szC}081?cR4W9VI%N(C5X4*{YiU~`!=`|O*k7amFQ0(s{_4d*OClS_eQpFseZ z%R2n3+licqnCz-hI81lcTNX4gXp7YWes zlpI!|u4l()oS=sdhp8E(DX9(Q0EaCb{hi>uq)H;{2Pn49Vxr87Dr1Gf;Q+DDMsWY9 zcbUjy<&5<5f&Axh+3`WoktGIRUrwb3O7&TQ@P&5PH) z^L@^RPFwbg^>P`pB!<2gD6WW7@-Jq;a~|D)9=(+OX6fOXY5#(b62|8#_XESlmJOC$MRPRu>EwnEE43xK(X+2>mwwuyp`*L zD8}5<0}L!d*tlGy)4# zK<)kNpYGVEOq|X)(Xi!dkw9`KGz=m)$jMn096rYY?3#zsY`*S4et?*t46a@|IQ{=_7}P33a=PBqX($Kc1~0UT0rOARV|fuX zKv-|2tzP>Kn(MGf^-fhg4de&xCS*M_k5nBO5`RDj$QIbK41^=OIc)Gr{yI*PMcS7}ZD5&~*Mc?Y%O3~MfU0Jz8B7$<3__uc#cU_R% zKSpW-9d+ z&2+~DZo=_Fe~ersBn(*IFp8Gb?9$uFpQvsx(vI$6gSqFlo%83DK`cmv+483U)%?yv z`F_sMk(IduZ#5rDpa)sRoXAUvMN9+1ddW+cSBw7t|j%g))b7NjNE@a*m^g5wB*$f)6p?%i)a|ROCn|sIpukrw7uzzM_ z%_ufxQ4!0s>Zs*YYtE$N0pLg@an zy*!Q)3-#;$p2IJ)Q**B+rZ3cHc8C5I1z}*y9aD`!+u#6gOuRv)!5aB%JvFtZh<;DH z9X5`bR1TrVCx{}vhJDvN-(b?l#0Gn!MI#T{u>JkMUEqDS$3UoK+Obg8z9e$hX3cSn z{xzkz1f_~w?N|cb8((B{1vAJ5s7fv}mEklm>4}9l%P+iM#1tKlD}*&Y<}$m~-c))D zBUS87;2wQZ)=8Fy@MHk9n^uLD90zAt5Ts71EpC}_EBTzrJX|&u6kBgK{mxRJ;C9#K z0B&T$UrJjXEtypy*>wsCf9BOsnJ8(yV5)Bw1G}$2Cz^5Q)xA3 zoF`&PC%|{3>j_uGNXjP8zDUL^+&q1 zD^~O&d`NxqbR{qZ;v5b|%n7_?v4~b6@Gh}jll0tTjm_eIypSK7w)t!5 zratB50{EAWwXsX$gRx%@mse6hR)rI4L92?saVWIvJBMiv#xoq_{_FtXGZNf+c<=IY zn%DY8_n+B2k6%T2V&kpLuFwhTnBWli-_wXh{pQQ}HYoR&Ui$=4FH*y{%gP-Mzv^PA z*0VY+_s2f1Y7~(M)zd5Qx*G2OUy>|taEzc)m-E;{b_G_@THWfiwrhMlgE*}1^plDW z`-dujD2eOI7v_cqqG(yvt5niNRHx6R!%vxluJhx)8P$nlF)zcw8;n=JPqdLb zhc}V#(vxPsY4)>{plXz`p>f*X@nms9?>Zxw>8SrlfA?c4bFk_G`?TYsuiaMg?{l)n P1hD#Y0ZrL|iUI!xJhl)P literal 0 HcmV?d00001 diff --git a/windows/Assets/icon.png b/windows/Assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d950b885d28ece54699cfee7f3965733a52f3efd GIT binary patch literal 20505 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 literal 0 HcmV?d00001 diff --git a/windows/ClaudeCheckerWindows.csproj b/windows/ClaudeCheckerWindows.csproj new file mode 100644 index 0000000..f7b4a47 --- /dev/null +++ b/windows/ClaudeCheckerWindows.csproj @@ -0,0 +1,32 @@ + + + + WinExe + net8.0-windows + true + enable + enable + ClaudeChecker + ClaudeCheckerWindows + Assets\icon.ico + 1.0.0 + x64 + + + + + + + + + + + + + + PreserveNewest + + + + + diff --git a/windows/Controls/GaugeControl.cs b/windows/Controls/GaugeControl.cs new file mode 100644 index 0000000..0b6a7af --- /dev/null +++ b/windows/Controls/GaugeControl.cs @@ -0,0 +1,84 @@ +using Brush = System.Windows.Media.Brush; +using Pen = System.Windows.Media.Pen; +using Point = System.Windows.Point; +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Media; + +namespace ClaudeCheckerWindows.Controls; + +public class GaugeControl : FrameworkElement +{ + public static readonly DependencyProperty PercentProperty = + DependencyProperty.Register(nameof(Percent), typeof(double), typeof(GaugeControl), + new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender)); + + public static readonly DependencyProperty AccentProperty = + DependencyProperty.Register(nameof(Accent), typeof(Brush), typeof(GaugeControl), + new FrameworkPropertyMetadata(Brushes.Orange, FrameworkPropertyMetadataOptions.AffectsRender)); + + public double Percent { get => (double)GetValue(PercentProperty); set => SetValue(PercentProperty, value); } + public Brush Accent { get => (Brush)GetValue(AccentProperty); set => SetValue(AccentProperty, value); } + + public GaugeControl() + { + ThemeManager.ThemeChanged += InvalidateVisual; + } + + private const double StartAngleDeg = 135; + private const double SweepDeg = 270; + private const double StrokeWidth = 6; + + protected override void OnRender(DrawingContext dc) + { + var cx = RenderSize.Width / 2; + var cy = RenderSize.Height / 2; + var radius = Math.Min(cx, cy) - StrokeWidth / 2 - 1; + + var trackColor = ThemeManager.IsDark + ? Color.FromArgb(40, 255, 255, 255) + : Color.FromArgb(30, 0, 0, 0); + DrawArc(dc, cx, cy, radius, StartAngleDeg, SweepDeg, + new Pen(new SolidColorBrush(trackColor), StrokeWidth)); + + if (Percent > 0) + DrawArc(dc, cx, cy, radius, StartAngleDeg, SweepDeg * (Percent / 100.0), + new Pen(Accent, StrokeWidth)); + + var dpi = VisualTreeHelper.GetDpi(this).PixelsPerDip; + var textColor = ThemeManager.IsDark ? Color.FromRgb(0xF5, 0xF5, 0xF5) : Color.FromRgb(0x1C, 0x1C, 0x1C); + + var pctText = new FormattedText( + $"{(int)Math.Round(Percent)}%", + CultureInfo.CurrentCulture, FlowDirection.LeftToRight, + new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.Bold, FontStretches.Normal), + 18, new SolidColorBrush(textColor), dpi); + + dc.DrawText(pctText, new Point(cx - pctText.Width / 2, cy - pctText.Height / 2)); + } + + private static void DrawArc(DrawingContext dc, double cx, double cy, double r, + double startDeg, double sweepDeg, Pen pen) + { + if (sweepDeg <= 0) return; + var start = PointOnCircle(cx, cy, r, startDeg); + var end = PointOnCircle(cx, cy, r, startDeg + sweepDeg); + var isLarge = sweepDeg > 180; + + var geo = new StreamGeometry(); + using (var ctx = geo.Open()) + { + ctx.BeginFigure(start, false, false); + ctx.ArcTo(end, new Size(r, r), 0, isLarge, SweepDirection.Clockwise, true, false); + } + geo.Freeze(); + dc.DrawGeometry(null, pen, geo); + } + + private static Point PointOnCircle(double cx, double cy, double r, double deg) + { + var rad = deg * Math.PI / 180; + return new Point(cx + r * Math.Cos(rad), cy + r * Math.Sin(rad)); + } +} diff --git a/windows/Controls/SparklineControl.cs b/windows/Controls/SparklineControl.cs new file mode 100644 index 0000000..f9964d0 --- /dev/null +++ b/windows/Controls/SparklineControl.cs @@ -0,0 +1,60 @@ +using Point = System.Windows.Point; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; + +namespace ClaudeCheckerWindows.Controls; + +public class SparklineControl : FrameworkElement +{ + public static readonly DependencyProperty DataProperty = + DependencyProperty.Register(nameof(Data), typeof(IList), typeof(SparklineControl), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender)); + + public static readonly DependencyProperty LineColorProperty = + DependencyProperty.Register(nameof(LineColor), typeof(Color), typeof(SparklineControl), + new FrameworkPropertyMetadata(Colors.Orange, FrameworkPropertyMetadataOptions.AffectsRender)); + + public IList? Data { get => (IList?)GetValue(DataProperty); set => SetValue(DataProperty, value); } + public Color LineColor { get => (Color)GetValue(LineColorProperty); set => SetValue(LineColorProperty, value); } + + protected override void OnRender(DrawingContext dc) + { + var data = Data; + if (data == null || data.Count < 1) return; + + var plot = data.Count == 1 ? new[] { data[0], data[0] } : [.. data]; + + var w = RenderSize.Width; + var h = RenderSize.Height; + var minV = double.MaxValue; + var maxV = double.MinValue; + foreach (var v in plot) { if (v < minV) minV = v; if (v > maxV) maxV = v; } + var range = System.Math.Max(maxV - minV, 1); + + Point Pt(int i) => new( + i / (double)(plot.Length - 1) * w, + h - (plot[i] - minV) / range * (h - 6) - 3); + + var fill = new StreamGeometry(); + using (var ctx = fill.Open()) + { + ctx.BeginFigure(new Point(0, h), true, true); + for (var i = 0; i < plot.Length; i++) ctx.LineTo(Pt(i), true, false); + ctx.LineTo(new Point(w, h), true, false); + } + fill.Freeze(); + dc.DrawGeometry(new SolidColorBrush(Color.FromArgb(26, LineColor.R, LineColor.G, LineColor.B)), null, fill); + + var line = new StreamGeometry(); + using (var ctx = line.Open()) + { + ctx.BeginFigure(Pt(0), false, false); + for (var i = 1; i < plot.Length; i++) ctx.LineTo(Pt(i), true, false); + } + line.Freeze(); + dc.DrawGeometry(null, + new System.Windows.Media.Pen(new SolidColorBrush(LineColor), 1.5) { LineJoin = PenLineJoin.Round }, + line); + } +} diff --git a/windows/GlobalUsings.cs b/windows/GlobalUsings.cs new file mode 100644 index 0000000..e181f0c --- /dev/null +++ b/windows/GlobalUsings.cs @@ -0,0 +1,9 @@ +// Global aliases to resolve ambiguities between System.Windows.* and System.Drawing.* +global using Application = System.Windows.Application; +global using Brushes = System.Windows.Media.Brushes; +global using Color = System.Windows.Media.Color; +global using FlowDirection = System.Windows.FlowDirection; +global using FontFamily = System.Windows.Media.FontFamily; +global using Orientation = System.Windows.Controls.Orientation; +global using ProgressBar = System.Windows.Controls.ProgressBar; +global using Size = System.Windows.Size; diff --git a/windows/LoginWindow.xaml b/windows/LoginWindow.xaml new file mode 100644 index 0000000..c0cb4f5 --- /dev/null +++ b/windows/LoginWindow.xaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +