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

This commit is contained in:
2025-12-15 00:05:29 -05:00
parent 9ff0d62651
commit 110c5d99a1
25 changed files with 2647 additions and 268 deletions

View File

@@ -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
});
}

View File

@@ -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(

View File

@@ -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;
}
}

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

@@ -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
),
),
],
),
],
),
),
],
);
},
),