16 Commits

Author SHA1 Message Date
f998d7ff64 Gosh dang it
All checks were successful
Flutter Release / get-version (push) Successful in 7s
Flutter Release / build-windows (push) Successful in 1m58s
Flutter Release / build-android (push) Successful in 9m53s
Flutter Release / create-release (push) Successful in 15s
2025-12-15 17:41:27 -05:00
3a5d2771ac Wholy V Control
All checks were successful
Flutter Release / get-version (push) Successful in 7s
Flutter Release / build-windows (push) Successful in 1m39s
Flutter Release / build-android (push) Successful in 8m19s
Flutter Release / create-release (push) Successful in 15s
2025-12-15 17:19:31 -05:00
f79b61795b Wasted 12 Mins
All checks were successful
Flutter Release / get-version (push) Successful in 6s
Flutter Release / build-windows (push) Successful in 1m58s
Flutter Release / build-android (push) Successful in 11m22s
Flutter Release / create-release (push) Successful in 15s
2025-12-15 13:51:42 -05:00
348c5ebacf Patch API URL
Some checks failed
Flutter Release / get-version (push) Successful in 6s
Flutter Release / build-windows (push) Successful in 1m58s
Flutter Release / build-android (push) Successful in 12m17s
Flutter Release / create-release (push) Failing after 16s
2025-12-15 13:38:14 -05:00
e82255d8a1 Disable cache
All checks were successful
Flutter Release / get-version (push) Successful in 10s
Flutter Release / build-windows (push) Successful in 1m58s
Flutter Release / build-android (push) Successful in 11m36s
Flutter Release / create-release (push) Successful in 30s
2025-12-15 13:21:50 -05:00
dae47fdeb9 Change upload artifcat 2025-12-15 12:47:08 -05:00
0bf330ac5b no more github actions for windows 2025-12-15 12:43:11 -05:00
435f41133a Go to stable 2025-12-15 12:41:24 -05:00
2f972536b5 FLutter v 2025-12-15 12:40:17 -05:00
7453943c37 Update app icons
Some checks failed
Flutter Release / get-version (push) Successful in 6s
Flutter Release / build-windows (push) Failing after 9s
Flutter Release / create-release (push) Has been cancelled
Flutter Release / build-android (push) Has been cancelled
2025-12-15 12:34:05 -05:00
f4bf073d52 Update SDK 2025-12-15 00:25:01 -05:00
110c5d99a1 Signing, Installer, New Workflows
Some checks failed
Flutter Release / get-version (push) Successful in 7s
Flutter Release / build-windows (push) Failing after 9s
Flutter Release / create-release (push) Has been cancelled
Flutter Release / build-android (push) Has been cancelled
2025-12-15 00:05:29 -05:00
9ff0d62651 check update
Some checks failed
Flutter Release / get-version (push) Successful in 8s
Flutter Release / build-windows (push) Failing after 9s
Flutter Release / build-android (push) Failing after 1m12s
Flutter Release / create-release (push) Has been skipped
2025-12-14 23:11:50 -05:00
e5fdbae3b2 update andriod sdk 2025-12-14 22:20:02 -05:00
2c557cdeac update dependencies
Some checks failed
Flutter Release / get-version (push) Successful in 8s
Flutter Release / build-android (push) Failing after 53s
Flutter Release / build-windows (push) Failing after 9s
Flutter Release / create-release (push) Has been skipped
2025-12-14 22:05:58 -05:00
57ccaad08a update workflows
Some checks failed
Flutter Release / get-version (push) Successful in 8s
Flutter Release / build-android (push) Failing after 1m59s
Flutter Release / build-windows (push) Failing after 10s
Flutter Release / create-release (push) Has been skipped
2025-12-14 22:02:24 -05:00
72 changed files with 3864 additions and 998 deletions

View File

@@ -31,15 +31,15 @@ This workflow automatically builds and releases rmtPocketWatcher Flutter app for
- Create a GitHub/Gitea release with both binaries - Create a GitHub/Gitea release with both binaries
- Include release notes with download instructions - Include release notes with download instructions
## 🔧 Manual Development Build ## 🔧 Manual Release Build
To trigger a manual dev build (debug versions): To trigger a manual release build:
1. Go to Actions tab in your repository 1. Go to Actions tab in your repository
2. Select "Flutter Dev Build" workflow 2. Select "Flutter Release" workflow
3. Click "Run workflow" 3. Click "Run workflow"
This will create debug builds for both Windows and Android. This will create production builds for both Windows and Android and publish a release.
## 🏗️ Local Development ## 🏗️ Local Development

View File

@@ -1,89 +0,0 @@
name: Flutter Dev Build
on:
workflow_dispatch:
jobs:
build-flutter-dev:
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
- name: Enable Windows desktop
run: flutter config --enable-windows-desktop
- name: Create dev .env file
working-directory: flutter_app
run: |
echo "WS_URL=ws://localhost:3001" > .env
echo "API_URL=http://localhost:3001" >> .env
- name: Install dependencies
working-directory: flutter_app
run: flutter pub get
- name: Run Flutter doctor
run: flutter doctor -v
- name: Build Windows debug
working-directory: flutter_app
run: flutter build windows --debug
- name: List build directory
working-directory: flutter_app
run: |
Write-Host "=== Build Directory Structure ==="
Get-ChildItem -Recurse build\windows\x64\runner\Debug | Select-Object FullName, Length
- name: Upload debug build
uses: actions/upload-artifact@v4
with:
name: rmtPocketWatcher-Windows-Debug
path: flutter_app/build/windows/x64/runner/Debug/
retention-days: 7
build-android-dev:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
- name: Create dev .env file
working-directory: flutter_app
run: |
echo "WS_URL=ws://10.0.2.2:3001" > .env
echo "API_URL=http://10.0.2.2:3001" >> .env
- name: Install dependencies
working-directory: flutter_app
run: flutter pub get
- name: Build Android debug APK
working-directory: flutter_app
run: flutter build apk --debug
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: rmtPocketWatcher-Android-Debug
path: flutter_app/build/app/outputs/flutter-apk/app-debug.apk
retention-days: 7

View File

@@ -20,6 +20,7 @@ jobs:
- name: Get version from pubspec.yaml - name: Get version from pubspec.yaml
id: version id: version
working-directory: flutter_app working-directory: flutter_app
shell: bash
run: | run: |
VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//') VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//')
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
@@ -32,42 +33,136 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Flutter - name: Verify Flutter setup
uses: subosito/flutter-action@v2 shell: powershell
with: run: |
flutter-version: '3.24.0' flutter --version
channel: 'stable' flutter doctor -v
- name: Enable Windows desktop
run: flutter config --enable-windows-desktop
- name: Create production .env file - name: Create production .env file
working-directory: flutter_app working-directory: flutter_app
shell: powershell
env: env:
WS_URL: ${{ secrets.WS_URL }} WS_URL: ${{ secrets.WS_URL }}
API_URL: ${{ secrets.API_URL }} API_URL: ${{ secrets.API_URL }}
run: | run: |
echo "WS_URL=$env:WS_URL" > .env "WS_URL=$env:WS_URL" | Out-File -FilePath .env -Encoding utf8
echo "API_URL=$env:API_URL" >> .env "API_URL=$env:API_URL" | Out-File -FilePath .env -Append -Encoding utf8
- name: Install dependencies - name: Install dependencies
working-directory: flutter_app working-directory: flutter_app
shell: powershell
run: flutter pub get run: flutter pub get
- name: Build Windows release - name: Setup Certificate for Signing
working-directory: flutter_app
run: flutter build windows --release
- name: Create Windows archive
working-directory: flutter_app working-directory: flutter_app
shell: powershell
env:
CERT_BASE64: ${{ secrets.CERT_BASE64 }}
CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}
run: | run: |
Compress-Archive -Path "build\windows\x64\runner\Release\*" -DestinationPath "rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip" if ($env:CERT_BASE64) {
Write-Host "Setting up certificate for code signing..." -ForegroundColor Green
# Create certificates directory if it doesn't exist
if (-not (Test-Path "certificates")) {
New-Item -ItemType Directory -Path "certificates" -Force | Out-Null
}
# Decode base64 certificate and save as PFX
$certBytes = [System.Convert]::FromBase64String($env:CERT_BASE64)
[System.IO.File]::WriteAllBytes("certificates\rmtPocketWatcher.pfx", $certBytes)
Write-Host "✅ Certificate installed successfully" -ForegroundColor Green
} else {
Write-Host "⚠️ No certificate provided - building unsigned" -ForegroundColor Yellow
}
- name: Upload Windows artifact - name: Build Windows release with installer
uses: actions/upload-artifact@v4 working-directory: flutter_app
shell: powershell
env:
CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}
run: |
# Set certificate password environment variable for build script
if ($env:CERT_PASSWORD) {
$env:MSIX_CERTIFICATE_PASSWORD = $env:CERT_PASSWORD
}
# Run our custom build script
.\build_windows.ps1 -Release
# The build script creates: build\rmtPocketWatcher-Windows-v{version}-release.zip
# Rename to simpler format for release
$version = "${{ needs.get-version.outputs.version }}"
# Find the generated zip and rename it
$sourceZip = "build\rmtPocketWatcher-Windows-v$version-release.zip"
if (Test-Path $sourceZip) {
Move-Item $sourceZip "rmtPocketWatcher-Windows-v$version.zip" -Force
Write-Host "Created rmtPocketWatcher-Windows-v$version.zip"
} else {
# Fallback: find any matching zip
$zipFiles = Get-ChildItem -Path "build" -Filter "rmtPocketWatcher-Windows-*.zip" -ErrorAction SilentlyContinue
if ($zipFiles) {
Move-Item $zipFiles[0].FullName "rmtPocketWatcher-Windows-v$version.zip" -Force
Write-Host "Created rmtPocketWatcher-Windows-v$version.zip from $($zipFiles[0].Name)"
}
}
# Build self-extracting portable exe (single file distribution)
Write-Host "Building self-extracting portable executable..."
.\build_sfx.ps1
# Copy SFX exe to root for upload
if (Test-Path "build\windows\sfx\rmtPocketWatcher-v$version-Portable.exe") {
Copy-Item "build\windows\sfx\rmtPocketWatcher-v$version-Portable.exe" "rmtPocketWatcher-Windows-Portable-v$version.exe" -Force
Write-Host "Created rmtPocketWatcher-Windows-Portable-v$version.exe"
}
# Copy MSIX to root for easier upload
$msixFile = Get-ChildItem -Path "build\windows\x64\runner\Release" -Filter "*.msix" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($msixFile) {
Copy-Item $msixFile.FullName "rmtPocketWatcher-Windows-v$version.msix" -Force
Write-Host "Created rmtPocketWatcher-Windows-v$version.msix"
}
# Export certificate for user installation (if certificate exists)
if (Test-Path "certificates\rmtPocketWatcher.pfx") {
Write-Host "Exporting certificate for user installation..."
try {
# Use PowerShell to export the certificate
$pfxPath = "certificates\rmtPocketWatcher.pfx"
$cerPath = "rmtPocketWatcher-Certificate.cer"
$certPassword = if ($env:CERT_PASSWORD) { $env:CERT_PASSWORD } else { "rmtPocketWatcher2024!" }
# Load PFX and export public certificate
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($pfxPath, $certPassword)
$certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
[System.IO.File]::WriteAllBytes($cerPath, $certBytes)
Write-Host "✅ Certificate exported: $cerPath" -ForegroundColor Green
} catch {
Write-Warning "Failed to export certificate: $($_.Exception.Message)"
}
}
# List created artifacts
Write-Host "Artifacts created:"
Get-ChildItem -Filter "*.zip" | ForEach-Object { Write-Host " - $($_.Name)" }
Get-ChildItem -Filter "*Portable*.exe" | ForEach-Object { Write-Host " - $($_.Name)" }
Get-ChildItem -Filter "*.msix" | ForEach-Object { Write-Host " - $($_.Name)" }
Get-ChildItem -Filter "*.cer" | ForEach-Object { Write-Host " - $($_.Name)" }
- name: Upload Windows artifacts
uses: actions/upload-artifact@v3
with: with:
name: rmtPocketWatcher-Windows name: rmtPocketWatcher-Windows
path: flutter_app/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip path: |
flutter_app/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip
flutter_app/rmtPocketWatcher-Windows-Portable-v${{ needs.get-version.outputs.version }}.exe
flutter_app/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.msix
flutter_app/rmtPocketWatcher-Certificate.cer
retention-days: 30 retention-days: 30
build-android: build-android:
@@ -82,37 +177,54 @@ jobs:
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: '3.24.0'
channel: 'stable' channel: 'stable'
cache: false
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Accept Android licenses
shell: bash
run: yes | sdkmanager --licenses || true
- name: Verify Flutter setup
shell: bash
run: flutter doctor -v
- name: Create production .env file - name: Create production .env file
working-directory: flutter_app working-directory: flutter_app
shell: bash
env: env:
WS_URL: ${{ secrets.WS_URL }} WS_URL: ${{ secrets.WS_URL }}
API_URL: ${{ secrets.API_URL }} API_URL: ${{ secrets.API_URL }}
run: | run: |
echo "WS_URL=$WS_URL" > .env echo "WS_URL=$WS_URL" > .env
echo "API_URL=$API_URL" >> .env echo "API_URL=$API_URL" >> .env
echo "Created .env file:"
cat .env | sed 's/=.*/=***/' # Show keys but mask values
- name: Install dependencies - name: Install dependencies
working-directory: flutter_app working-directory: flutter_app
shell: bash
run: flutter pub get run: flutter pub get
- name: Build Android APK - name: Build Android APK
working-directory: flutter_app working-directory: flutter_app
run: flutter build apk --release shell: bash
run: flutter build apk --release --verbose
- name: Rename APK - name: Rename APK
working-directory: flutter_app working-directory: flutter_app
shell: bash
run: | run: |
mv build/app/outputs/flutter-apk/app-release.apk rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk cp build/app/outputs/flutter-apk/app-release.apk rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk
- name: Upload Android artifact - name: Upload Android artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: rmtPocketWatcher-Android name: rmtPocketWatcher-Android
path: flutter_app/rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk path: flutter_app/rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk
@@ -126,13 +238,13 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Download Windows artifact - name: Download Windows artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: rmtPocketWatcher-Windows name: rmtPocketWatcher-Windows
path: ./artifacts path: ./artifacts
- name: Download Android artifact - name: Download Android artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: rmtPocketWatcher-Android name: rmtPocketWatcher-Android
path: ./artifacts path: ./artifacts
@@ -150,7 +262,10 @@ jobs:
**Lambda Banking Conglomerate** - Star Citizen AUEC Price Tracker **Lambda Banking Conglomerate** - Star Citizen AUEC Price Tracker
### Downloads ### Downloads
- **Windows**: `rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip` - **Windows (Full)**: `rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip` - Complete standalone package
- **Windows (Portable)**: `rmtPocketWatcher-Windows-Portable-v${{ needs.get-version.outputs.version }}.exe` - Single self-extracting executable
- **Windows (Installer)**: `rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.msix` - Windows Store-style installer
- **Certificate**: `rmtPocketWatcher-Certificate.cer` - Required for signed executables (see installation notes)
- **Android**: `rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk` - **Android**: `rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk`
### Features ### Features
@@ -162,13 +277,28 @@ jobs:
- Vendor comparison tables - Vendor comparison tables
### Installation ### Installation
**Windows**: Extract the ZIP file and run `rmtpocketwatcher.exe`
#### Certificate Installation (Required for signed executables)
If Windows shows "Unknown publisher" warnings, install the certificate first:
1. Download `rmtPocketWatcher-Certificate.cer`
2. Right-click → "Install Certificate"
3. Choose "Local Machine" → "Place all certificates in the following store"
4. Browse → Select "Trusted Root Certification Authorities" → OK
5. Complete the installation
#### Application Installation
**Windows (Full)**: Extract the ZIP file and run `rmtpocketwatcher.exe` - includes all dependencies
**Windows (Portable)**: Just run the .exe - auto-extracts to AppData and launches
**Windows (Installer)**: Double-click the MSIX file for Windows Store-style installation
**Android**: Install the APK file (enable "Install from unknown sources") **Android**: Install the APK file (enable "Install from unknown sources")
--- ---
*Built with Flutter for cross-platform compatibility* *Built with Flutter for cross-platform compatibility*
files: | files: |
./artifacts/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip ./artifacts/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip
./artifacts/rmtPocketWatcher-Windows-Portable-v${{ needs.get-version.outputs.version }}.exe
./artifacts/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.msix
./artifacts/rmtPocketWatcher-Certificate.cer
./artifacts/rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk ./artifacts/rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,129 +0,0 @@
# Flutter Migration Guide
## Migration from Electron to Flutter
The rmtPocketWatcher application has been successfully migrated from Electron + React to Flutter for true cross-platform support.
### What Changed
**Before (Electron):**
- Electron + React + TypeScript
- Windows desktop only
- ~100MB bundle size
- WebView-based rendering
**After (Flutter):**
- Flutter + Dart
- Windows, macOS, Linux, Android, iOS support
- ~15-20MB bundle size
- Native rendering
### Project Structure
```
flutter_app/
├── lib/
│ ├── main.dart # App entry point
│ ├── models/
│ │ └── price_data.dart # Data models (PriceData, LatestPrice, PriceAlert)
│ ├── providers/
│ │ └── price_provider.dart # State management with Provider
│ ├── screens/
│ │ └── home_screen.dart # Main dashboard screen
│ ├── services/
│ │ ├── api_service.dart # REST API communication
│ │ ├── websocket_service.dart # WebSocket connection
│ │ └── storage_service.dart # Local storage (SharedPreferences)
│ └── widgets/
│ ├── alerts_panel.dart # Price alerts UI
│ ├── price_chart.dart # Historical price chart
│ ├── price_stats_card.dart # Stats display cards
│ └── vendor_table.dart # Vendor comparison table
├── assets/ # Images, fonts, etc.
├── .env # Environment configuration
└── pubspec.yaml # Dependencies
```
### Key Features Preserved
**Real-time price tracking** - WebSocket connection to backend
**Bloomberg-style dashboard** - Stats cards, charts, vendor table
**Price alerts** - Client-side evaluation with notifications
**Historical charts** - Interactive price history with fl_chart
**Vendor comparison** - Sortable table of all sellers
**Auto-reconnect** - WebSocket reconnection logic
**Local storage** - Alerts and settings persistence
### New Capabilities
🆕 **Mobile support** - Same app runs on Android/iOS
🆕 **Better performance** - Native rendering, smaller memory footprint
🆕 **Cross-platform** - Windows, macOS, Linux from same codebase
🆕 **Material Design 3** - Modern, consistent UI across platforms
🆕 **Responsive layout** - Adapts to different screen sizes
### Backend Compatibility
The Flutter app is **100% compatible** with the existing TypeScript backend:
- Same REST API endpoints (`/prices/latest`, `/index/history`)
- Same WebSocket protocol (`/ws/index`)
- Same data formats (JSON)
- No backend changes required
### Development Commands
```bash
# Install dependencies
flutter pub get
# Run on different platforms
flutter run -d windows # Windows desktop
flutter run -d android # Android device/emulator
flutter run -d ios # iOS device/simulator
# Build releases
flutter build windows # Windows executable
flutter build apk # Android APK
flutter build ipa # iOS app bundle
# Development tools
flutter doctor # Check setup
flutter devices # List available devices
flutter logs # View app logs
```
### Environment Configuration
The app uses `.env` file for configuration:
```env
API_URL=http://localhost:3000
WS_URL=ws://localhost:3000/ws/index
```
### Mobile Adaptations
The UI automatically adapts for mobile:
- **Desktop**: Multi-column layout with side-by-side panels
- **Mobile**: Single-column layout with scrollable sections
- **Responsive**: Charts and tables adjust to screen size
- **Touch-friendly**: Larger tap targets on mobile
### Migration Benefits
1. **Cross-platform**: One codebase for all platforms
2. **Performance**: 50-80% smaller memory usage
3. **Native feel**: Platform-specific UI elements
4. **Future-proof**: Mobile support for user growth
5. **Maintenance**: Single codebase to maintain
6. **Distribution**: App stores + direct downloads
### Next Steps
1. **Test the Flutter app**: `flutter run -d windows`
2. **Verify backend connection**: Ensure backend is running
3. **Test on mobile**: Run on Android/iOS devices
4. **Build releases**: Create platform-specific builds
5. **Update deployment**: Replace Electron builds with Flutter
The migration preserves all functionality while adding mobile support and improving performance. The backend remains unchanged, ensuring a smooth transition.

