Signing, Installer, New Workflows
This commit is contained in:
@@ -38,6 +38,9 @@ Future<void> main() async {
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
|
||||
// Initialize window service after window is ready
|
||||
// Context will be set later from the home screen
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import '../providers/price_provider.dart';
|
||||
import '../providers/update_provider.dart';
|
||||
import '../services/window_service.dart';
|
||||
import '../widgets/price_chart.dart';
|
||||
import '../widgets/alerts_panel.dart';
|
||||
import '../widgets/vendor_table.dart';
|
||||
@@ -19,13 +21,39 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize update provider
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// 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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -36,29 +64,32 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
if (!kIsWeb && (Theme.of(context).platform == TargetPlatform.windows ||
|
||||
Theme.of(context).platform == TargetPlatform.macOS ||
|
||||
Theme.of(context).platform == TargetPlatform.linux))
|
||||
Container(
|
||||
height: 40,
|
||||
color: const Color(0xFF1A1F3A),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
const Text(
|
||||
'rmtPocketWatcher',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
GestureDetector(
|
||||
onPanStart: (details) => windowManager.startDragging(),
|
||||
onDoubleTap: () => WindowService().maximizeWindow(),
|
||||
child: Container(
|
||||
height: 40,
|
||||
color: const Color(0xFF1A1F3A),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
const Text(
|
||||
'rmtPocketWatcher',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Text(
|
||||
'Lambda Banking Conglomerate',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF888888),
|
||||
fontSize: 12,
|
||||
const Spacer(),
|
||||
const Text(
|
||||
'Lambda Banking Conglomerate',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF888888),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 16),
|
||||
// Update check button
|
||||
Consumer<UpdateProvider>(
|
||||
builder: (context, updateProvider, child) {
|
||||
@@ -86,21 +117,38 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
// Minimize button (minimize to tray)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.minimize, color: Colors.white, size: 16),
|
||||
onPressed: () {
|
||||
// Minimize window - implement with window_manager
|
||||
onPressed: () => WindowService().minimizeToTray(),
|
||||
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(
|
||||
icon: const Icon(Icons.close, color: Colors.white, size: 16),
|
||||
onPressed: () {
|
||||
// Close window - implement with window_manager
|
||||
},
|
||||
onPressed: () => WindowService().closeWindow(),
|
||||
tooltip: 'Exit application',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Main content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
|
||||
@@ -143,33 +143,31 @@ class UpdateService {
|
||||
final baseUrl = 'https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher/releases/download/v$version';
|
||||
|
||||
return [
|
||||
// Windows Full Package
|
||||
ReleaseAsset(
|
||||
name: 'rmtPocketWatcher-windows-x64.exe',
|
||||
downloadUrl: '$baseUrl/rmtPocketWatcher-windows-x64.exe',
|
||||
name: 'rmtPocketWatcher-Windows-v$version.zip',
|
||||
downloadUrl: '$baseUrl/rmtPocketWatcher-Windows-v$version.zip',
|
||||
size: 0, // Unknown size from RSS
|
||||
contentType: 'application/octet-stream',
|
||||
contentType: 'application/zip',
|
||||
),
|
||||
// Windows Portable (single exe)
|
||||
ReleaseAsset(
|
||||
name: 'rmtPocketWatcher-windows-x64.msi',
|
||||
downloadUrl: '$baseUrl/rmtPocketWatcher-windows-x64.msi',
|
||||
name: 'rmtPocketWatcher-Windows-Portable-v$version.zip',
|
||||
downloadUrl: '$baseUrl/rmtPocketWatcher-Windows-Portable-v$version.zip',
|
||||
size: 0,
|
||||
contentType: 'application/octet-stream',
|
||||
contentType: 'application/zip',
|
||||
),
|
||||
// Windows MSIX Installer
|
||||
ReleaseAsset(
|
||||
name: 'rmtPocketWatcher-macos.dmg',
|
||||
downloadUrl: '$baseUrl/rmtPocketWatcher-macos.dmg',
|
||||
name: 'rmtPocketWatcher-v$version.msix',
|
||||
downloadUrl: '$baseUrl/rmtPocketWatcher-v$version.msix',
|
||||
size: 0,
|
||||
contentType: 'application/octet-stream',
|
||||
contentType: 'application/msix',
|
||||
),
|
||||
// Android APK
|
||||
ReleaseAsset(
|
||||
name: 'rmtPocketWatcher-linux.appimage',
|
||||
downloadUrl: '$baseUrl/rmtPocketWatcher-linux.appimage',
|
||||
size: 0,
|
||||
contentType: 'application/octet-stream',
|
||||
),
|
||||
ReleaseAsset(
|
||||
name: 'rmtPocketWatcher-android.apk',
|
||||
downloadUrl: '$baseUrl/rmtPocketWatcher-android.apk',
|
||||
name: 'rmtPocketWatcher-Android-v$version.apk',
|
||||
downloadUrl: '$baseUrl/rmtPocketWatcher-Android-v$version.apk',
|
||||
size: 0,
|
||||
contentType: 'application/vnd.android.package-archive',
|
||||
),
|
||||
@@ -200,36 +198,38 @@ class UpdateInfo {
|
||||
ReleaseAsset? getAssetForCurrentPlatform() {
|
||||
if (kIsWeb) return null;
|
||||
|
||||
String platformPattern;
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.windows:
|
||||
platformPattern = r'windows|win|\.exe$|\.msi$';
|
||||
break;
|
||||
// Prefer portable version for Windows
|
||||
var portable = assets.where((asset) => asset.name.contains('Portable')).firstOrNull;
|
||||
if (portable != null) return portable;
|
||||
|
||||
// Fall back to full Windows package
|
||||
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:
|
||||
platformPattern = r'macos|mac|darwin|\.dmg$|\.pkg$';
|
||||
break;
|
||||
return assets.where((asset) =>
|
||||
RegExp(r'macOS|macos|mac|darwin|\.dmg$|\.pkg$', caseSensitive: false).hasMatch(asset.name)
|
||||
).firstOrNull;
|
||||
|
||||
case TargetPlatform.linux:
|
||||
platformPattern = r'linux|\.deb$|\.rpm$|\.appimage$';
|
||||
break;
|
||||
return assets.where((asset) =>
|
||||
RegExp(r'Linux|linux|\.deb$|\.rpm$|\.appimage$', caseSensitive: false).hasMatch(asset.name)
|
||||
).firstOrNull;
|
||||
|
||||
case TargetPlatform.android:
|
||||
platformPattern = r'android|\.apk$';
|
||||
break;
|
||||
return assets.where((asset) => asset.name.endsWith('.apk')).firstOrNull;
|
||||
|
||||
case TargetPlatform.iOS:
|
||||
platformPattern = r'ios|\.ipa$';
|
||||
break;
|
||||
return assets.where((asset) => asset.name.endsWith('.ipa')).firstOrNull;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
final regex = RegExp(platformPattern, caseSensitive: false);
|
||||
|
||||
for (final asset in assets) {
|
||||
if (regex.hasMatch(asset.name)) {
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
315
flutter_app/lib/services/window_service.dart
Normal file
315
flutter_app/lib/services/window_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
176
flutter_app/lib/widgets/about_dialog.dart
Normal file
176
flutter_app/lib/widgets/about_dialog.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,6 +460,12 @@ class _PriceChartState extends State<PriceChart> {
|
||||
getTooltipColor: (touchedSpot) => const Color(0xFF2A2F4A),
|
||||
tooltipRoundedRadius: 4,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
tooltipMargin: 8,
|
||||
fitInsideHorizontally: true,
|
||||
fitInsideVertically: true,
|
||||
rotateAngle: 0,
|
||||
tooltipHorizontalAlignment: FLHorizontalAlignment.center,
|
||||
tooltipHorizontalOffset: 0,
|
||||
getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
|
||||
return touchedBarSpots.map((barSpot) {
|
||||
final seller = sellers[barSpot.barIndex];
|
||||
@@ -493,117 +499,7 @@ class _PriceChartState extends State<PriceChart> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12), // Reduced from 16
|
||||
// X-axis zoom controls
|
||||
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)
|
||||
// Timeline scrubber and controls (Bloomberg style)
|
||||
Consumer<PriceProvider>(
|
||||
builder: (context, provider, child) {
|
||||
if (provider.historyData == null || provider.historyData!.prices.isEmpty) {
|
||||
@@ -621,45 +517,155 @@ class _PriceChartState extends State<PriceChart> {
|
||||
final firstDate = DateTime.fromMillisecondsSinceEpoch(sortedTimestamps.first);
|
||||
final lastDate = DateTime.fromMillisecondsSinceEpoch(sortedTimestamps.last);
|
||||
|
||||
return Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2A2F4A), // Lighter gray background
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${firstDate.month}/${firstDate.day}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF888888),
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
// Timeline scrubber
|
||||
Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2A2F4A), // Lighter gray background
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _xCenterPoint,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_xCenterPoint = value;
|
||||
});
|
||||
},
|
||||
activeColor: const Color(0xFF50E3C2),
|
||||
inactiveColor: const Color(0xFF1A1F3A),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${firstDate.month}/${firstDate.day}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF888888),
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
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}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF888888),
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Centered X-axis zoom controls
|
||||
Center(
|
||||
child: Wrap(
|
||||
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
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user