View File

@@ -1,178 +0,0 @@
# Flutter Migration Complete ✅
## Migration Summary
Successfully migrated rmtPocketWatcher from Electron + React to Flutter for true cross-platform support.
### ✅ What's Been Completed
**Core Application Structure:**
- ✅ Flutter project created with proper dependencies
- ✅ Material Design 3 theme with Bloomberg-style dark colors
- ✅ Provider state management setup
- ✅ Cross-platform compatibility (Windows, macOS, Linux, Android, iOS)
**Data Layer:**
-`PriceData`, `LatestPrice`, `HistoryData`, `PriceAlert` models
- ✅ WebSocket service with auto-reconnect logic
- ✅ REST API service for initial data fetching
- ✅ Local storage service using SharedPreferences
**UI Components:**
-`HomeScreen` - Main dashboard layout
-`PriceStatsCard` - Connection status, lowest price, seller info
-`AlertsPanel` - Add/manage price alerts with enable/disable
-`PriceChart` - Interactive historical price chart with fl_chart
-`VendorTable` - Sortable vendor comparison table
**Features Preserved:**
- ✅ Real-time WebSocket price updates
- ✅ Historical price charts with time range selection
- ✅ Client-side price alerts with local storage
- ✅ Vendor comparison table with sorting
- ✅ Bloomberg-style terminal interface
- ✅ Auto-reconnect WebSocket logic
**Platform Support:**
- ✅ Windows desktop (requires Visual Studio C++ components)
- ✅ Web browser (for testing without C++ setup)
- ✅ Ready for macOS, Linux, Android, iOS
### 🔧 Setup Requirements
**For Windows Desktop Development:**
1. Install Visual Studio 2022 Community
2. Add "Desktop development with C++" workload
3. Include these components:
- MSVC v143 - VS 2022 C++ x64/x86 build tools (Latest)
- Windows 11 SDK (10.0.22621.0)
- CMake tools for Visual Studio
**Quick Commands:**
```bash
cd flutter_app
# Install dependencies
flutter pub get
# Check setup
flutter doctor
# Run on Windows (after VS setup)
flutter run -d windows
# Run on web (works without C++ components)
flutter run -d chrome
# Build for Windows
flutter build windows
```
### 🎯 Next Steps
**Immediate (Ready to Use):**
1. **Fix Visual Studio Components** - Install C++ workload for Windows builds
2. **Test Backend Connection** - Ensure TypeScript backend is running on port 3000
3. **Run Flutter App** - `flutter run -d windows` or `flutter run -d chrome`
4. **Verify Features** - Test WebSocket, alerts, charts, vendor table
**Short Term:**
1. **Mobile Testing** - Test responsive layout on Android/iOS
2. **Performance Optimization** - Test with 50k+ chart datapoints
3. **Notifications** - Implement native notifications for alerts
4. **Window Controls** - Add minimize/close functionality for desktop
**Long Term:**
1. **App Store Distribution** - Prepare for Google Play, App Store
2. **Auto-Updates** - Implement update mechanism
3. **Advanced Charts** - Add more chart types and interactions
4. **Settings Panel** - Theme, notification preferences
### 📱 Mobile Support
The Flutter app automatically supports mobile with responsive design:
**Desktop Layout:**
- Multi-column stats cards
- Side-by-side panels
- Full-width charts and tables
**Mobile Layout:**
- Single-column scrollable layout
- Stacked stats cards
- Touch-friendly controls
- Responsive charts
### 🔄 Backend Compatibility
**100% Compatible** with existing TypeScript backend:
- Same REST endpoints: `/prices/latest`, `/index/history`
- Same WebSocket protocol: `/ws/index`
- Same JSON data formats
- No backend changes required
### 📊 Performance Benefits
**Flutter vs Electron:**
- **Bundle Size**: ~15MB vs ~100MB (85% smaller)
- **Memory Usage**: ~50MB vs ~150MB (67% less)
- **Startup Time**: ~2s vs ~5s (60% faster)
- **Native Performance**: 60fps rendering vs WebView overhead
### 🚀 Deployment Options
**Desktop:**
- Windows: `.exe` installer
- macOS: `.dmg` or `.pkg`
- Linux: `.deb`, `.rpm`, or AppImage
**Mobile:**
- Android: `.apk` or Google Play Store
- iOS: App Store (requires Apple Developer account)
**Web:**
- Progressive Web App (PWA)
- Direct hosting on any web server
### 🛠️ Development Workflow
```bash
# Development
flutter run -d windows # Hot reload development
flutter run -d android # Test mobile layout
flutter run -d chrome # Web testing
# Building
flutter build windows --release # Production Windows build
flutter build apk --release # Production Android build
flutter build web --release # Production web build
# Testing
flutter test # Unit tests
flutter drive --target=test_driver/app.dart # Integration tests
```
### 📋 Migration Checklist
- [x] Flutter project structure created
- [x] All Electron features migrated
- [x] Cross-platform compatibility added
- [x] Backend integration preserved
- [x] UI/UX maintained (Bloomberg style)
- [x] State management implemented
- [x] Local storage working
- [x] WebSocket auto-reconnect
- [x] Responsive design for mobile
- [x] Documentation completed
### 🎉 Success Metrics
The Flutter migration delivers:
- **5 platforms** from 1 codebase (vs 1 platform)
- **85% smaller** bundle size
- **67% less** memory usage
- **Mobile support** for future growth
- **Native performance** across all platforms
- **Maintained feature parity** with Electron version
The rmtPocketWatcher Flutter app is ready for production use and provides a solid foundation for cross-platform expansion!

171
SETUP.md
View File

@@ -1,171 +0,0 @@
# rmtPocketWatcher Setup Guide
## What's Been Created
**Backend Scraper Service**
- Playwright-based scrapers for Eldorado and PlayerAuctions
- Automatic retry logic and error handling
- Scheduled scraping every 5 minutes
- Tracks all seller listings with platform, price, and delivery time
**Database Layer**
- PostgreSQL with Prisma ORM
- Three tables: VendorPrice, PriceIndex, ScrapeLog
- Stores all historical listings for trend analysis
- Indexed for fast queries
**API Layer**
- Fastify REST API with 6 endpoints
- WebSocket for real-time updates
- Filter by seller, platform, date range
- Historical price data
**Docker Setup**
- Docker Compose orchestration
- PostgreSQL container with health checks
- Backend container with auto-migration
- Volume persistence for database
## Current Status
The Docker Compose stack is building. This will:
1. Pull PostgreSQL 16 Alpine image
2. Build the backend Node.js container
3. Install Playwright and dependencies
4. Generate Prisma client
5. Start both services
## Once Build Completes
### Check Status
```bash
# View logs
docker-compose logs -f backend
# Check if services are running
docker ps
```
### Test the API
```bash
# Health check
curl http://localhost:3000/health
# Get latest prices (after first scrape)
curl http://localhost:3000/api/prices/latest
# Get lowest price
curl http://localhost:3000/api/prices/lowest
# Get price history
curl "http://localhost:3000/api/index/history?range=7d"
```
### Monitor Scraping
The backend will automatically:
- Scrape Eldorado and PlayerAuctions every 5 minutes
- Save all listings to the database
- Calculate and store the lowest price
- Log all scrape attempts
Check logs to see scraping activity:
```bash
docker-compose logs -f backend | grep "scraping"
```
## Database Access
### Using Prisma Studio
```bash
cd backend
npm run db:studio
```
### Using psql
```bash
docker exec -it rmtpw-postgres psql -U rmtpw -d rmtpocketwatcher
```
## Stopping and Restarting
```bash
# Stop services
docker-compose down
# Start services
docker-compose up -d
# Rebuild after code changes
docker-compose up --build -d
# View logs
docker-compose logs -f
# Remove everything including data
docker-compose down -v
```
## Environment Variables
Edit `.env` or `docker-compose.yml` to configure:
- `SCRAPE_INTERVAL_MINUTES` - How often to scrape (default: 5)
- `SCRAPER_HEADLESS` - Run browser in headless mode (default: true)
- `PORT` - API server port (default: 3000)
- `DATABASE_URL` - PostgreSQL connection string
## Next Steps
1. **Wait for build to complete** (~2-5 minutes)
2. **Verify services are running**: `docker ps`
3. **Check first scrape**: `docker-compose logs -f backend`
4. **Test API endpoints**: See examples above
5. **Build Electron frontend** (coming next)
## Troubleshooting
### Backend won't start
```bash
# Check logs
docker-compose logs backend
# Restart backend
docker-compose restart backend
```
### Database connection issues
```bash
# Check postgres is healthy
docker-compose ps
# Restart postgres
docker-compose restart postgres
```
### Scraper errors
```bash
# View detailed logs
docker-compose logs -f backend | grep -A 5 "error"
# Check scrape log in database
docker exec -it rmtpw-postgres psql -U rmtpw -d rmtpocketwatcher -c "SELECT * FROM scrape_log ORDER BY timestamp DESC LIMIT 10;"
```
## File Structure
```
rmtPocketWatcher/
├── docker-compose.yml # Docker orchestration
├── .env # Environment variables
├── backend/
│ ├── Dockerfile # Backend container definition
│ ├── prisma/
│ │ └── schema.prisma # Database schema
│ ├── src/
│ │ ├── scrapers/ # Scraping logic
│ │ ├── api/ # REST + WebSocket API
│ │ ├── database/ # Prisma client & repository
│ │ └── index.ts # Main server
│ └── package.json
└── README.md
```

View File

@@ -206,4 +206,12 @@ ios/Flutter/flutter_export_environment.sh
**/GeneratedPluginRegistrant.swift **/GeneratedPluginRegistrant.swift
**/generated_plugin_registrant.cc **/generated_plugin_registrant.cc
**/generated_plugin_registrant.h **/generated_plugin_registrant.h
**/generated_plugins.cmake **/generated_plugins.cmake
# Code signing certificates (NEVER commit private keys!)
certificates/*.pfx
certificates/*.p12
certificates/*.key
# Public certificates can be committed if needed
# certificates/*.cer
# certificates/*.crt

View File

@@ -0,0 +1,126 @@
# rmtPocketWatcher - Windows Build Instructions
This document explains how to build rmtPocketWatcher for Windows distribution.
## Prerequisites
- Flutter SDK 3.22.3 or later
- Windows 10/11 with Visual Studio Build Tools
- PowerShell (for advanced build script)
## Quick Build (Batch Script)
For a simple build process, use the batch script:
```cmd
build_windows.bat
```
This will create:
- `build\windows\standalone\rmtpocketwatcher.exe` - Standalone executable with all dependencies
- `build\rmtPocketWatcher-Windows-Standalone.zip` - Distribution archive
## Advanced Build (PowerShell Script)
For more control and MSIX installer creation:
```powershell
# Release build (recommended for distribution)
.\build_windows.ps1 -Release
# Debug build (for development)
.\build_windows.ps1 -Debug
```
This creates:
- **Standalone Package**: Complete folder with all dependencies
- **Portable Executable**: Single-file distribution (if possible)
- **MSIX Installer**: Windows Store-style installer
- **Distribution Archive**: ZIP file ready for sharing
## Output Files
After building, you'll find:
### Standalone Distribution
- `build\windows\standalone\` - Complete application folder
- `build\windows\standalone\rmtpocketwatcher.exe` - Main executable
- `build\windows\standalone\data\` - Flutter engine and assets
- `build\windows\standalone\VERSION.txt` - Version information
### Distribution Archives
- `build\rmtPocketWatcher-Windows-v{version}.zip` - Full standalone package
- `build\rmtPocketWatcher-Windows-Portable-v{version}.zip` - Portable version (exe only)
### Installer (if created)
- `build\windows\x64\runner\Release\*.msix` - Windows installer package
## Distribution Options
### Option 1: Standalone ZIP (Recommended)
- Share the `rmtPocketWatcher-Windows-v{version}.zip` file
- Users extract and run `rmtpocketwatcher.exe`
- No installation required
- All dependencies included
### Option 2: Portable Executable
- Share the `rmtPocketWatcher-Windows-Portable-v{version}.zip` file
- Contains only the executable
- Smallest download size
- May require Visual C++ Redistributable on target machine
### Option 3: MSIX Installer
- Share the `.msix` file
- Windows Store-style installation
- Automatic updates support
- Requires Windows 10 version 1809 or later
## Testing
To test the standalone build:
```cmd
cd build\windows\standalone
rmtpocketwatcher.exe
```
## Troubleshooting
### Build Fails
- Ensure Flutter is properly installed: `flutter doctor`
- Check Windows development setup: `flutter doctor -v`
- Clean and retry: `flutter clean && flutter pub get`
### MSIX Creation Fails
- MSIX creation is optional and may fail on some systems
- The standalone executable will still be created
- Install Windows SDK if you need MSIX support
### Runtime Issues
- Ensure the `.env` file is present in the build directory
- Check that all assets are included in the `data` folder
- Verify Visual C++ Redistributable is installed on target machine
## CI/CD Integration
The build scripts are designed to work with the GitHub Actions workflow in `.gitea/workflows/release.yml`. The workflow automatically:
1. Builds both standalone and portable versions
2. Creates MSIX installer (if possible)
3. Packages everything for release
4. Uploads artifacts to the release
## Manual Flutter Commands
If you prefer manual control:
```cmd
# Basic build
flutter build windows --release
# Create MSIX (requires msix package)
flutter pub get
flutter pub run msix:create
```
Note: Manual builds won't include the packaging and organization provided by the build scripts.

View File

@@ -0,0 +1,236 @@
# Code Signing Certificate Guide for rmtPocketWatcher
This guide explains how to create and use code signing certificates for your Windows application to eliminate security warnings and build user trust.
## Quick Start
1. **Create Certificate**: `.\create_certificate.ps1`
2. **Build & Sign**: `.\build_windows.ps1 -Release`
3. **Distribute**: Share the signed executables and optionally the public certificate
## Certificate Types
### Self-Signed Certificates (Free)
- **Cost**: Free
- **Trust Level**: Low (requires manual installation)
- **Best For**: Development, internal distribution, open source projects
- **Limitations**: Users see "Unknown Publisher" warnings initially
### Commercial Certificates ($100-$500/year)
- **Cost**: $100-$500 annually
- **Trust Level**: High (automatically trusted)
- **Best For**: Commercial software, wide distribution
- **Providers**: DigiCert, Sectigo, GlobalSign, Entrust
## Self-Signed Certificate Setup
### Step 1: Create Certificate
```powershell
.\create_certificate.ps1
```
This creates:
- `certificates/rmtPocketWatcher.pfx` - Private certificate (keep secure!)
- `certificates/rmtPocketWatcher.cer` - Public certificate (for distribution)
- `certificates/CERTIFICATE_INFO.txt` - Certificate details
### Step 2: Build with Signing
```powershell
.\build_windows.ps1 -Release
```
This automatically:
- Builds the application
- Signs the executable with your certificate
- Creates signed MSIX installer
- Packages everything for distribution
### Step 3: Manual Signing (if needed)
```powershell
.\sign_executable.ps1 -ExePath "path\to\your\app.exe"
```
## Certificate Installation for Users
### Automatic Installation (Recommended)
When users run your signed app for the first time:
1. Windows shows "Unknown Publisher" warning
2. User clicks "More info" → "Run anyway"
3. Certificate is automatically added to their trusted store
### Manual Installation (Optional)
For organizations or power users:
1. Distribute the `.cer` file alongside your app
2. Users double-click the `.cer` file
3. Click "Install Certificate"
4. Choose "Local Machine" (requires admin) or "Current User"
5. Select "Trusted Root Certification Authorities"
6. Click "Next" and "Finish"
## Commercial Certificate Setup
### Step 1: Purchase Certificate
Popular providers:
- **DigiCert**: $474/year (EV), $239/year (OV)
- **Sectigo**: $199/year (EV), $85/year (OV)
- **GlobalSign**: $249/year (EV), $127/year (OV)
### Step 2: Certificate Validation
- **Organization Validation (OV)**: Business verification (1-3 days)
- **Extended Validation (EV)**: Enhanced verification (1-5 days)
- **Individual**: Personal ID verification
### Step 3: Install Certificate
1. Download certificate from provider
2. Install to Windows Certificate Store
3. Update build scripts with certificate details
### Step 4: Update Configuration
```yaml
# pubspec.yaml
msix_config:
certificate_path: path/to/commercial/cert.pfx
certificate_password: your_secure_password
```
## Security Best Practices
### Certificate Storage
- **Never commit** `.pfx` or `.p12` files to version control
- Store certificates in secure, encrypted locations
- Use strong passwords for certificate files
- Backup certificates securely
### Password Management
- Use strong, unique passwords for certificates
- Store passwords in secure password managers
- Use environment variables in CI/CD pipelines
- Rotate passwords regularly
### CI/CD Integration
```yaml
# GitHub Actions example
- name: Setup Certificate
run: |
echo "${{ secrets.CERT_BASE64 }}" | base64 -d > cert.pfx
- name: Sign Application
run: |
signtool sign /f cert.pfx /p "${{ secrets.CERT_PASSWORD }}" app.exe
```
## Troubleshooting
### "SignTool not found"
**Solution**: Install Windows SDK
- Download from: https://developer.microsoft.com/windows/downloads/windows-sdk/
- Or install Visual Studio with Windows development tools
### "Certificate not valid for code signing"
**Solution**: Ensure certificate has "Code Signing" usage
```powershell
# Check certificate usage
Get-PfxCertificate -FilePath cert.pfx | Select-Object -ExpandProperty Extensions
```
### "Timestamp server unavailable"
**Solution**: Try different timestamp servers
- http://timestamp.digicert.com
- http://timestamp.sectigo.com
- http://timestamp.globalsign.com
### "Access denied" when signing
**Solution**: Run PowerShell as Administrator
```powershell
# Check if running as admin
([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
```
### Users still see warnings
**Possible causes**:
1. Certificate not properly installed
2. Certificate expired
3. System clock incorrect
4. Antivirus interference
## Certificate Lifecycle
### Monitoring Expiration
```powershell
# Check certificate expiration
$cert = Get-PfxCertificate -FilePath "certificates/rmtPocketWatcher.pfx"
$daysUntilExpiry = ($cert.NotAfter - (Get-Date)).Days
Write-Host "Certificate expires in $daysUntilExpiry days"
```
### Renewal Process
1. **Self-signed**: Run `.\create_certificate.ps1 -Force`
2. **Commercial**: Renew through certificate provider
3. Update build scripts with new certificate
4. Re-sign and redistribute applications
### Migration to Commercial
1. Purchase commercial certificate
2. Update `pubspec.yaml` configuration
3. Update build scripts
4. Re-sign all distributed applications
5. Notify users of the change
## Cost-Benefit Analysis
### Self-Signed Certificates
**Pros**:
- Free
- Full control
- Good for development/testing
- Suitable for open source projects
**Cons**:
- Users see warnings initially
- Requires user education
- Not suitable for commercial distribution
### Commercial Certificates
**Pros**:
- Immediate trust
- Professional appearance
- Better user experience
- Required for some distribution channels
**Cons**:
- Annual cost
- Validation process
- Vendor dependency
## Recommendations
### For Development/Testing
- Use self-signed certificates
- Document installation process for users
- Consider upgrading for production releases
### For Commercial Distribution
- Invest in commercial certificates
- Choose reputable certificate authorities
- Plan for certificate lifecycle management
### For Open Source Projects
- Start with self-signed certificates
- Consider community funding for commercial certificates
- Provide clear installation instructions
## Scripts Reference
| Script | Purpose | Usage |
|--------|---------|-------|
| `create_certificate.ps1` | Create self-signed certificate | `.\create_certificate.ps1` |
| `sign_executable.ps1` | Sign individual executables | `.\sign_executable.ps1 -ExePath app.exe` |
| `build_windows.ps1` | Build and sign complete package | `.\build_windows.ps1 -Release` |
## Support
For certificate-related issues:
1. Check Windows Event Viewer for detailed errors
2. Verify certificate validity and usage
3. Test on clean Windows installation
4. Consult certificate provider documentation

View File

@@ -0,0 +1,149 @@
# CI/CD Certificate Setup Guide
This guide explains how to set up code signing certificates for automated builds in your CI/CD pipeline.
## Prerequisites
1. **Certificate Created**: Run `.\create_certificate.ps1` to create your certificate
2. **Local Testing**: Verify signing works locally with `.\build_windows.ps1 -Release`
## Step 1: Encode Certificate
Run the encoding script to prepare your certificate for CI/CD:
```powershell
.\encode_certificate.ps1
```
This creates `certificate_base64.txt` containing your certificate encoded as base64.
## Step 2: Add Action Secrets
### For Gitea Actions:
1. Go to your repository settings
2. Navigate to "Secrets and Variables" → "Actions"
3. Add these secrets:
| Secret Name | Value | Description |
|-------------|-------|-------------|
| `CERT_BASE64` | Contents of `certificate_base64.txt` | Base64 encoded certificate |
| `CERT_PASSWORD` | `rmtPocketWatcher2024!` | Certificate password |
### For GitHub Actions:
1. Go to repository "Settings" → "Secrets and variables" → "Actions"
2. Click "New repository secret"
3. Add the same secrets as above
## Step 3: Security Cleanup
**IMPORTANT**: After adding the secrets, delete the local files:
```powershell
Remove-Item certificate_base64.txt -Force
```
## Step 4: Verify Setup
1. Push a change to trigger the workflow
2. Check the build logs for:
- "✅ Certificate installed successfully"
- "✅ Executable signed successfully"
- "✅ MSIX installer signed successfully"
## How It Works
### Certificate Installation
The workflow automatically:
1. Decodes the base64 certificate
2. Saves it as `certificates/rmtPocketWatcher.pfx`
3. Uses it for signing during the build process
### Signing Process
The build script signs:
- **Standalone executable**: `rmtpocketwatcher.exe`
- **MSIX installer**: `*.msix` file
### Environment Variables
- `CERT_PASSWORD`: Used by signing scripts
- `MSIX_CERTIFICATE_PASSWORD`: Used by MSIX creation
## Troubleshooting
### "Certificate not found" Error
- Verify `CERT_BASE64` secret is set correctly
- Check the base64 encoding is complete (no line breaks)
### "Invalid certificate password" Error
- Verify `CERT_PASSWORD` secret matches your certificate password
- Default password is `rmtPocketWatcher2024!`
### "SignTool not found" Error
- This should not occur in GitHub/Gitea runners
- Windows runners include Windows SDK by default
### Unsigned Executables
- Check workflow logs for certificate setup messages
- Verify both secrets are set correctly
- Ensure certificate is valid and not expired
## Security Best Practices
### Certificate Protection
- Never commit `.pfx` files to version control
- Use repository secrets for sensitive data
- Regularly rotate certificate passwords
### Access Control
- Limit repository access to trusted contributors
- Use branch protection rules
- Require reviews for workflow changes
### Monitoring
- Monitor build logs for signing failures
- Set up notifications for failed builds
- Regularly verify certificate expiration dates
## Commercial Certificate Migration
When upgrading to a commercial certificate:
1. **Obtain Certificate**: Purchase from DigiCert, Sectigo, etc.
2. **Update Secrets**: Replace `CERT_BASE64` with new certificate
3. **Update Password**: Change `CERT_PASSWORD` if different
4. **Test Build**: Verify signing works with new certificate
## Certificate Lifecycle
### Monitoring Expiration
Add this to your workflow to check certificate expiration:
```yaml
- name: Check Certificate Expiration
shell: pwsh
run: |
if (Test-Path "certificates\rmtPocketWatcher.pfx") {
$cert = Get-PfxCertificate -FilePath "certificates\rmtPocketWatcher.pfx"
$daysUntilExpiry = ($cert.NotAfter - (Get-Date)).Days
Write-Host "Certificate expires in $daysUntilExpiry days"
if ($daysUntilExpiry -lt 30) {
Write-Warning "Certificate expires soon!"
}
}
```
### Renewal Process
1. Create new certificate (self-signed or commercial)
2. Encode with `.\encode_certificate.ps1`
3. Update `CERT_BASE64` secret
4. Update `CERT_PASSWORD` if changed
5. Test with a new build
## Support
For issues with certificate setup:
1. Check workflow logs for detailed error messages
2. Verify certificate validity locally first
3. Test encoding/decoding process manually
4. Consult the main Certificate Guide for certificate creation issues

View File

View File

@@ -0,0 +1,204 @@
# rmtPocketWatcher - Complete Deployment Summary
## 🎉 System Overview
Your rmtPocketWatcher Flutter application now has a complete, professional deployment system with:
**Self-Signed Code Signing Certificate**
**Signed Standalone Executable**
**Signed MSIX Installer**
**Automated Build & Signing Pipeline**
**RSS-Based Update System**
**CI/CD Integration**
## 📁 Generated Files
### Certificates (Keep Secure!)
- `certificates/rmtPocketWatcher.pfx` - Private certificate (password: `rmtPocketWatcher2024!`)
- `certificates/rmtPocketWatcher.cer` - Public certificate for user installation
- `certificates/CERTIFICATE_INFO.txt` - Certificate details and instructions
### Distribution Files
- `build/windows/standalone/rmtpocketwatcher.exe` - **Signed standalone executable**
- `build/windows/x64/runner/Release/rmtpocketwatcher.msix` - **Signed MSIX installer**
- `build/rmtPocketWatcher-Windows-v1.0.1-release.zip` - Complete distribution package
## 🚀 Quick Start Commands
### Create Certificate (One-time setup)
```powershell
.\create_certificate.ps1
```
### Build & Sign Everything
```powershell
.\build_windows.ps1 -Release
```
### Sign Individual Files
```powershell
.\sign_executable.ps1 -ExePath "path\to\app.exe"
```
## 📦 Distribution Options
### Option 1: Standalone ZIP (Recommended)
**File**: `rmtPocketWatcher-Windows-v1.0.1-release.zip`
- **Size**: ~50-100MB
- **User Experience**: Extract and run - no installation needed
- **Trust Level**: Signed executable reduces Windows warnings
- **Best For**: General distribution, users without admin rights
### Option 2: MSIX Installer
**File**: `rmtpocketwatcher.msix`
- **Size**: ~30-60MB
- **User Experience**: Double-click to install via Windows Package Manager
- **Trust Level**: Signed installer, clean install/uninstall
- **Best For**: Users who prefer traditional installation, enterprise deployment
### Option 3: Public Certificate Distribution
**File**: `rmtPocketWatcher.cer`
- **Size**: ~2KB
- **Purpose**: Pre-install certificate for enhanced trust
- **Best For**: Organizations, power users, eliminating all warnings
## 🔒 Security Features
### Code Signing Benefits
-**Eliminates "Unknown Publisher" warnings**
-**Verifies file integrity** (detects tampering)
-**Establishes publisher identity**
-**Enables Windows SmartScreen trust**
-**Professional appearance**
### Certificate Details
- **Subject**: Lambda Banking Conglomerate
- **Valid**: 3 years (until December 2028)
- **Algorithm**: SHA256 with RSA encryption
- **Timestamp**: DigiCert timestamp server (ensures validity even after cert expires)
## 🔄 Update System
### Automatic Updates
- Checks RSS feed every 4 hours: `https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher/releases.rss`
- Shows notification banner when updates available
- Users can manually check via title bar button
- Supports multiple download formats (Portable, Full, MSIX)
### Version Management
- Current version: `1.0.1` (from pubspec.yaml)
- Semantic versioning: MAJOR.MINOR.PATCH
- Automatic CI/CD releases on version changes
## 🏗️ CI/CD Pipeline
### Automated Workflow
The `.gitea/workflows/release.yml` automatically:
1. **Detects version changes** in pubspec.yaml
2. **Builds Windows & Android** versions
3. **Signs all executables** (when certificates available)
4. **Creates multiple distribution formats**
5. **Publishes to releases page** with detailed notes
### Manual Triggers
- Push to main branch with version change
- Manual workflow dispatch
- Tag creation (v1.0.1 format)
## 👥 User Instructions
### For End Users (Standalone ZIP)
```
1. Download rmtPocketWatcher-Windows-v1.0.1-release.zip
2. Extract to any folder (Desktop, Program Files, etc.)
3. Double-click rmtpocketwatcher.exe
4. If Windows shows a warning:
- Click "More info" → "Run anyway" (first time only)
- Certificate will be automatically trusted for future runs
```
### For End Users (MSIX Installer)
```
1. Download rmtpocketwatcher.msix
2. Double-click the file
3. Click "Install" when prompted
4. Find "rmtPocketWatcher" in Start Menu
5. Updates can be installed over existing version
```
### For Organizations (Certificate Pre-installation)
```
1. Distribute rmtPocketWatcher.cer to users
2. Users double-click and install to "Trusted Root"
3. All future app versions will be automatically trusted
4. No security warnings for any Lambda Banking Conglomerate software
```
## 🛠️ Maintenance
### Certificate Renewal (Every 3 Years)
```powershell
# Check expiration
$cert = Get-PfxCertificate -FilePath "certificates/rmtPocketWatcher.pfx"
$daysLeft = ($cert.NotAfter - (Get-Date)).Days
Write-Host "Certificate expires in $daysLeft days"
# Renew certificate
.\create_certificate.ps1 -Force
```
### Upgrading to Commercial Certificate
1. Purchase from DigiCert, Sectigo, or similar ($100-500/year)
2. Update `pubspec.yaml` with new certificate path
3. Update build scripts with new password
4. Re-sign and redistribute applications
## 📊 Trust Levels Comparison
| Distribution Method | Initial Trust | User Action Required | Long-term Trust |
|-------------------|---------------|---------------------|-----------------|
| **Unsigned** | ❌ High warnings | Click through multiple warnings | ❌ Always warns |
| **Self-signed** | ⚠️ Moderate warning | "More info" → "Run anyway" | ✅ Trusted after first run |
| **Self-signed + Pre-installed Cert** | ✅ Full trust | None | ✅ Always trusted |
| **Commercial Certificate** | ✅ Full trust | None | ✅ Always trusted |
## 🎯 Recommendations
### For Development/Testing
- ✅ Current self-signed setup is perfect
- Provides professional appearance
- Eliminates most user friction
### For Commercial Distribution
- Consider upgrading to commercial certificate ($200-500/year)
- Provides immediate trust without user interaction
- Required for some enterprise environments
### For Open Source Projects
- ✅ Current setup is ideal
- Document certificate installation for power users
- Consider community funding for commercial certificate
## 📞 Support & Troubleshooting
### Common Issues
1. **"Windows protected your PC"** - Click "More info" → "Run anyway"
2. **Certificate expired** - Run `.\create_certificate.ps1 -Force`
3. **SignTool not found** - Install Windows SDK
4. **Access denied** - Run PowerShell as Administrator
### Getting Help
- Check `CERTIFICATE_GUIDE.md` for detailed troubleshooting
- Review Windows Event Viewer for signing errors
- Verify certificate validity with `Get-AuthenticodeSignature`
## 🏆 Achievement Unlocked!
Your rmtPocketWatcher application now has:
- **Professional code signing** ✅
- **Multiple distribution formats** ✅
- **Automated build pipeline** ✅
- **Built-in update system** ✅
- **Enterprise-ready deployment** ✅
Users will see "Lambda Banking Conglomerate" as the verified publisher, eliminating security warnings and building trust in your Star Citizen AUEC price tracking application!

View File

@@ -0,0 +1,163 @@
# rmtPocketWatcher - Distribution Guide
This guide explains how to distribute rmtPocketWatcher to end users.
## Available Distribution Formats
### 1. Standalone ZIP Package (Recommended)
**File**: `rmtPocketWatcher-Windows-v{version}.zip`
- **Size**: ~50-100MB (includes all dependencies)
- **Requirements**: Windows 10/11 (any edition)
- **Installation**: Extract ZIP and run `rmtpocketwatcher.exe`
- **Pros**: Works on any Windows system, no installation needed
- **Cons**: Larger download size
### 2. MSIX Installer
**File**: `rmtpocketwatcher.msix`
- **Size**: ~30-60MB
- **Requirements**: Windows 10 version 1809+ or Windows 11
- **Installation**: Double-click to install via Windows Package Manager
- **Pros**: Clean installation/uninstallation, automatic updates support
- **Cons**: Requires newer Windows versions
### 3. Portable Executable (Future)
**File**: `rmtPocketWatcher-Windows-Portable-v{version}.zip`
- **Size**: ~5-15MB (single executable)
- **Requirements**: Windows 10/11 + Visual C++ Redistributable
- **Installation**: Extract and run `rmtpocketwatcher.exe`
- **Pros**: Smallest download, truly portable
- **Cons**: May require additional runtime libraries
## Distribution Channels
### Direct Download
1. Upload files to your Gitea releases page
2. Users download appropriate version for their system
3. Provide installation instructions
### GitHub/Gitea Releases
- Automated via CI/CD pipeline
- Includes release notes and changelogs
- Multiple download options in one place
## User Instructions
### For Standalone ZIP (Most Users)
```
1. Download rmtPocketWatcher-Windows-v{version}.zip
2. Extract the ZIP file to any folder (e.g., Desktop, Program Files)
3. Double-click rmtpocketwatcher.exe to run
4. No installation or admin rights required
```
### For MSIX Installer (Advanced Users)
```
1. Download rmtpocketwatcher.msix
2. Double-click the file
3. Click "Install" when prompted
4. Find "rmtPocketWatcher" in Start Menu
5. Uninstall via Settings > Apps if needed
```
## System Requirements
### Minimum Requirements
- **OS**: Windows 10 version 1903 or later
- **RAM**: 4GB (8GB recommended)
- **Storage**: 200MB free space
- **Network**: Internet connection for price data
### Recommended Requirements
- **OS**: Windows 11
- **RAM**: 8GB or more
- **Storage**: 1GB free space (for data storage)
- **Network**: Stable broadband connection
## Troubleshooting
### Common Issues
#### "Windows protected your PC" SmartScreen Warning
- Click "More info" → "Run anyway"
- This happens because the app isn't digitally signed
- Consider code signing for production releases
#### Missing Visual C++ Runtime
- Download and install Microsoft Visual C++ Redistributable
- Usually only affects portable versions
- Standalone ZIP includes all dependencies
#### Antivirus False Positives
- Some antivirus software may flag the executable
- Add exception for rmtpocketwatcher.exe
- This is common with unsigned executables
#### App Won't Start
- Check Windows Event Viewer for error details
- Ensure .env file is present (for standalone version)
- Try running as administrator
### Performance Issues
- Close other resource-intensive applications
- Check network connectivity for real-time data
- Consider increasing Windows virtual memory
## Security Considerations
### For Developers
- Consider code signing certificates for production
- Implement automatic update verification
- Use HTTPS for all network communications
### For Users
- Download only from official sources
- Verify file checksums if provided
- Keep Windows and antivirus software updated
## Update Process
### Automatic Updates (Built-in)
- App checks for updates every 4 hours
- Shows notification banner when available
- Users can manually check via refresh button
- Downloads handled by system browser
### Manual Updates
- Download new version
- Replace old files with new ones (standalone)
- Or install new MSIX over existing installation
## Support Information
### Getting Help
- Check the GitHub/Gitea issues page
- Review the README.md file
- Contact Lambda Banking Conglomerate
### Reporting Issues
- Include Windows version and build number
- Describe steps to reproduce the problem
- Attach relevant log files if available
- Mention which distribution format you're using
## Developer Notes
### Building for Distribution
```powershell
# Create all distribution formats
.\build_windows.ps1 -Release
# Test the build
cd build\windows\standalone
.\rmtpocketwatcher.exe
```
### CI/CD Integration
- Builds are automated via GitHub Actions
- Releases are created automatically on version changes
- All distribution formats are included in releases
### Version Management
- Update version in `pubspec.yaml`
- Follow semantic versioning (MAJOR.MINOR.PATCH)
- Include changelog in release notes

View File

@@ -0,0 +1,60 @@
# Update System
The rmtPocketWatcher Flutter app includes an automatic update checking system that monitors the Gitea repository for new releases.
## How It Works
1. **Automatic Checks**: The app checks for updates every 4 hours automatically
2. **Manual Checks**: Users can manually check for updates using the refresh button in the title bar
3. **Version Comparison**: Compares current app version (from pubspec.yaml) with latest release tag
4. **Platform Detection**: Automatically detects the appropriate download asset for the current platform
## Components
### UpdateService
- Fetches releases from Gitea RSS feed: `https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher/releases.rss`
- Parses XML/RSS format to extract release information
- Compares version numbers and generates platform-specific download URLs
### UpdateProvider
- Manages update checking state and notifications
- Stores last check time and dismissed updates in SharedPreferences
- Provides reactive state for UI components
### UpdateNotificationBanner
- Shows a dismissible banner when updates are available
- Displays version information and release details
- Provides direct links to download or view release page
## Configuration
The system is configured to work with your Gitea repository structure:
- Repository: `LambdaBankingConglomerate/rmtPocketWatcher`
- RSS Feed: `https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher/releases.rss`
- Release tags should follow semantic versioning (e.g., `v1.0.0`, `v1.2.3`)
- Expected asset naming convention:
- Windows: `rmtPocketWatcher-Windows-v{version}.zip`
- Android: `rmtPocketWatcher-Android-v{version}.apk`
- Future platforms can be added with similar naming patterns
## Usage
The update system is automatically initialized when the app starts:
```dart
// In HomeScreen initState
context.read<UpdateProvider>().initialize();
context.read<UpdateProvider>().startPeriodicChecks();
```
Users will see:
1. A notification banner when updates are available
2. An update icon in the title bar (desktop)
3. A detailed dialog with release notes and download options
## Privacy & Security
- Only checks public release information
- No personal data is transmitted
- Downloads are handled by the system browser/app store
- Users control when to update (no forced updates)

View File

@@ -0,0 +1,230 @@
# Window Management & System Tray Guide
Your rmtPocketWatcher application now includes comprehensive window management and system tray functionality for a professional desktop experience.
## 🪟 Window Controls
### Title Bar Features
- **Draggable Area**: Click and drag anywhere on the title bar to move the window
- **Double-click**: Double-click the title bar to maximize/restore the window
- **Custom Controls**: Professional-looking minimize, maximize, and close buttons
### Window Control Buttons
| Button | Icon | Function | Tooltip |
|--------|------|----------|---------|
| **Minimize** | `` | Minimizes to system tray | "Minimize to system tray" |
| **Maximize** | `⛶`/`⧉` | Toggle maximize/restore | "Maximize window" / "Restore window" |
| **Close** | `×` | Closes to system tray | "Close to system tray" |
### Key Behaviors
- **Close button** minimizes to tray instead of exiting the application
- **Minimize button** sends the window directly to the system tray
- **Window dragging** works from anywhere on the title bar
- **Maximize toggle** remembers window state
## 🔔 System Tray Integration
### Tray Icon Features
- **Dynamic Tooltip**: Shows current connection status
- **Single Click**: Toggle window visibility (show/hide)
- **Right Click**: Opens context menu with options
- **Visual Indicator**: Icon represents the application state
### System Tray Menu
| Menu Item | Function |
|-----------|----------|
| **Show rmtPocketWatcher** | Restores window from tray |
| **Check for Updates** | Shows window and triggers update check |
| **🔄 Update Available!** | Appears when updates are detected |
| **About** | Shows application information dialog |
| **Exit** | Completely closes the application |
### Dynamic Status Updates
- **Tooltip Updates**: Reflects connection status ("Connected", "Disconnecting", etc.)
- **Menu Updates**: Shows update availability in real-time
- **Status Integration**: Syncs with price provider and update provider
## 🎛️ User Experience
### Window Lifecycle
1. **Startup**: Window appears normally
2. **Minimize**: Window hides to system tray
3. **Tray Click**: Window restores and focuses
4. **Close**: Window hides to tray (doesn't exit)
5. **Exit**: Only via tray menu "Exit" option
### Professional Features
- **No Taskbar Clutter**: Minimized windows don't show in taskbar
- **Background Operation**: App continues running when minimized
- **Quick Access**: Single click to restore from tray
- **Status Awareness**: Tray shows current application state
## 🔧 Technical Implementation
### Core Components
#### WindowService (`lib/services/window_service.dart`)
- Manages all window operations
- Handles system tray integration
- Provides event listeners for window and tray events
- Integrates with application providers
#### Key Methods
```dart
WindowService().minimizeToTray() // Hide to system tray
WindowService().showWindow() // Restore from tray
WindowService().maximizeWindow() // Toggle maximize state
WindowService().closeWindow() // Close to tray
WindowService().exitApp() // Complete application exit
```
#### Provider Integration
- **PriceProvider**: Updates tray tooltip with connection status
- **UpdateProvider**: Updates tray menu with update availability
- **Real-time Sync**: Tray reflects current application state
### Event Handling
#### Window Events
- `onWindowClose()`: Intercepts close and minimizes to tray
- `onWindowMinimize()`: Handles minimize operations
- `onWindowRestore()`: Manages window restoration
- `onWindowMaximize()`/`onWindowUnmaximize()`: Toggle states
#### Tray Events
- `onTrayIconMouseDown()`: Single click to toggle visibility
- `onTrayIconRightMouseDown()`: Right click for context menu
- `onTrayMenuItemClick()`: Handle menu item selections
## 🎨 Visual Design
### Title Bar Styling
- **Background**: Dark theme (`#1A1F3A`)
- **Text**: White application name and organization
- **Buttons**: Consistent with application theme
- **Height**: 40px for comfortable interaction
### System Tray
- **Icon**: Application logo (falls back to analytics icon)
- **Tooltip**: Dynamic status information
- **Menu**: Consistent with application theme
## 🚀 Usage Examples
### For End Users
#### Basic Window Management
```
• Drag title bar to move window
• Double-click title bar to maximize
• Click minimize () to hide to tray
• Click close (×) to hide to tray
• Right-click tray icon for menu
• Click "Exit" in tray menu to quit
```
#### System Tray Operations
```
• Single-click tray icon: Show/hide window
• Right-click tray icon: Open context menu
• Menu → "Show rmtPocketWatcher": Restore window
• Menu → "Check for Updates": Check for new versions
• Menu → "About": View application information
• Menu → "Exit": Completely close application
```
### For Developers
#### Integrating with Providers
```dart
// Update tray status based on connection
WindowService().updateTrayTooltip('Connected to servers');
// Update menu with update availability
WindowService().updateTrayMenu(hasUpdate: true);
// Show about dialog from tray
WindowService()._showAboutDialog();
```
## 🔒 Security & Privacy
### Safe Operations
- **No Data Collection**: Window management doesn't collect user data
- **Local Storage**: All preferences stored locally
- **Secure Minimize**: Sensitive data hidden when minimized
- **Clean Exit**: Proper cleanup on application exit
### User Control
- **Explicit Actions**: All operations require user interaction
- **Clear Feedback**: Visual and tooltip feedback for all actions
- **Reversible**: All window states can be restored
- **Transparent**: Clear indication of application status
## 🛠️ Troubleshooting
### Common Issues
#### System Tray Icon Not Appearing
- **Cause**: System tray initialization failed
- **Solution**: Check Windows notification area settings
- **Workaround**: Use window controls instead of tray
#### Window Won't Restore
- **Cause**: Window service not initialized
- **Solution**: Restart application
- **Debug**: Check console for initialization errors
#### Tray Menu Not Working
- **Cause**: Context menu creation failed
- **Solution**: Update tray manager package
- **Workaround**: Use window controls
### Debug Information
```dart
// Check window service status
print('Window service initialized: ${WindowService().isInitialized}');
print('Minimized to tray: ${WindowService().isMinimizedToTray}');
```
## 📈 Future Enhancements
### Planned Features
- **Tray Notifications**: Show price alerts in tray
- **Quick Actions**: Direct price check from tray menu
- **Status Icons**: Different icons for different connection states
- **Keyboard Shortcuts**: Global hotkeys for window operations
### Customization Options
- **Tray Behavior**: Option to minimize to taskbar instead
- **Close Behavior**: Option to exit instead of minimize
- **Startup Options**: Start minimized to tray
- **Theme Integration**: Tray menu theme matching
## 📋 Summary
Your rmtPocketWatcher application now provides:
**Professional Window Management**
- Draggable title bar with custom controls
- Proper minimize, maximize, and close operations
- Seamless window state management
**Complete System Tray Integration**
- Hide to tray instead of taskbar clutter
- Dynamic status updates in tooltip
- Full-featured context menu
**User-Friendly Experience**
- Intuitive window operations
- Clear visual feedback
- Professional desktop application behavior
**Developer-Friendly Architecture**
- Clean service-based implementation
- Provider integration for real-time updates
- Extensible for future enhancements
The application now behaves like a professional desktop application with proper window management and system tray integration, providing users with a seamless and intuitive experience for monitoring AUEC prices.

View File

@@ -1,4 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Internet permissions (required for API/WebSocket) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Notification permissions --> <!-- Notification permissions -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1a1a2e</color>
</resources>

81
flutter_app/build_sfx.ps1 Normal file
View File

@@ -0,0 +1,81 @@
# rmtPocketWatcher Self-Extracting Executable Builder
# Creates a single .exe that extracts and runs the app
$ErrorActionPreference = "Stop"
$7zPath = "${env:ProgramFiles}\7-Zip\7z.exe"
if (-not (Test-Path $7zPath)) {
$7zPath = "${env:ProgramFiles(x86)}\7-Zip\7z.exe"
}
if (-not (Test-Path $7zPath)) {
Write-Error "7-Zip not found. Please install 7-Zip from https://7-zip.org"
exit 1
}
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$StandaloneDir = Join-Path $ScriptDir "build\windows\standalone"
$OutputDir = Join-Path $ScriptDir "build\windows\sfx"
$SfxModule = "${env:ProgramFiles}\7-Zip\7z.sfx"
$PubspecPath = Join-Path $ScriptDir "pubspec.yaml"
if (-not (Test-Path $StandaloneDir)) {
Write-Error "Standalone build not found. Run build_windows.ps1 first."
exit 1
}
Write-Host "Creating self-extracting executable..." -ForegroundColor Green
# Create output directory
if (Test-Path $OutputDir) { Remove-Item -Recurse -Force $OutputDir }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
# Create SFX config file - silent extract to AppData and run
$SfxConfig = @"
;!@Install@!UTF-8!
Title="rmtPocketWatcher"
InstallPath="%LOCALAPPDATA%\\rmtPocketWatcher"
RunProgram="rmtpocketwatcher.exe"
GUIMode="2"
OverwriteMode="2"
;!@InstallEnd@!
"@
$SfxConfigPath = "$OutputDir\sfx_config.txt"
$SfxConfig | Out-File -FilePath $SfxConfigPath -Encoding UTF8
# Create 7z archive of standalone folder
$ArchivePath = "$OutputDir\app.7z"
Write-Host "Compressing application..." -ForegroundColor Yellow
& $7zPath a -t7z -mx=9 -mf=BCJ2 -r $ArchivePath "$StandaloneDir\*" | Out-Null
if (-not (Test-Path $ArchivePath)) {
Write-Error "Failed to create archive"
exit 1
}
# Combine SFX module + config + archive
$Version = (Select-String -Path $PubspecPath -Pattern "^version: (.+)$").Matches[0].Groups[1].Value -replace '\+.*', ''
$SfxExePath = "$OutputDir\rmtPocketWatcher-v$Version-Portable.exe"
Write-Host "Building self-extracting executable..." -ForegroundColor Yellow
# Read binary files and concatenate
$sfxBytes = [System.IO.File]::ReadAllBytes($SfxModule)
$configBytes = [System.IO.File]::ReadAllBytes($SfxConfigPath)
$archiveBytes = [System.IO.File]::ReadAllBytes($ArchivePath)
$outputStream = [System.IO.File]::Create($SfxExePath)
$outputStream.Write($sfxBytes, 0, $sfxBytes.Length)
$outputStream.Write($configBytes, 0, $configBytes.Length)
$outputStream.Write($archiveBytes, 0, $archiveBytes.Length)
$outputStream.Close()
# Cleanup temp files
Remove-Item $SfxConfigPath -Force
Remove-Item $ArchivePath -Force
$FileSize = [math]::Round((Get-Item $SfxExePath).Length / 1MB, 2)
Write-Host "`n✅ Self-extracting executable created!" -ForegroundColor Green
Write-Host " File: $SfxExePath" -ForegroundColor Cyan
Write-Host " Size: $FileSize MB" -ForegroundColor Cyan
Write-Host "`nThis single .exe can be distributed and run on any Windows PC." -ForegroundColor Yellow

View File

@@ -0,0 +1,69 @@
@echo off
REM rmtPocketWatcher Windows Build Script (Batch version)
REM Creates both standalone executable and MSIX installer
echo Building rmtPocketWatcher for Windows (Release mode)
echo =============================================
REM Clean previous builds
echo Cleaning previous builds...
flutter clean
if exist "build" rmdir /s /q "build"
REM Install dependencies
echo Installing dependencies...
flutter pub get
REM Build Flutter Windows app
echo Building Flutter Windows app...
flutter build windows --release
REM Check if build was successful
if not exist "build\windows\x64\runner\Release\rmtpocketwatcher.exe" (
echo ERROR: Build failed - executable not found
pause
exit /b 1
)
echo ✓ Flutter build completed successfully
REM Create standalone executable directory
echo Creating standalone executable package...
if exist "build\windows\standalone" rmdir /s /q "build\windows\standalone"
mkdir "build\windows\standalone"
REM Copy all necessary files for standalone distribution
xcopy "build\windows\x64\runner\Release\*" "build\windows\standalone\" /E /I /H /Y
REM Create version info
echo rmtPocketWatcher - Lambda Banking Conglomerate > "build\windows\standalone\README.txt"
echo Star Citizen AUEC Price Tracker >> "build\windows\standalone\README.txt"
echo. >> "build\windows\standalone\README.txt"
echo To run: Double-click rmtpocketwatcher.exe >> "build\windows\standalone\README.txt"
echo No installation required - all dependencies included. >> "build\windows\standalone\README.txt"
echo ✓ Standalone executable created at: build\windows\standalone
REM Create MSIX installer (optional)
echo Creating MSIX installer...
flutter pub run msix:create
if %errorlevel% neq 0 (
echo WARNING: MSIX installer creation failed, continuing with standalone only...
) else (
echo ✓ MSIX installer created
)
REM Create distribution archive
echo Creating distribution archive...
powershell -Command "Compress-Archive -Path 'build\windows\standalone\*' -DestinationPath 'build\rmtPocketWatcher-Windows-Standalone.zip' -CompressionLevel Optimal -Force"
echo.
echo 🎉 Build completed successfully!
echo =============================================
echo Standalone executable: build\windows\standalone\rmtpocketwatcher.exe
echo Distribution archive: build\rmtPocketWatcher-Windows-Standalone.zip
echo.
echo To test: cd build\windows\standalone ^&^& rmtpocketwatcher.exe
echo To distribute: Share the ZIP file
echo.
pause

View File

@@ -0,0 +1,177 @@
# rmtPocketWatcher Windows Build Script
# Creates both standalone executable and MSIX installer
param(
[switch]$Release = $false,
[switch]$Debug = $false
)
$ErrorActionPreference = "Stop"
# Determine build mode
$BuildMode = if ($Release) { "release" } elseif ($Debug) { "debug" } else { "release" }
$BuildModeCapital = (Get-Culture).TextInfo.ToTitleCase($BuildMode)
Write-Host "Building rmtPocketWatcher for Windows ($BuildModeCapital mode)" -ForegroundColor Green
Write-Host "=============================================" -ForegroundColor Green
# Clean previous builds
Write-Host "Cleaning previous builds..." -ForegroundColor Yellow
flutter clean
if (Test-Path "build") {
Remove-Item -Recurse -Force "build"
}
# Install dependencies
Write-Host "Installing dependencies..." -ForegroundColor Yellow
flutter pub get
# Build Flutter Windows app
Write-Host "Building Flutter Windows app..." -ForegroundColor Yellow
flutter build windows --$BuildMode
# Check if build was successful
$ExePath = "build\windows\x64\runner\$BuildModeCapital\rmtpocketwatcher.exe"
if (-not (Test-Path $ExePath)) {
Write-Error "Build failed - executable not found at $ExePath"
exit 1
}
Write-Host "✅ Flutter build completed successfully" -ForegroundColor Green
# Create standalone executable directory
$StandaloneDir = "build\windows\standalone"
Write-Host "Creating standalone executable package..." -ForegroundColor Yellow
if (Test-Path $StandaloneDir) {
Remove-Item -Recurse -Force $StandaloneDir
}
New-Item -ItemType Directory -Path $StandaloneDir -Force | Out-Null
# Copy all necessary files for standalone distribution
$SourceDir = "build\windows\x64\runner\$BuildModeCapital"
Copy-Item -Path "$SourceDir\*" -Destination $StandaloneDir -Recurse -Force
# Create version info
$Version = (Select-String -Path "pubspec.yaml" -Pattern "^version: (.+)$").Matches[0].Groups[1].Value -replace '\+.*', ''
$VersionInfo = @"
rmtPocketWatcher v$Version
Lambda Banking Conglomerate
Star Citizen AUEC Price Tracker
Built: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Mode: $BuildModeCapital
To run: Double-click rmtpocketwatcher.exe
No installation required - all dependencies included.
"@
$VersionInfo | Out-File -FilePath "$StandaloneDir\VERSION.txt" -Encoding UTF8
Write-Host "✅ Standalone executable created at: $StandaloneDir" -ForegroundColor Green
# Sign the standalone executable
$CertPath = "certificates\rmtPocketWatcher.pfx"
if (Test-Path $CertPath) {
Write-Host "Signing standalone executable..." -ForegroundColor Yellow
try {
.\sign_executable.ps1 -ExePath "$StandaloneDir\rmtpocketwatcher.exe" -Force
} catch {
Write-Warning "Failed to sign executable: $($_.Exception.Message)"
Write-Host "Continuing without signing..." -ForegroundColor Yellow
}
} else {
Write-Host "No certificate found - executable will be unsigned" -ForegroundColor Yellow
Write-Host "Run .\create_certificate.ps1 to create a self-signed certificate" -ForegroundColor Yellow
}
# Create MSIX installer
Write-Host "Creating MSIX installer..." -ForegroundColor Yellow
try {
# Check for certificate
$CertPath = "certificates\rmtPocketWatcher.pfx"
$CertPassword = if ($env:MSIX_CERTIFICATE_PASSWORD) { $env:MSIX_CERTIFICATE_PASSWORD } else { "rmtPocketWatcher2024!" }
if (Test-Path $CertPath) {
Write-Host "Using certificate for signing: $CertPath" -ForegroundColor Cyan
$env:MSIX_CERTIFICATE_PATH = $CertPath
$env:MSIX_CERTIFICATE_PASSWORD = $CertPassword
} else {
Write-Host "No certificate found - creating unsigned MSIX" -ForegroundColor Yellow
Write-Host "Run .\create_certificate.ps1 to create a self-signed certificate" -ForegroundColor Yellow
}
flutter pub run msix:create
$MsixPath = Get-ChildItem -Path "build\windows\x64\runner\$BuildModeCapital" -Filter "*.msix" | Select-Object -First 1
if ($MsixPath) {
Write-Host "✅ MSIX installer created: $($MsixPath.FullName)" -ForegroundColor Green
# Sign the MSIX if certificate exists
if (Test-Path $CertPath) {
Write-Host "Signing MSIX installer..." -ForegroundColor Yellow
try {
$signtool = "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe"
if (-not (Test-Path $signtool)) {
# Try to find signtool in common locations
$signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits" -Recurse -Name "signtool.exe" | Select-Object -First 1
if ($signtool) {
$signtool = "${env:ProgramFiles(x86)}\Windows Kits\$signtool"
}
}
if (Test-Path $signtool) {
& $signtool sign /f $CertPath /p $CertPassword /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 $MsixPath.FullName
if ($LASTEXITCODE -eq 0) {
Write-Host "✅ MSIX installer signed successfully" -ForegroundColor Green
} else {
Write-Warning "Failed to sign MSIX installer"
}
} else {
Write-Warning "SignTool not found - install Windows SDK to enable signing"
}
} catch {
Write-Warning "Failed to sign MSIX: $($_.Exception.Message)"
}
}
} else {
Write-Warning "MSIX installer creation completed but file not found in expected location"
}
} catch {
Write-Warning "MSIX installer creation failed: $($_.Exception.Message)"
Write-Host "Continuing with standalone executable only..." -ForegroundColor Yellow
}
# Create distribution archive
Write-Host "Creating distribution archive..." -ForegroundColor Yellow
$ArchiveName = "rmtPocketWatcher-Windows-v$Version-$BuildMode.zip"
$ArchivePath = "build\$ArchiveName"
if (Test-Path $ArchivePath) {
Remove-Item $ArchivePath -Force
}
Compress-Archive -Path "$StandaloneDir\*" -DestinationPath $ArchivePath -CompressionLevel Optimal
Write-Host "✅ Distribution archive created: $ArchivePath" -ForegroundColor Green
# Summary
Write-Host "`n🎉 Build completed successfully!" -ForegroundColor Green
Write-Host "=============================================" -ForegroundColor Green
Write-Host "Standalone executable: $StandaloneDir\rmtpocketwatcher.exe" -ForegroundColor Cyan
Write-Host "Distribution archive: $ArchivePath" -ForegroundColor Cyan
if ($MsixPath) {
Write-Host "MSIX installer: $($MsixPath.FullName)" -ForegroundColor Cyan
}
Write-Host "`nTo test the standalone version:" -ForegroundColor Yellow
Write-Host " cd $StandaloneDir" -ForegroundColor White
Write-Host " .\rmtpocketwatcher.exe" -ForegroundColor White
Write-Host "`nTo distribute:" -ForegroundColor Yellow
Write-Host " - Share the ZIP file: $ArchiveName" -ForegroundColor White
Write-Host " - Users extract and run rmtpocketwatcher.exe" -ForegroundColor White
if ($MsixPath) {
Write-Host " - Or share the MSIX installer for Windows Store-style installation" -ForegroundColor White
}

View File

@@ -0,0 +1,31 @@
rmtPocketWatcher Code Signing Certificate
========================================
Certificate Details:
- Subject: CN=Lambda Banking Conglomerate, O=Lambda Banking Conglomerate, C=US
- Thumbprint: 4A4AFB542D1E34E3C96FD6EAAD3B88A6BA246093
- Valid From: 12/14/2025 23:26:52
- Valid Until: 12/14/2028 23:36:52
- Algorithm: sha1RSA
Files Created:
- certificates\rmtPocketWatcher.pfx (PFX with private key - keep secure!)
- certificates\rmtPocketWatcher.cer (Public certificate for distribution)
Password: rmtPocketWatcher2024!
Usage:
- Use the PFX file for signing applications
- Distribute the CER file to users who need to trust your apps
- Keep the PFX file secure and never share it publicly
Installation Instructions for Users:
1. Double-click certificates\rmtPocketWatcher.cer
2. Click "Install Certificate"
3. Choose "Local Machine" (requires admin) or "Current User"
4. Select "Place all certificates in the following store"
5. Browse and select "Trusted Root Certification Authorities"
6. Click "Next" and "Finish"
Note: This is a self-signed certificate. For production use,
consider purchasing a certificate from a trusted CA.

Binary file not shown.

View File

@@ -0,0 +1,162 @@
# Self-Signed Certificate Creation Script for rmtPocketWatcher
# Creates a code signing certificate for Windows applications
param(
[string]$CertName = "Lambda Banking Conglomerate",
[string]$AppName = "rmtPocketWatcher",
[int]$ValidYears = 3,
[switch]$Force = $false
)
$ErrorActionPreference = "Stop"
Write-Host "Creating Self-Signed Certificate for $AppName" -ForegroundColor Green
Write-Host "================================================" -ForegroundColor Green
# Certificate paths
$CertDir = "certificates"
$CertPath = "$CertDir\$AppName.pfx"
$CerPath = "$CertDir\$AppName.cer"
$Password = "rmtPocketWatcher2024!"
# Create certificates directory
if (-not (Test-Path $CertDir)) {
New-Item -ItemType Directory -Path $CertDir -Force | Out-Null
Write-Host "Created certificates directory: $CertDir" -ForegroundColor Yellow
}
# Check if certificate already exists
if ((Test-Path $CertPath) -and -not $Force) {
Write-Host "Certificate already exists at: $CertPath" -ForegroundColor Yellow
Write-Host "Use -Force to recreate the certificate" -ForegroundColor Yellow
# Check if certificate is still valid
try {
$cert = Get-PfxCertificate -FilePath $CertPath
$daysUntilExpiry = ($cert.NotAfter - (Get-Date)).Days
if ($daysUntilExpiry -gt 30) {
Write-Host "Current certificate is valid for $daysUntilExpiry more days" -ForegroundColor Green
Write-Host "Certificate Subject: $($cert.Subject)" -ForegroundColor Cyan
Write-Host "Certificate Thumbprint: $($cert.Thumbprint)" -ForegroundColor Cyan
return
} else {
Write-Host "Certificate expires in $daysUntilExpiry days, recreating..." -ForegroundColor Yellow
$Force = $true
}
} catch {
Write-Host "Existing certificate is invalid, recreating..." -ForegroundColor Yellow
$Force = $true
}
}
# Remove existing certificate if forcing recreation
if ($Force -and (Test-Path $CertPath)) {
Remove-Item $CertPath -Force
Write-Host "Removed existing certificate" -ForegroundColor Yellow
}
Write-Host "Creating new self-signed certificate..." -ForegroundColor Yellow
# Create the certificate
$notAfter = (Get-Date).AddYears($ValidYears)
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject "CN=$CertName, O=$CertName, C=US" `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" `
-KeyExportPolicy Exportable `
-KeyUsage DigitalSignature `
-NotAfter $notAfter `
-CertStoreLocation "Cert:\CurrentUser\My"
Write-Host "✅ Certificate created successfully" -ForegroundColor Green
Write-Host "Certificate Thumbprint: $($cert.Thumbprint)" -ForegroundColor Cyan
Write-Host "Valid Until: $($cert.NotAfter)" -ForegroundColor Cyan
# Export certificate to PFX (with private key)
$securePassword = ConvertTo-SecureString -String $Password -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath $CertPath -Password $securePassword | Out-Null
Write-Host "✅ Exported PFX certificate to: $CertPath" -ForegroundColor Green
# Export certificate to CER (public key only, for distribution)
Export-Certificate -Cert $cert -FilePath $CerPath | Out-Null
Write-Host "✅ Exported CER certificate to: $CerPath" -ForegroundColor Green
# Install certificate to Trusted Root (requires admin)
Write-Host "Installing certificate to Trusted Root Certification Authorities..." -ForegroundColor Yellow
try {
# Check if running as administrator
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
if ($isAdmin) {
Import-Certificate -FilePath $CerPath -CertStoreLocation "Cert:\LocalMachine\Root" | Out-Null
Write-Host "✅ Certificate installed to Trusted Root (system-wide)" -ForegroundColor Green
} else {
Import-Certificate -FilePath $CerPath -CertStoreLocation "Cert:\CurrentUser\Root" | Out-Null
Write-Host "✅ Certificate installed to Trusted Root (current user)" -ForegroundColor Green
Write-Host "⚠️ Run as Administrator to install system-wide" -ForegroundColor Yellow
}
} catch {
Write-Host "❌ Failed to install certificate to Trusted Root: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "You may need to install it manually" -ForegroundColor Yellow
}
# Create certificate info file
$certInfo = @"
rmtPocketWatcher Code Signing Certificate
========================================
Certificate Details:
- Subject: $($cert.Subject)
- Thumbprint: $($cert.Thumbprint)
- Valid From: $($cert.NotBefore)
- Valid Until: $($cert.NotAfter)
- Algorithm: $($cert.SignatureAlgorithm.FriendlyName)
Files Created:
- $CertPath (PFX with private key - keep secure!)
- $CerPath (Public certificate for distribution)
Password: $Password
Usage:
- Use the PFX file for signing applications
- Distribute the CER file to users who need to trust your apps
- Keep the PFX file secure and never share it publicly
Installation Instructions for Users:
1. Double-click $CerPath
2. Click "Install Certificate"
3. Choose "Local Machine" (requires admin) or "Current User"
4. Select "Place all certificates in the following store"
5. Browse and select "Trusted Root Certification Authorities"
6. Click "Next" and "Finish"
Note: This is a self-signed certificate. For production use,
consider purchasing a certificate from a trusted CA.
"@
$certInfo | Out-File -FilePath "$CertDir\CERTIFICATE_INFO.txt" -Encoding UTF8
Write-Host "✅ Certificate information saved to: $CertDir\CERTIFICATE_INFO.txt" -ForegroundColor Green
Write-Host "`n🎉 Certificate setup completed!" -ForegroundColor Green
Write-Host "================================================" -ForegroundColor Green
Write-Host "PFX Certificate: $CertPath" -ForegroundColor Cyan
Write-Host "Public Certificate: $CerPath" -ForegroundColor Cyan
Write-Host "Password: $Password" -ForegroundColor Cyan
Write-Host "`nNext steps:" -ForegroundColor Yellow
Write-Host "1. Update your build scripts to use this certificate" -ForegroundColor White
Write-Host "2. Test signing your application" -ForegroundColor White
Write-Host "3. Distribute the .cer file to users if needed" -ForegroundColor White
# Add to .gitignore if not already there
$gitignorePath = ".gitignore"
if (Test-Path $gitignorePath) {
$gitignoreContent = Get-Content $gitignorePath -Raw
if ($gitignoreContent -notmatch "certificates/") {
Add-Content $gitignorePath "`n# Code signing certificates`ncertificates/*.pfx`ncertificates/*.p12"
Write-Host "✅ Added certificate files to .gitignore" -ForegroundColor Green
}
}

View File

@@ -0,0 +1,37 @@
# Encode Certificate for CI/CD
# Converts the PFX certificate to base64 for use as GitHub/Gitea action secret
param(
[string]$CertPath = "certificates\rmtPocketWatcher.pfx",
[string]$OutputFile = "certificate_base64.txt"
)
$ErrorActionPreference = "Stop"
Write-Host "Encoding Certificate for CI/CD" -ForegroundColor Green
Write-Host "==============================" -ForegroundColor Green
# Check if certificate exists
if (-not (Test-Path $CertPath)) {
Write-Error "Certificate not found at: $CertPath"
Write-Host "Create a certificate first using .\create_certificate.ps1" -ForegroundColor Yellow
exit 1
}
# Read certificate and encode as base64
Write-Host "Reading certificate: $CertPath" -ForegroundColor Yellow
$certBytes = [System.IO.File]::ReadAllBytes((Resolve-Path $CertPath))
$certBase64 = [System.Convert]::ToBase64String($certBytes)
# Save to file
$certBase64 | Out-File -FilePath $OutputFile -Encoding ASCII -NoNewline
Write-Host "✅ Certificate encoded successfully!" -ForegroundColor Green
Write-Host "Base64 encoded certificate saved to: $OutputFile" -ForegroundColor Cyan
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host "1. Copy the contents of $OutputFile" -ForegroundColor White
Write-Host "2. Add as action secret named 'CERT_BASE64'" -ForegroundColor White
Write-Host "3. Add certificate password as secret named 'CERT_PASSWORD'" -ForegroundColor White
Write-Host ""
Write-Host "⚠️ IMPORTANT: Delete $OutputFile after copying to keep certificate secure!" -ForegroundColor Red

View File

@@ -427,7 +427,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";

View File

@@ -1,122 +1 @@
{ {"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'providers/price_provider.dart'; import 'providers/price_provider.dart';
import 'providers/update_provider.dart';
import 'screens/home_screen.dart'; import 'screens/home_screen.dart';
import 'services/notification_service.dart'; import 'services/notification_service.dart';
import 'widgets/loading_screen.dart'; import 'widgets/loading_screen.dart';
@@ -37,6 +38,9 @@ Future<void> main() async {
windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
// Initialize window service after window is ready
// Context will be set later from the home screen
}); });
} }
@@ -61,8 +65,11 @@ class _MyAppState extends State<MyApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return MultiProvider(
create: (_) => PriceProvider(), providers: [
ChangeNotifierProvider(create: (_) => PriceProvider()),
ChangeNotifierProvider(create: (_) => UpdateProvider()),
],
child: MaterialApp( child: MaterialApp(
title: 'rmtPocketWatcher', title: 'rmtPocketWatcher',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,

View File

@@ -0,0 +1,95 @@
import 'package:flutter/foundation.dart';
import '../services/update_service.dart';
import '../services/storage_service.dart';
class UpdateProvider extends ChangeNotifier {
final UpdateService _updateService = UpdateService();
final StorageService _storageService = StorageService();
UpdateInfo? _availableUpdate;
bool _isChecking = false;
DateTime? _lastChecked;
bool _updateDismissed = false;
UpdateInfo? get availableUpdate => _availableUpdate;
bool get isChecking => _isChecking;
DateTime? get lastChecked => _lastChecked;
bool get hasUpdate => _availableUpdate != null && !_updateDismissed;
/// Check for updates manually
Future<void> checkForUpdates({bool force = false}) async {
if (_isChecking) return;
// Don't check too frequently unless forced
if (!force && _lastChecked != null) {
final timeSinceLastCheck = DateTime.now().difference(_lastChecked!);
if (timeSinceLastCheck.inHours < 1) {
if (kDebugMode) {
print('Skipping update check - last checked ${timeSinceLastCheck.inMinutes} minutes ago');
}
return;
}
}
_isChecking = true;
notifyListeners();
try {
final updateInfo = await _updateService.checkForUpdates();
_availableUpdate = updateInfo;
_lastChecked = DateTime.now();
_updateDismissed = false;
// Save last check time
await _storageService.setString('last_update_check', _lastChecked!.toIso8601String());
if (updateInfo != null) {
if (kDebugMode) {
print('Update available: ${updateInfo.latestVersion}');
}
// Check if this version was already dismissed
final dismissedVersion = await _storageService.getString('dismissed_update_version');
if (dismissedVersion == updateInfo.latestVersion) {
_updateDismissed = true;
}
}
} catch (e) {
if (kDebugMode) {
print('Error checking for updates: $e');
}
} finally {
_isChecking = false;
notifyListeners();
}
}
/// Dismiss the current update notification
Future<void> dismissUpdate() async {
if (_availableUpdate != null) {
_updateDismissed = true;
await _storageService.setString('dismissed_update_version', _availableUpdate!.latestVersion);
notifyListeners();
}
}
/// Initialize the provider and load saved state
Future<void> initialize() async {
// Load last check time
final lastCheckString = await _storageService.getString('last_update_check');
if (lastCheckString != null) {
_lastChecked = DateTime.tryParse(lastCheckString);
}
// Check for updates on startup (but don't force it)
await checkForUpdates();
}
/// Check for updates automatically in the background
void startPeriodicChecks() {
// Check every 4 hours
Stream.periodic(const Duration(hours: 4)).listen((_) {
checkForUpdates();
});
}
}

View File

@@ -1,14 +1,59 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import '../providers/price_provider.dart'; import '../providers/price_provider.dart';
import '../providers/update_provider.dart';
import '../services/window_service.dart';
import '../widgets/price_chart.dart'; import '../widgets/price_chart.dart';
import '../widgets/alerts_panel.dart'; import '../widgets/alerts_panel.dart';
import '../widgets/vendor_table.dart'; import '../widgets/vendor_table.dart';
import '../widgets/update_notification.dart';
class HomeScreen extends StatelessWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
// Initialize providers and window service
WidgetsBinding.instance.addPostFrameCallback((_) async {
// Initialize window service with context
await WindowService().initialize(context: context);
context.read<UpdateProvider>().initialize();
context.read<UpdateProvider>().startPeriodicChecks();
// Listen to provider changes to update system tray
_setupProviderListeners();
});
}
void _setupProviderListeners() {
// Listen to price provider for connection status
context.read<PriceProvider>().addListener(_updateTrayStatus);
// Listen to update provider for update notifications
context.read<UpdateProvider>().addListener(_updateTrayMenu);
}
void _updateTrayStatus() {
final priceProvider = context.read<PriceProvider>();
WindowService().updateTrayTooltip(
'AUEC Tracker - ${priceProvider.connectionStatus}'
);
}
void _updateTrayMenu() {
final updateProvider = context.read<UpdateProvider>();
WindowService().updateTrayMenu(hasUpdate: updateProvider.hasUpdate);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -19,44 +64,91 @@ class HomeScreen extends StatelessWidget {
if (!kIsWeb && (Theme.of(context).platform == TargetPlatform.windows || if (!kIsWeb && (Theme.of(context).platform == TargetPlatform.windows ||
Theme.of(context).platform == TargetPlatform.macOS || Theme.of(context).platform == TargetPlatform.macOS ||
Theme.of(context).platform == TargetPlatform.linux)) Theme.of(context).platform == TargetPlatform.linux))
Container( GestureDetector(
height: 40, onPanStart: (details) => windowManager.startDragging(),
color: const Color(0xFF1A1F3A), onDoubleTap: () => WindowService().maximizeWindow(),
child: Row( child: Container(
children: [ height: 40,
const SizedBox(width: 16), color: const Color(0xFF1A1F3A),
const Text( child: Row(
'rmtPocketWatcher', children: [
style: TextStyle( const SizedBox(width: 16),
color: Colors.white, const Text(
fontSize: 14, 'rmtPocketWatcher',
fontWeight: FontWeight.bold, style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
), ),
), const Spacer(),
const Spacer(), const Text(
const Text( 'Lambda Banking Conglomerate',
'Lambda Banking Conglomerate', style: TextStyle(
style: TextStyle( color: Color(0xFF888888),
color: Color(0xFF888888), fontSize: 12,
fontSize: 12, ),
), ),
const SizedBox(width: 16),
// Update check button
Consumer<UpdateProvider>(
builder: (context, updateProvider, child) {
return IconButton(
icon: updateProvider.isChecking
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFF50E3C2),
),
)
: Icon(
updateProvider.hasUpdate ? Icons.system_update : Icons.refresh,
color: updateProvider.hasUpdate ? const Color(0xFF50E3C2) : Colors.white,
size: 16,
),
onPressed: updateProvider.isChecking
? null
: () => updateProvider.checkForUpdates(force: true),
tooltip: updateProvider.hasUpdate
? 'Update available'
: 'Check for updates',
);
},
), ),
const SizedBox(width: 16), // Minimize button (minimize to tray)
IconButton( IconButton(
icon: const Icon(Icons.minimize, color: Colors.white, size: 16), icon: const Icon(Icons.minimize, color: Colors.white, size: 16),
onPressed: () { onPressed: () => WindowService().minimizeToTray(),
// Minimize window - implement with window_manager tooltip: 'Minimize to system tray',
),
// Maximize/Restore button
FutureBuilder<bool>(
future: windowManager.isMaximized(),
builder: (context, snapshot) {
bool isMaximized = snapshot.data ?? false;
return IconButton(
icon: Icon(
isMaximized ? Icons.fullscreen_exit : Icons.fullscreen,
color: Colors.white,
size: 16,
),
onPressed: () => WindowService().maximizeWindow(),
tooltip: isMaximized ? 'Restore window' : 'Maximize window',
);
}, },
), ),
// Close button (exit app)
IconButton( IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 16), icon: const Icon(Icons.close, color: Colors.white, size: 16),
onPressed: () { onPressed: () => WindowService().closeWindow(),
// Close window - implement with window_manager tooltip: 'Exit application',
},
), ),
], ],
), ),
), ),
),
// Main content // Main content
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -64,6 +156,8 @@ class HomeScreen extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Update notification banner
const UpdateNotificationBanner(),
// Top stats row - Bloomberg style // Top stats row - Bloomberg style
Consumer<PriceProvider>( Consumer<PriceProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {

View File

@@ -51,4 +51,15 @@ class StorageService {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_customAuecKey, amount); await prefs.setDouble(_customAuecKey, amount);
} }
// Generic string storage methods for update checking
Future<String?> getString(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
}
Future<void> setString(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
}
} }

View File

@@ -0,0 +1,283 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:xml/xml.dart';
class UpdateService {
static const String _releasesRssUrl = 'https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher/releases.rss';
/// Check if the app was installed via MSIX (Windows Store-style installation)
static bool isInstalledViaMsix() {
if (!Platform.isWindows) return false;
try {
// MSIX apps are installed in WindowsApps folder
final exePath = Platform.resolvedExecutable;
return exePath.contains('WindowsApps') ||
exePath.contains('Program Files\\WindowsApps');
} catch (e) {
return false;
}
}
/// Check if an update is available by comparing current version with latest release
Future<UpdateInfo?> checkForUpdates() async {
try {
// Get current app version
final packageInfo = await PackageInfo.fromPlatform();
final currentVersion = packageInfo.version;
if (kDebugMode) {
print('Current app version: $currentVersion');
print('Checking for updates at: $_releasesRssUrl');
}
// Fetch latest release from Gitea RSS feed
final response = await http.get(
Uri.parse(_releasesRssUrl),
headers: {
'Accept': 'application/rss+xml, application/xml, text/xml',
'User-Agent': 'rmtPocketWatcher/$currentVersion',
},
);
if (response.statusCode == 200) {
final document = XmlDocument.parse(response.body);
final items = document.findAllElements('item');
if (items.isEmpty) {
if (kDebugMode) {
print('No releases found in RSS feed');
}
return null;
}
// Get the latest release (first item in RSS feed)
final latestItem = items.first;
final title = latestItem.findElements('title').first.innerText;
final link = latestItem.findElements('link').first.innerText;
final description = latestItem.findElements('description').firstOrNull?.innerText ?? '';
final pubDate = latestItem.findElements('pubDate').first.innerText;
// Extract version from title (assuming format like "v1.2.3" or "Release v1.2.3")
final versionMatch = RegExp(r'v?(\d+\.\d+\.\d+)').firstMatch(title);
if (versionMatch == null) {
if (kDebugMode) {
print('Could not extract version from title: $title');
}
return null;
}
final latestVersion = versionMatch.group(1)!;
if (kDebugMode) {
print('Latest release version: $latestVersion');
print('Release title: $title');
}
// Compare versions
if (isNewerVersion(latestVersion, currentVersion)) {
return UpdateInfo(
currentVersion: currentVersion,
latestVersion: latestVersion,
releaseUrl: link,
releaseName: title,
releaseNotes: description,
publishedAt: _parseRssDate(pubDate),
assets: _generateAssetUrls(latestVersion), // Generate expected asset URLs
);
} else {
if (kDebugMode) {
print('App is up to date');
}
return null;
}
} else {
if (kDebugMode) {
print('Failed to fetch RSS feed: ${response.statusCode}');
print('Response: ${response.body}');
}
return null;
}
} catch (e) {
if (kDebugMode) {
print('Error checking for updates: $e');
}
return null;
}
}
/// Extract version number from git tag (e.g., "v1.2.3" -> "1.2.3")
@visibleForTesting
String extractVersionFromTag(String tag) {
return tag.startsWith('v') ? tag.substring(1) : tag;
}
/// Compare two version strings (e.g., "1.2.3" vs "1.2.2")
@visibleForTesting
bool isNewerVersion(String latest, String current) {
final latestParts = latest.split('.').map(int.parse).toList();
final currentParts = current.split('.').map(int.parse).toList();
// Ensure both have same number of parts
while (latestParts.length < currentParts.length) {
latestParts.add(0);
}
while (currentParts.length < latestParts.length) {
currentParts.add(0);
}
for (int i = 0; i < latestParts.length; i++) {
if (latestParts[i] > currentParts[i]) {
return true;
} else if (latestParts[i] < currentParts[i]) {
return false;
}
}
return false; // Versions are equal
}
/// Parse RSS date format to DateTime
DateTime _parseRssDate(String rssDate) {
try {
// RSS dates are typically in RFC 2822 format
// Example: "Mon, 02 Jan 2006 15:04:05 MST"
return DateTime.parse(rssDate);
} catch (e) {
if (kDebugMode) {
print('Failed to parse RSS date: $rssDate, error: $e');
}
return DateTime.now();
}
}
/// Generate expected asset URLs based on version and platform
List<ReleaseAsset> _generateAssetUrls(String version) {
final baseUrl = 'https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher/releases/download/v$version';
return [
// Windows Full Package (ZIP with all DLLs)
ReleaseAsset(
name: 'rmtPocketWatcher-Windows-v$version.zip',
downloadUrl: '$baseUrl/rmtPocketWatcher-Windows-v$version.zip',
size: 0,
contentType: 'application/zip',
),
// Windows Portable (self-extracting EXE)
ReleaseAsset(
name: 'rmtPocketWatcher-Windows-Portable-v$version.exe',
downloadUrl: '$baseUrl/rmtPocketWatcher-Windows-Portable-v$version.exe',
size: 0,
contentType: 'application/octet-stream',
),
// Windows MSIX Installer
ReleaseAsset(
name: 'rmtPocketWatcher-Windows-v$version.msix',
downloadUrl: '$baseUrl/rmtPocketWatcher-Windows-v$version.msix',
size: 0,
contentType: 'application/msix',
),
// Certificate for code signing verification
ReleaseAsset(
name: 'rmtPocketWatcher-Certificate.cer',
downloadUrl: '$baseUrl/rmtPocketWatcher-Certificate.cer',
size: 0,
contentType: 'application/x-x509-ca-cert',
),
// Android APK
ReleaseAsset(
name: 'rmtPocketWatcher-Android-v$version.apk',
downloadUrl: '$baseUrl/rmtPocketWatcher-Android-v$version.apk',
size: 0,
contentType: 'application/vnd.android.package-archive',
),
];
}
}
class UpdateInfo {
final String currentVersion;
final String latestVersion;
final String releaseUrl;
final String releaseName;
final String releaseNotes;
final DateTime publishedAt;
final List<ReleaseAsset> assets;
UpdateInfo({
required this.currentVersion,
required this.latestVersion,
required this.releaseUrl,
required this.releaseName,
required this.releaseNotes,
required this.publishedAt,
required this.assets,
});
/// Get the appropriate download asset for the current platform
ReleaseAsset? getAssetForCurrentPlatform() {
if (kIsWeb) return null;
switch (defaultTargetPlatform) {
case TargetPlatform.windows:
// If installed via MSIX, prefer MSIX for updates
if (UpdateService.isInstalledViaMsix()) {
var msix = assets.where((asset) => asset.name.endsWith('.msix')).firstOrNull;
if (msix != null) return msix;
}
// Prefer portable self-extracting exe for non-MSIX installs
var portable = assets.where((asset) => asset.name.contains('Portable') && asset.name.endsWith('.exe')).firstOrNull;
if (portable != null) return portable;
// Fall back to full Windows package (ZIP)
var windows = assets.where((asset) => asset.name.contains('Windows') && asset.name.endsWith('.zip')).firstOrNull;
if (windows != null) return windows;
// Last resort: MSIX installer
return assets.where((asset) => asset.name.endsWith('.msix')).firstOrNull;
case TargetPlatform.macOS:
return assets.where((asset) =>
RegExp(r'macOS|macos|mac|darwin|\.dmg$|\.pkg$', caseSensitive: false).hasMatch(asset.name)
).firstOrNull;
case TargetPlatform.linux:
return assets.where((asset) =>
RegExp(r'Linux|linux|\.deb$|\.rpm$|\.appimage$', caseSensitive: false).hasMatch(asset.name)
).firstOrNull;
case TargetPlatform.android:
return assets.where((asset) => asset.name.endsWith('.apk')).firstOrNull;
case TargetPlatform.iOS:
return assets.where((asset) => asset.name.endsWith('.ipa')).firstOrNull;
default:
return null;
}
}
}
class ReleaseAsset {
final String name;
final String downloadUrl;
final int size;
final String contentType;
ReleaseAsset({
required this.name,
required this.downloadUrl,
required this.size,
required this.contentType,
});
String get formattedSize {
if (size < 1024) return '${size}B';
if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(1)}KB';
if (size < 1024 * 1024 * 1024) return '${(size / (1024 * 1024)).toStringAsFixed(1)}MB';
return '${(size / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB';
}
}

View File

@@ -0,0 +1,315 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart';
import 'package:tray_manager/tray_manager.dart';
class WindowService with WindowListener, TrayListener {
static final WindowService _instance = WindowService._internal();
factory WindowService() => _instance;
WindowService._internal();
bool _isInitialized = false;
bool _isMinimizedToTray = false;
BuildContext? _context;
Future<void> initialize({BuildContext? context}) async {
if (_isInitialized) return;
_context = context;
// Initialize window manager
windowManager.addListener(this);
// Initialize system tray
await _initializeTray();
_isInitialized = true;
}
Future<void> _initializeTray() async {
try {
await trayManager.setIcon(
kIsWeb ? '' : 'icon.ico',
isTemplate: false,
);
await trayManager.setToolTip('rmtPocketWatcher - AUEC Price Tracker');
// Create tray menu
Menu menu = Menu(
items: [
MenuItem(
key: 'show_window',
label: 'Show rmtPocketWatcher',
),
MenuItem.separator(),
MenuItem(
key: 'check_updates',
label: 'Check for Updates',
),
MenuItem.separator(),
MenuItem(
key: 'about',
label: 'About',
),
MenuItem.separator(),
MenuItem(
key: 'exit_app',
label: 'Exit',
),
],
);
await trayManager.setContextMenu(menu);
trayManager.addListener(this);
if (kDebugMode) {
print('System tray initialized successfully');
}
} catch (e) {
if (kDebugMode) {
print('Failed to initialize system tray: $e');
}
}
}
// Window controls
Future<void> minimizeWindow() async {
await windowManager.minimize();
}
Future<void> minimizeToTray() async {
await windowManager.hide();
_isMinimizedToTray = true;
if (kDebugMode) {
print('Window minimized to system tray');
}
}
Future<void> showWindow() async {
await windowManager.show();
await windowManager.focus();
_isMinimizedToTray = false;
if (kDebugMode) {
print('Window restored from system tray');
}
}
Future<void> maximizeWindow() async {
bool isMaximized = await windowManager.isMaximized();
if (isMaximized) {
await windowManager.unmaximize();
} else {
await windowManager.maximize();
}
}
Future<void> closeWindow() async {
// Close button should exit the app
await exitApp();
}
Future<void> exitApp() async {
await trayManager.destroy();
await windowManager.destroy();
SystemNavigator.pop();
}
// Window event handlers
@override
void onWindowClose() async {
// Prevent default close behavior
await closeWindow();
}
@override
void onWindowMinimize() async {
// When window is minimized (via minimize button or taskbar), go to tray
await minimizeToTray();
if (kDebugMode) {
print('Window minimized to tray');
}
}
@override
void onWindowRestore() {
_isMinimizedToTray = false;
if (kDebugMode) {
print('Window restored');
}
}
@override
void onWindowMaximize() {
if (kDebugMode) {
print('Window maximized');
}
}
@override
void onWindowUnmaximize() {
if (kDebugMode) {
print('Window unmaximized');
}
}
// Tray event handlers
@override
void onTrayIconMouseDown() async {
// Single click to show/hide window
if (_isMinimizedToTray) {
await showWindow();
} else {
await minimizeToTray();
}
}
@override
void onTrayIconRightMouseDown() async {
// Right click shows context menu (handled automatically)
}
@override
void onTrayMenuItemClick(MenuItem menuItem) async {
switch (menuItem.key) {
case 'show_window':
await showWindow();
break;
case 'check_updates':
await showWindow();
// The update check will be handled by the UI
if (kDebugMode) {
print('Checking for updates from tray menu');
}
break;
case 'about':
await showWindow();
_showAboutDialog();
break;
case 'exit_app':
await exitApp();
break;
}
}
// Update tray tooltip with current status
Future<void> updateTrayTooltip(String status) async {
try {
await trayManager.setToolTip('rmtPocketWatcher - $status');
} catch (e) {
if (kDebugMode) {
print('Failed to update tray tooltip: $e');
}
}
}
// Update tray menu with dynamic content
Future<void> updateTrayMenu({bool hasUpdate = false}) async {
try {
Menu menu = Menu(
items: [
MenuItem(
key: 'show_window',
label: 'Show rmtPocketWatcher',
),
MenuItem.separator(),
MenuItem(
key: 'check_updates',
label: hasUpdate ? '🔄 Update Available!' : 'Check for Updates',
),
MenuItem.separator(),
MenuItem(
key: 'about',
label: 'About',
),
MenuItem.separator(),
MenuItem(
key: 'exit_app',
label: 'Exit',
),
],
);
await trayManager.setContextMenu(menu);
} catch (e) {
if (kDebugMode) {
print('Failed to update tray menu: $e');
}
}
}
// Getters
bool get isMinimizedToTray => _isMinimizedToTray;
bool get isInitialized => _isInitialized;
void _showAboutDialog() {
if (_context != null) {
// Import the about dialog dynamically to avoid circular imports
showDialog(
context: _context!,
builder: (context) {
// We'll create a simple about dialog here to avoid import issues
return AlertDialog(
backgroundColor: const Color(0xFF1A1F3A),
title: const Row(
children: [
Icon(Icons.analytics, color: Color(0xFF50E3C2), size: 32),
SizedBox(width: 12),
Text('rmtPocketWatcher', style: TextStyle(color: Colors.white)),
],
),
content: const Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Star Citizen AUEC Price Tracker',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
SizedBox(height: 8),
Text(
'Developed by Lambda Banking Conglomerate',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
SizedBox(height: 16),
Text(
'Features:',
style: TextStyle(
color: Color(0xFF50E3C2),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text('• Real-time AUEC price tracking', style: TextStyle(color: Colors.white70, fontSize: 12)),
Text('• Multiple vendor monitoring', style: TextStyle(color: Colors.white70, fontSize: 12)),
Text('• Historical price charts', style: TextStyle(color: Colors.white70, fontSize: 12)),
Text('• Price alerts & notifications', style: TextStyle(color: Colors.white70, fontSize: 12)),
Text('• System tray integration', style: TextStyle(color: Colors.white70, fontSize: 12)),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF50E3C2),
foregroundColor: Colors.black,
),
child: const Text('Close'),
),
],
);
},
);
}
}
// Cleanup
void dispose() {
windowManager.removeListener(this);
trayManager.removeListener(this);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class AppAboutDialog extends StatelessWidget {
const AppAboutDialog({super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
final packageInfo = snapshot.data;
return AlertDialog(
backgroundColor: const Color(0xFF1A1F3A),
title: Row(
children: [
Image.asset(
'assets/logo.png',
width: 32,
height: 32,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.analytics, color: Color(0xFF50E3C2), size: 32),
),
const SizedBox(width: 12),
const Text(
'rmtPocketWatcher',
style: TextStyle(color: Colors.white),
),
],
),
content: SizedBox(
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Version ${packageInfo?.version ?? 'Unknown'}',
style: const TextStyle(
color: Color(0xFF50E3C2),
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
const Text(
'Star Citizen AUEC Price Tracker',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
const SizedBox(height: 8),
const Text(
'Bloomberg-style terminal interface for real-time RMT price monitoring',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
const SizedBox(height: 16),
const Divider(color: Color(0xFF50E3C2)),
const SizedBox(height: 16),
_buildInfoRow('Developer', 'Lambda Banking Conglomerate'),
const SizedBox(height: 8),
_buildInfoRow('Platform', 'Flutter Desktop'),
const SizedBox(height: 8),
_buildInfoRow('Build', packageInfo?.buildNumber ?? 'Unknown'),
const SizedBox(height: 16),
const Text(
'Features:',
style: TextStyle(
color: Color(0xFF50E3C2),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
_buildFeatureItem('Real-time AUEC price tracking'),
_buildFeatureItem('Multiple vendor monitoring'),
_buildFeatureItem('Historical price charts'),
_buildFeatureItem('Price alerts & notifications'),
_buildFeatureItem('System tray integration'),
_buildFeatureItem('Automatic updates'),
const SizedBox(height: 16),
const Text(
'Data Sources:',
style: TextStyle(
color: Color(0xFF50E3C2),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
_buildFeatureItem('Eldorado.gg'),
_buildFeatureItem('PlayerAuctions'),
],
),
),
actions: [
TextButton(
onPressed: () => _launchUrl('https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher'),
child: const Text(
'Source Code',
style: TextStyle(color: Color(0xFF50E3C2)),
),
),
TextButton(
onPressed: () => _launchUrl('https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher/releases'),
child: const Text(
'Releases',
style: TextStyle(color: Color(0xFF50E3C2)),
),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF50E3C2),
foregroundColor: Colors.black,
),
child: const Text('Close'),
),
],
);
},
);
}
Widget _buildInfoRow(String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
'$label:',
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
],
);
}
Widget _buildFeatureItem(String feature) {
return Padding(
padding: const EdgeInsets.only(left: 16, bottom: 4),
child: Row(
children: [
const Icon(
Icons.check_circle,
color: Color(0xFF50E3C2),
size: 12,
),
const SizedBox(width: 8),
Text(
feature,
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
);
}
void _launchUrl(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}
}

View File

@@ -318,8 +318,12 @@ class _PriceChartState extends State<PriceChart> {
_yAxisMax = _baseYAxisMax; _yAxisMax = _baseYAxisMax;
} }
// Calculate 65% of viewport height for the chart
final screenHeight = MediaQuery.of(context).size.height;
final chartHeight = screenHeight * 0.65;
return Container( return Container(
height: 250, // Reduced from 300 for more compact layout height: chartHeight,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF0A0E27), color: const Color(0xFF0A0E27),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@@ -333,23 +337,28 @@ class _PriceChartState extends State<PriceChart> {
_yAxisMax = _baseYAxisMax; _yAxisMax = _baseYAxisMax;
}); });
}, },
child: Listener( child: NotificationListener<ScrollNotification>(
onPointerSignal: (pointerSignal) { onNotification: (ScrollNotification notification) {
if (pointerSignal is PointerScrollEvent) { // Consume scroll notifications to prevent them from bubbling up
setState(() { return true;
// Scroll up = zoom in (decrease Y max), scroll down = zoom out (increase Y max)
final delta = pointerSignal.scrollDelta.dy;
final zoomFactor = delta > 0 ? 1.1 : 0.9; // Zoom sensitivity
_yAxisMax *= zoomFactor;
// Clamp Y-axis max to reasonable bounds
final minY = maxPrice * 0.1; // Don't zoom in too much
final maxY = maxPrice * 10; // Don't zoom out too much
_yAxisMax = _yAxisMax.clamp(minY, maxY);
});
}
}, },
child: Listener(
onPointerSignal: (pointerSignal) {
if (pointerSignal is PointerScrollEvent) {
setState(() {
// Scroll up = zoom in (decrease Y max), scroll down = zoom out (increase Y max)
final delta = pointerSignal.scrollDelta.dy;
final zoomFactor = delta > 0 ? 1.1 : 0.9; // Zoom sensitivity
_yAxisMax *= zoomFactor;
// Clamp Y-axis max to reasonable bounds
final minY = maxPrice * 0.1; // Don't zoom in too much
final maxY = maxPrice * 10; // Don't zoom out too much
_yAxisMax = _yAxisMax.clamp(minY, maxY);
});
}
},
child: LineChart( child: LineChart(
LineChartData( LineChartData(
backgroundColor: const Color(0xFF0A0E27), backgroundColor: const Color(0xFF0A0E27),
@@ -460,6 +469,12 @@ class _PriceChartState extends State<PriceChart> {
getTooltipColor: (touchedSpot) => const Color(0xFF2A2F4A), getTooltipColor: (touchedSpot) => const Color(0xFF2A2F4A),
tooltipRoundedRadius: 4, tooltipRoundedRadius: 4,
tooltipPadding: const EdgeInsets.all(8), tooltipPadding: const EdgeInsets.all(8),
tooltipMargin: 8,
fitInsideHorizontally: true,
fitInsideVertically: true,
rotateAngle: 0,
tooltipHorizontalAlignment: FLHorizontalAlignment.center,
tooltipHorizontalOffset: 0,
getTooltipItems: (List<LineBarSpot> touchedBarSpots) { getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
return touchedBarSpots.map((barSpot) { return touchedBarSpots.map((barSpot) {
final seller = sellers[barSpot.barIndex]; final seller = sellers[barSpot.barIndex];
@@ -485,6 +500,7 @@ class _PriceChartState extends State<PriceChart> {
handleBuiltInTouches: true, handleBuiltInTouches: true,
), ),
), ),
),
), ),
), ),
), ),
@@ -493,117 +509,7 @@ class _PriceChartState extends State<PriceChart> {
}, },
), ),
const SizedBox(height: 12), // Reduced from 16 const SizedBox(height: 12), // Reduced from 16
// X-axis zoom controls // Timeline scrubber and controls (Bloomberg style)
Consumer<PriceProvider>(
builder: (context, provider, child) {
if (provider.historyData == null || provider.historyData!.prices.isEmpty) {
return const SizedBox();
}
return Wrap(
alignment: WrapAlignment.center,
spacing: 4, // Reduced spacing
runSpacing: 8,
children: [
// Zoom out button
IconButton(
onPressed: _xZoomLevel > 1.0 ? () {
setState(() {
_xZoomLevel = (_xZoomLevel / 1.5).clamp(1.0, 10.0);
});
} : null,
icon: const Icon(Icons.zoom_out, color: Color(0xFF50E3C2), size: 18), // Smaller icon
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
disabledBackgroundColor: const Color(0xFF1A1F3A),
minimumSize: const Size(32, 32), // Smaller buttons
),
),
// Left navigation
IconButton(
onPressed: _xCenterPoint > 0.1 ? () {
setState(() {
final step = 0.1 / _xZoomLevel;
_xCenterPoint = (_xCenterPoint - step).clamp(0.0, 1.0);
});
} : null,
icon: const Icon(Icons.chevron_left, color: Color(0xFF50E3C2), size: 18),
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
disabledBackgroundColor: const Color(0xFF1A1F3A),
minimumSize: const Size(32, 32),
),
),
// Zoom level indicator
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // More compact
decoration: BoxDecoration(
color: const Color(0xFF2A2F4A),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'${(_xZoomLevel * 100).toInt()}%',
style: const TextStyle(
color: Color(0xFF50E3C2),
fontSize: 10, // Smaller font
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
),
// Right navigation
IconButton(
onPressed: _xCenterPoint < 0.9 ? () {
setState(() {
final step = 0.1 / _xZoomLevel;
_xCenterPoint = (_xCenterPoint + step).clamp(0.0, 1.0);
});
} : null,
icon: const Icon(Icons.chevron_right, color: Color(0xFF50E3C2), size: 18),
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
disabledBackgroundColor: const Color(0xFF1A1F3A),
minimumSize: const Size(32, 32),
),
),
// Zoom in button
IconButton(
onPressed: _xZoomLevel < 10.0 ? () {
setState(() {
_xZoomLevel = (_xZoomLevel * 1.5).clamp(1.0, 10.0);
});
} : null,
icon: const Icon(Icons.zoom_in, color: Color(0xFF50E3C2), size: 18),
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
disabledBackgroundColor: const Color(0xFF1A1F3A),
minimumSize: const Size(32, 32),
),
),
// Reset button
ElevatedButton.icon(
onPressed: () {
setState(() {
_xZoomLevel = 1.0;
_xCenterPoint = 0.5;
_yAxisMax = _baseYAxisMax;
});
},
icon: const Icon(Icons.refresh, size: 14), // Smaller icon
label: const Text('Reset', style: TextStyle(fontSize: 10)), // Shorter text, smaller font
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
foregroundColor: const Color(0xFF50E3C2),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // More compact
minimumSize: const Size(0, 32), // Smaller height
),
),
],
);
},
),
const SizedBox(height: 12), // Reduced from 16
// Timeline scrubber (Bloomberg style)
Consumer<PriceProvider>( Consumer<PriceProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
if (provider.historyData == null || provider.historyData!.prices.isEmpty) { if (provider.historyData == null || provider.historyData!.prices.isEmpty) {
@@ -621,45 +527,155 @@ class _PriceChartState extends State<PriceChart> {
final firstDate = DateTime.fromMillisecondsSinceEpoch(sortedTimestamps.first); final firstDate = DateTime.fromMillisecondsSinceEpoch(sortedTimestamps.first);
final lastDate = DateTime.fromMillisecondsSinceEpoch(sortedTimestamps.last); final lastDate = DateTime.fromMillisecondsSinceEpoch(sortedTimestamps.last);
return Container( return Column(
height: 40, children: [
padding: const EdgeInsets.symmetric(horizontal: 16), // Timeline scrubber
decoration: BoxDecoration( Container(
color: const Color(0xFF2A2F4A), // Lighter gray background height: 40,
borderRadius: BorderRadius.circular(4), padding: const EdgeInsets.symmetric(horizontal: 16),
), decoration: BoxDecoration(
child: Row( color: const Color(0xFF2A2F4A), // Lighter gray background
children: [ borderRadius: BorderRadius.circular(4),
Text(
'${firstDate.month}/${firstDate.day}',
style: const TextStyle(
color: Color(0xFF888888),
fontSize: 10,
fontFamily: 'monospace',
),
), ),
Expanded( child: Row(
child: Slider( children: [
value: _xCenterPoint, Text(
onChanged: (value) { '${firstDate.month}/${firstDate.day}',
setState(() { style: const TextStyle(
_xCenterPoint = value; color: Color(0xFF888888),
}); fontSize: 10,
}, fontFamily: 'monospace',
activeColor: const Color(0xFF50E3C2), ),
inactiveColor: const Color(0xFF1A1F3A), ),
), Expanded(
child: Slider(
value: _xCenterPoint,
onChanged: (value) {
setState(() {
_xCenterPoint = value;
});
},
activeColor: const Color(0xFF50E3C2),
inactiveColor: const Color(0xFF1A1F3A),
),
),
Text(
'${lastDate.month}/${lastDate.day}',
style: const TextStyle(
color: Color(0xFF888888),
fontSize: 10,
fontFamily: 'monospace',
),
),
],
), ),
Text( ),
'${lastDate.month}/${lastDate.day}', const SizedBox(height: 8),
style: const TextStyle( // Centered X-axis zoom controls
color: Color(0xFF888888), Center(
fontSize: 10, child: Wrap(
fontFamily: 'monospace', alignment: WrapAlignment.center,
), crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4, // Reduced spacing
runSpacing: 8,
children: [
// Zoom out button
IconButton(
onPressed: _xZoomLevel > 1.0 ? () {
setState(() {
_xZoomLevel = (_xZoomLevel / 1.5).clamp(1.0, 10.0);
});
} : null,
icon: const Icon(Icons.zoom_out, color: Color(0xFF50E3C2), size: 18), // Smaller icon
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
disabledBackgroundColor: const Color(0xFF1A1F3A),
minimumSize: const Size(32, 32), // Smaller buttons
),
),
// Left navigation
IconButton(
onPressed: _xCenterPoint > 0.1 ? () {
setState(() {
final step = 0.1 / _xZoomLevel;
_xCenterPoint = (_xCenterPoint - step).clamp(0.0, 1.0);
});
} : null,
icon: const Icon(Icons.chevron_left, color: Color(0xFF50E3C2), size: 18),
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
disabledBackgroundColor: const Color(0xFF1A1F3A),
minimumSize: const Size(32, 32),
),
),
// Zoom level indicator
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // More compact
decoration: BoxDecoration(
color: const Color(0xFF2A2F4A),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'${(_xZoomLevel * 100).toInt()}%',
style: const TextStyle(
color: Color(0xFF50E3C2),
fontSize: 10, // Smaller font
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
),
// Right navigation
IconButton(
onPressed: _xCenterPoint < 0.9 ? () {
setState(() {
final step = 0.1 / _xZoomLevel;
_xCenterPoint = (_xCenterPoint + step).clamp(0.0, 1.0);
});
} : null,
icon: const Icon(Icons.chevron_right, color: Color(0xFF50E3C2), size: 18),
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
disabledBackgroundColor: const Color(0xFF1A1F3A),
minimumSize: const Size(32, 32),
),
),
// Zoom in button
IconButton(
onPressed: _xZoomLevel < 10.0 ? () {
setState(() {
_xZoomLevel = (_xZoomLevel * 1.5).clamp(1.0, 10.0);
});
} : null,
icon: const Icon(Icons.zoom_in, color: Color(0xFF50E3C2), size: 18),
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
disabledBackgroundColor: const Color(0xFF1A1F3A),
minimumSize: const Size(32, 32),
),
),
// Reset button
ElevatedButton.icon(
onPressed: () {
setState(() {
_xZoomLevel = 1.0;
_xCenterPoint = 0.5;
_yAxisMax = _baseYAxisMax;
});
},
icon: const Icon(Icons.refresh, size: 14), // Smaller icon
label: const Text('Reset', style: TextStyle(fontSize: 10)), // Shorter text, smaller font
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2A2F4A),
foregroundColor: const Color(0xFF50E3C2),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // More compact
minimumSize: const Size(0, 32), // Smaller height
),
),
],
), ),
], ),
), ],
); );
}, },
), ),

View File

@@ -0,0 +1,308 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../providers/update_provider.dart';
import '../services/update_service.dart';
class UpdateNotificationBanner extends StatelessWidget {
const UpdateNotificationBanner({super.key});
@override
Widget build(BuildContext context) {
return Consumer<UpdateProvider>(
builder: (context, updateProvider, child) {
if (!updateProvider.hasUpdate) {
return const SizedBox.shrink();
}
final update = updateProvider.availableUpdate!;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF1A4B3A), // Dark green background
border: Border.all(color: const Color(0xFF50E3C2), width: 1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(
Icons.system_update,
color: Color(0xFF50E3C2),
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Update Available: v${update.latestVersion}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Text(
'Current: v${update.currentVersion}',
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
),
),
],
),
),
TextButton(
onPressed: () => _showUpdateDialog(context, update),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF50E3C2),
),
child: const Text('View'),
),
IconButton(
onPressed: () => updateProvider.dismissUpdate(),
icon: const Icon(Icons.close, color: Colors.white70, size: 18),
tooltip: 'Dismiss',
),
],
),
);
},
);
}
void _showUpdateDialog(BuildContext context, UpdateInfo update) {
showDialog(
context: context,
builder: (context) => UpdateDialog(update: update),
);
}
}
class UpdateDialog extends StatelessWidget {
final UpdateInfo update;
const UpdateDialog({super.key, required this.update});
@override
Widget build(BuildContext context) {
final asset = update.getAssetForCurrentPlatform();
return AlertDialog(
backgroundColor: const Color(0xFF1A1F3A),
title: Row(
children: [
const Icon(Icons.system_update, color: Color(0xFF50E3C2)),
const SizedBox(width: 8),
Text(
'Update Available',
style: const TextStyle(color: Colors.white),
),
],
),
content: SizedBox(
width: 500,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildVersionInfo(),
const SizedBox(height: 16),
_buildReleaseInfo(),
if (update.releaseNotes.isNotEmpty) ...[
const SizedBox(height: 16),
_buildReleaseNotes(),
],
if (asset != null) ...[
const SizedBox(height: 16),
_buildDownloadInfo(asset),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Later', style: TextStyle(color: Colors.white70)),
),
if (asset != null)
ElevatedButton(
onPressed: () => _downloadUpdate(asset.downloadUrl),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF50E3C2),
foregroundColor: Colors.black,
),
child: const Text('Download'),
),
ElevatedButton(
onPressed: () => _openReleasePage(update.releaseUrl),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF50E3C2),
foregroundColor: Colors.black,
),
child: const Text('View Release'),
),
],
);
}
Widget _buildVersionInfo() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0A0E27),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF50E3C2).withValues(alpha: 0.3)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Current Version',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
Text(
'v${update.currentVersion}',
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
const Icon(Icons.arrow_forward, color: Color(0xFF50E3C2)),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text(
'Latest Version',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
Text(
'v${update.latestVersion}',
style: const TextStyle(color: Color(0xFF50E3C2), fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
],
),
);
}
Widget _buildReleaseInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
update.releaseName,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Released ${_formatDate(update.publishedAt)}',
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
);
}
Widget _buildReleaseNotes() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Release Notes',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0A0E27),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF50E3C2).withValues(alpha: 0.3)),
),
child: Text(
update.releaseNotes,
style: const TextStyle(color: Colors.white70, fontSize: 12),
maxLines: 8,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
Widget _buildDownloadInfo(ReleaseAsset asset) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0A0E27),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF50E3C2).withValues(alpha: 0.3)),
),
child: Row(
children: [
const Icon(Icons.download, color: Color(0xFF50E3C2), size: 20),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
asset.name,
style: const TextStyle(color: Colors.white, fontSize: 14),
),
Text(
asset.formattedSize,
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
],
),
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays > 0) {
return '${difference.inDays} day${difference.inDays == 1 ? '' : 's'} ago';
} else if (difference.inHours > 0) {
return '${difference.inHours} hour${difference.inHours == 1 ? '' : 's'} ago';
} else {
return '${difference.inMinutes} minute${difference.inMinutes == 1 ? '' : 's'} ago';
}
}
void _downloadUpdate(String downloadUrl) async {
final uri = Uri.parse(downloadUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}
void _openReleasePage(String releaseUrl) async {
final uri = Uri.parse(releaseUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}
}

View File

@@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
archive:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -33,6 +41,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -49,6 +73,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
console:
dependency: transitive
description:
name: console
sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a
url: "https://pub.dev"
source: hosted
version: "4.1.0"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -109,10 +141,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237" sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.70.2" version: "0.68.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -126,38 +158,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.2.1" version: "5.2.1"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "5.0.0"
flutter_local_notifications: flutter_local_notifications:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_local_notifications name: flutter_local_notifications
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "18.0.1" version: "17.2.4"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
name: flutter_local_notifications_linux name: flutter_local_notifications_linux
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "4.0.1"
flutter_local_notifications_platform_interface: flutter_local_notifications_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: flutter_local_notifications_platform_interface name: flutter_local_notifications_platform_interface
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.0" version: "7.2.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -168,6 +208,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
get_it:
dependency: transitive
description:
name: get_it
sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9
url: "https://pub.dev"
source: hosted
version: "8.3.0"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -184,14 +232,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "51555e36056541237b15b57afc31a0f53d4f9aefd9bd00873a6dc0090e54e332"
url: "https://pub.dev"
source: hosted
version: "4.6.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.20.2" version: "0.19.0"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@@ -228,10 +284,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "5.1.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -264,6 +320,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.17.0"
msix:
dependency: "direct dev"
description:
name: msix
sha256: f88033fcb9e0dd8de5b18897cbebbd28ea30596810f4a7c86b12b0c03ace87e5
url: "https://pub.dev"
source: hosted
version: "3.16.12"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -272,6 +336,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
url: "https://pub.dev"
source: hosted
version: "8.3.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -352,6 +440,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
provider: provider:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -360,46 +456,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.5+1" version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
name: screen_retriever name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.1.9"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -569,10 +641,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: timezone name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.10.1" version: "0.9.4"
tray_manager: tray_manager:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -589,6 +661,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
url: "https://pub.dev"
source: hosted
version: "6.3.28"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
url: "https://pub.dev"
source: hosted
version: "6.3.6"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -609,34 +745,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "0.5.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel: web_socket_channel:
dependency: "direct main" dependency: "direct main"
description: description:
name: web_socket_channel name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "2.4.5"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
window_manager: window_manager:
dependency: "direct main" dependency: "direct main"
description: description:
name: window_manager name: window_manager
sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.3" version: "0.3.9"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -646,13 +782,21 @@ packages:
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml: xml:
dependency: transitive dependency: "direct main"
description: description:
name: xml name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.6.1" version: "6.6.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks: sdks:
dart: ">=3.10.3 <4.0.0" dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0" flutter: ">=3.35.0"

View File

@@ -1,5 +1,5 @@
name: rmtpocketwatcher name: rmtpocketwatcher
description: "A new Flutter project." description: "Track USD vs aUEC"
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
@@ -16,10 +16,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 1.0.4
environment: environment:
sdk: ^3.10.3 sdk: '>=3.5.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
@@ -33,39 +33,48 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.6
# WebSocket communication # WebSocket communication
web_socket_channel: ^3.0.1 web_socket_channel: ^2.4.5
# HTTP requests # HTTP requests
http: ^1.2.2 http: ^1.1.2
# State management # State management
provider: ^6.1.2 provider: ^6.1.2
# Charts # Charts
fl_chart: ^0.70.1 fl_chart: ^0.68.0
# Local notifications # Local notifications
flutter_local_notifications: ^18.0.1 flutter_local_notifications: ^17.2.3
# Local storage # Local storage
shared_preferences: ^2.3.3 shared_preferences: ^2.2.3
sqflite: ^2.4.1 sqflite: ^2.3.3
path_provider: ^2.1.5 path_provider: ^2.1.4
# Environment variables # Environment variables
flutter_dotenv: ^5.2.1 flutter_dotenv: ^5.1.0
# Intl for formatting # Intl for formatting
intl: ^0.20.1 intl: ^0.19.0
# Window management (desktop) # Window management (desktop)
window_manager: ^0.4.3 window_manager: ^0.3.9
# System tray (desktop) # System tray (desktop)
tray_manager: ^0.2.4 tray_manager: ^0.2.3
# Package info for version checking
package_info_plus: ^8.0.2
# URL launcher for opening release pages
url_launcher: ^6.3.1
# XML parsing for RSS feeds
xml: ^6.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -76,7 +85,21 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your # activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^6.0.0 flutter_lints: ^5.0.0
# Windows installer creation
msix: ^3.16.8
# App icon generation
flutter_launcher_icons: ^0.14.3
# Flutter Launcher Icons configuration
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/logo.png"
adaptive_icon_background: "#1a1a2e"
adaptive_icon_foreground: "assets/logo.png"
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec
@@ -121,3 +144,22 @@ flutter:
# #
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package # see https://flutter.dev/to/font-from-package
# MSIX configuration for Windows installer
msix_config:
display_name: rmtPocketWatcher
publisher_display_name: Lambda Banking Conglomerate
identity_name: LambdaBankingConglomerate.rmtPocketWatcher
msix_version: 1.0.4.0
description: Star Citizen AUEC Price Tracker - Bloomberg-style terminal interface for real-time RMT price monitoring
publisher: CN=Lambda Banking Conglomerate, O=Lambda Banking Conglomerate, C=US
logo_path: assets/logo.png
start_menu_icon_path: assets/logo.png
tile_icon_path: assets/logo.png
icons_background_color: transparent
architecture: x64
capabilities: 'internetClient,privateNetworkClientServer'
store: false
install_certificate: false
certificate_path: certificates/rmtPocketWatcher.pfx
certificate_password: rmtPocketWatcher2024!

View File

@@ -0,0 +1,110 @@
# Sign Executable Script for rmtPocketWatcher
# Signs the standalone executable with the self-signed certificate
param(
[string]$ExePath = "build\windows\standalone\rmtpocketwatcher.exe",
[string]$CertPath = "certificates\rmtPocketWatcher.pfx",
[string]$CertPassword = $(if ($env:CERT_PASSWORD) { $env:CERT_PASSWORD } else { "rmtPocketWatcher2024!" }),
[switch]$Force = $false
)
$ErrorActionPreference = "Stop"
Write-Host "Signing rmtPocketWatcher Executable" -ForegroundColor Green
Write-Host "===================================" -ForegroundColor Green
# Check if executable exists
if (-not (Test-Path $ExePath)) {
Write-Error "Executable not found at: $ExePath"
Write-Host "Build the application first using .\build_windows.ps1" -ForegroundColor Yellow
exit 1
}
# Check if certificate exists
if (-not (Test-Path $CertPath)) {
Write-Error "Certificate not found at: $CertPath"
Write-Host "Create a certificate first using .\create_certificate.ps1" -ForegroundColor Yellow
exit 1
}
# Check if already signed (unless forcing)
if (-not $Force) {
try {
$signature = Get-AuthenticodeSignature -FilePath $ExePath
if ($signature.Status -eq "Valid") {
Write-Host "Executable is already signed and valid" -ForegroundColor Green
Write-Host "Certificate: $($signature.SignerCertificate.Subject)" -ForegroundColor Cyan
Write-Host "Use -Force to re-sign" -ForegroundColor Yellow
return
}
} catch {
# File not signed or error checking, continue with signing
}
}
# Find SignTool
Write-Host "Looking for SignTool..." -ForegroundColor Yellow
$signtool = $null
# Common SignTool locations
$signToolPaths = @(
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe",
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe",
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.18362.0\x64\signtool.exe"
)
foreach ($path in $signToolPaths) {
if (Test-Path $path) {
$signtool = $path
break
}
}
# If not found in common locations, search for it
if (-not $signtool) {
Write-Host "Searching for SignTool in Windows Kits..." -ForegroundColor Yellow
$foundSignTools = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits" -Recurse -Name "signtool.exe" -ErrorAction SilentlyContinue
if ($foundSignTools) {
$signtool = Join-Path "${env:ProgramFiles(x86)}\Windows Kits" $foundSignTools[0]
}
}
if (-not $signtool -or -not (Test-Path $signtool)) {
Write-Error "SignTool not found. Please install Windows SDK."
Write-Host "Download from: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/" -ForegroundColor Yellow
exit 1
}
Write-Host "Found SignTool: $signtool" -ForegroundColor Cyan
# Sign the executable
Write-Host "Signing executable: $ExePath" -ForegroundColor Yellow
try {
& $signtool sign `
/f $CertPath `
/p $CertPassword `
/fd SHA256 `
/tr http://timestamp.digicert.com `
/td SHA256 `
/d "rmtPocketWatcher" `
/du "https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher" `
$ExePath
if ($LASTEXITCODE -eq 0) {
Write-Host "✅ Executable signed successfully!" -ForegroundColor Green
# Verify the signature
$signature = Get-AuthenticodeSignature -FilePath $ExePath
Write-Host "Signature Status: $($signature.Status)" -ForegroundColor Cyan
Write-Host "Signer Certificate: $($signature.SignerCertificate.Subject)" -ForegroundColor Cyan
Write-Host "Timestamp: $($signature.TimeStamperCertificate.NotBefore)" -ForegroundColor Cyan
} else {
Write-Error "Failed to sign executable (Exit code: $LASTEXITCODE)"
}
} catch {
Write-Error "Error signing executable: $($_.Exception.Message)"
}
Write-Host "`n🎉 Code signing completed!" -ForegroundColor Green
Write-Host "The executable should now be trusted by Windows" -ForegroundColor Green

View File

@@ -0,0 +1,69 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:rmtpocketwatcher/services/update_service.dart';
void main() {
group('UpdateService', () {
late UpdateService updateService;
setUp(() {
updateService = UpdateService();
});
test('version extraction works correctly', () {
// Test version extraction
expect(updateService.extractVersionFromTag('v1.2.3'), equals('1.2.3'));
expect(updateService.extractVersionFromTag('1.2.3'), equals('1.2.3'));
expect(updateService.extractVersionFromTag('v2.0.0'), equals('2.0.0'));
});
test('newer version detection works correctly', () {
expect(updateService.isNewerVersion('1.2.4', '1.2.3'), isTrue);
expect(updateService.isNewerVersion('1.3.0', '1.2.9'), isTrue);
expect(updateService.isNewerVersion('2.0.0', '1.9.9'), isTrue);
expect(updateService.isNewerVersion('1.2.3', '1.2.3'), isFalse);
expect(updateService.isNewerVersion('1.2.2', '1.2.3'), isFalse);
expect(updateService.isNewerVersion('1.1.9', '1.2.0'), isFalse);
});
test('platform asset detection works', () {
final assets = [
ReleaseAsset(
name: 'rmtPocketWatcher-windows-x64.exe',
downloadUrl: 'https://example.com/windows.exe',
size: 1024,
contentType: 'application/octet-stream',
),
ReleaseAsset(
name: 'rmtPocketWatcher-macos.dmg',
downloadUrl: 'https://example.com/macos.dmg',
size: 2048,
contentType: 'application/octet-stream',
),
ReleaseAsset(
name: 'rmtPocketWatcher-linux.appimage',
downloadUrl: 'https://example.com/linux.appimage',
size: 3072,
contentType: 'application/octet-stream',
),
];
final updateInfo = UpdateInfo(
currentVersion: '1.0.0',
latestVersion: '1.1.0',
releaseUrl: 'https://example.com/release',
releaseName: 'Test Release',
releaseNotes: 'Test notes',
publishedAt: DateTime.now(),
assets: assets,
);
// This test would need to mock the platform detection
// For now, just verify the asset list is properly structured
expect(updateInfo.assets.length, equals(3));
expect(updateInfo.assets.first.name, contains('windows'));
});
});
}
// Note: The methods extractVersionFromTag and isNewerVersion are now public
// and marked with @visibleForTesting for testing purposes

View File

@@ -43,6 +43,10 @@ function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>") target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
# Use static runtime for truly portable builds (no VC++ Redistributable required)
set_property(TARGET ${TARGET} PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endfunction() endfunction()
# Flutter library and tool build rules. # Flutter library and tool build rules.