Added xp-overlay feature
This commit is contained in:
parent
4c07690e85
commit
dda6bfff42
@ -28,7 +28,9 @@
|
||||
gtk3
|
||||
pcre
|
||||
libepoxy
|
||||
# For drift
|
||||
glib.dev
|
||||
sysprof
|
||||
# For drift / sqlite3 dart
|
||||
sqlite
|
||||
# For xp overlay
|
||||
gtk-layer-shell
|
||||
|
@ -185,8 +185,15 @@ Requirements:
|
||||
break;
|
||||
case 'test':
|
||||
if (parts.length > 1) {
|
||||
final level = int.tryParse(parts[1]) ?? 1;
|
||||
_monitor.testTheme(level);
|
||||
final levelOrCommand = parts[1].toLowerCase();
|
||||
if (levelOrCommand == 'overlay') {
|
||||
_testOverlay(parts.length > 2 ? int.tryParse(parts[2]) : null);
|
||||
} else {
|
||||
final level = int.tryParse(levelOrCommand) ?? 1;
|
||||
_monitor.testTheme(level);
|
||||
}
|
||||
} else {
|
||||
print('Usage: test [level|overlay] - Specify a level number or "overlay" to test XP overlay');
|
||||
}
|
||||
break;
|
||||
case 'restore':
|
||||
@ -203,6 +210,7 @@ Requirements:
|
||||
Available commands:
|
||||
- stats: Show current productivity stats
|
||||
- test [level]: Test theme for specific level
|
||||
- test overlay [value]: Test XP overlay at cursor position
|
||||
- restore: Restore desktop backup
|
||||
- refresh: Refresh base config from current system config
|
||||
- build: Build Flutter dashboard and copy to static files
|
||||
@ -246,6 +254,32 @@ void _handleBuildCommand() async {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test the XP overlay with an optional XP value
|
||||
Future<void> _testOverlay(int? xpValue) async {
|
||||
print('🧪 Testing XP overlay system...');
|
||||
final windowManager = WindowManagerDetector.instance.detect();
|
||||
|
||||
if (windowManager == WindowManager.unknown) {
|
||||
print('❌ No supported window manager detected');
|
||||
return;
|
||||
}
|
||||
|
||||
final overlayManager = _monitor.cursorOverlayManager;
|
||||
if (overlayManager == null) {
|
||||
print('❌ Overlay manager not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
final available = await overlayManager.isAvailable();
|
||||
if (!available) {
|
||||
print('❌ Overlay system is not available. Check logs for details.');
|
||||
return;
|
||||
}
|
||||
|
||||
print('✅ Overlay system is available, showing test overlay...');
|
||||
await overlayManager.testOverlay(testXP: xpValue ?? 42);
|
||||
}
|
||||
|
||||
Future<void> _quit() async {
|
||||
_monitor.stop();
|
||||
await _dashboardServer.stop();
|
||||
|
@ -92,6 +92,16 @@ class ConfigManager {
|
||||
},
|
||||
"zoom_multipliers": {"active_meeting": 8, "background_meeting": 5, "zoom_focused": 2, "zoom_background": 0},
|
||||
},
|
||||
"overlay": {
|
||||
"enabled": true,
|
||||
"app_path": "", // Auto-detect if empty
|
||||
"duration_seconds": 3,
|
||||
"colors": {
|
||||
"regular": "#4CAF50", // Green
|
||||
"high": "#FF9800", // Orange
|
||||
"bonus": "#FFC107", // Amber
|
||||
},
|
||||
},
|
||||
"achievements": {
|
||||
"level_based": {
|
||||
"5": {
|
||||
@ -199,6 +209,15 @@ class ConfigManager {
|
||||
_ensureInitialized();
|
||||
return _config!['logging'] ?? {};
|
||||
}
|
||||
|
||||
Map<String, dynamic> get overlay {
|
||||
_ensureInitialized();
|
||||
return _config!['overlay'] ?? {
|
||||
"enabled": true,
|
||||
"app_path": "",
|
||||
"duration_seconds": 3,
|
||||
};
|
||||
}
|
||||
|
||||
// Specific getters for commonly used values
|
||||
int getBaseXP(String activityType) {
|
||||
@ -270,6 +289,19 @@ class ConfigManager {
|
||||
int calculateLevel(int totalXP) {
|
||||
return (totalXP / getXPPerLevel()).floor() + 1;
|
||||
}
|
||||
|
||||
// Overlay configuration getters
|
||||
String? getOverlayAppPath() {
|
||||
return overlay['app_path'] as String?;
|
||||
}
|
||||
|
||||
bool isOverlayEnabled() {
|
||||
return overlay['enabled'] as bool? ?? true;
|
||||
}
|
||||
|
||||
int getOverlayDuration() {
|
||||
return overlay['duration_seconds'] as int? ?? 3;
|
||||
}
|
||||
|
||||
// Update configuration programmatically
|
||||
Future<void> updateConfig(String path, dynamic value) async {
|
||||
|
@ -66,13 +66,22 @@ class ProductivityMonitor {
|
||||
|
||||
// Initialize cursor overlay manager
|
||||
if (_windowManager != null) {
|
||||
final overlayAppPath = '/home/nate/source/xp_nix/xp_overlay_app/build/linux/x64/release/bundle/xp_overlay_app';
|
||||
_cursorOverlayManager = CursorOverlayManager(
|
||||
windowManager: _windowManager!,
|
||||
overlayAppPath: overlayAppPath,
|
||||
enabled: ConfigManager.instance.isOverlayEnabled(),
|
||||
);
|
||||
|
||||
// Log overlay availability on startup
|
||||
_cursorOverlayManager!.isAvailable().then((available) {
|
||||
if (available) {
|
||||
Logger.info('XP cursor overlay system initialized successfully');
|
||||
} else {
|
||||
Logger.warn('XP cursor overlay system is not available');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_cursorOverlayManager = null;
|
||||
Logger.warn('Window manager not available, cursor overlay disabled');
|
||||
}
|
||||
|
||||
// Initialize zoom detector (only if not in test mode)
|
||||
@ -739,6 +748,9 @@ class ProductivityMonitor {
|
||||
Future<void> checkForLevelUpNow() async {
|
||||
await _checkForLevelUp();
|
||||
}
|
||||
|
||||
/// Get the cursor overlay manager (used for testing)
|
||||
CursorOverlayManager? get cursorOverlayManager => _cursorOverlayManager;
|
||||
|
||||
// WebSocket broadcasting methods
|
||||
void _broadcastLevelUp(int newLevel) {
|
||||
|
@ -1,24 +1,83 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert'; // For utf8
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../interfaces/i_window_manager.dart';
|
||||
import '../logging/logger.dart';
|
||||
import '../config/config_manager.dart';
|
||||
|
||||
/// Manages cursor-positioned XP overlays by coordinating cursor tracking
|
||||
/// and spawning Flutter overlay processes
|
||||
class CursorOverlayManager {
|
||||
final IWindowManager _windowManager;
|
||||
final String _overlayAppPath;
|
||||
final bool _enabled;
|
||||
|
||||
/// Create a CursorOverlayManager with the specified window manager and overlay app path
|
||||
///
|
||||
/// If overlayAppPath is not provided, tries to resolve the path automatically
|
||||
CursorOverlayManager({
|
||||
required IWindowManager windowManager,
|
||||
required String overlayAppPath,
|
||||
String? overlayAppPath,
|
||||
bool enabled = true,
|
||||
}) : _windowManager = windowManager,
|
||||
_overlayAppPath = overlayAppPath;
|
||||
_overlayAppPath = overlayAppPath ?? _resolveOverlayAppPath(),
|
||||
_enabled = enabled;
|
||||
|
||||
/// Attempts to find the overlay app binary path
|
||||
static String _resolveOverlayAppPath() {
|
||||
final List<String?> pathOptions = [
|
||||
// Check for configuration in xp_config.json
|
||||
ConfigManager.instance.getOverlayAppPath(),
|
||||
];
|
||||
|
||||
final List<String> possiblePaths = [
|
||||
// Check relative to the current script directory
|
||||
p.join(p.dirname(Platform.script.toFilePath()), '..', '..', '..', 'xp_overlay_app', 'build', 'linux', 'x64', 'release', 'bundle', 'xp_overlay_app'),
|
||||
|
||||
// Check relative to the current working directory
|
||||
p.join(Directory.current.path, '..', 'xp_overlay_app', 'build', 'linux', 'x64', 'release', 'bundle', 'xp_overlay_app'),
|
||||
|
||||
// Fixed paths for development
|
||||
'/home/nate/source/non-work/xp_nix/xp_overlay_app/build/linux/x64/release/bundle/xp_overlay_app',
|
||||
];
|
||||
|
||||
// Add non-null paths from options to possible paths
|
||||
for (final path in pathOptions) {
|
||||
if (path != null && path.isNotEmpty) {
|
||||
possiblePaths.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (final path in possiblePaths) {
|
||||
if (path != null && path.isNotEmpty && File(path).existsSync()) {
|
||||
Logger.info('Found overlay app at: $path');
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.warn('Could not find overlay app binary, overlays will be disabled');
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Show XP number at current cursor position
|
||||
///
|
||||
/// [xpValue] - XP amount to display
|
||||
/// [duration] - How long to show the overlay (default 3 seconds)
|
||||
Future<void> showXPNumber(int xpValue, {Duration duration = const Duration(seconds: 3)}) async {
|
||||
/// [duration] - How long to show the overlay (default from config, fallback 3 seconds)
|
||||
Future<void> showXPNumber(
|
||||
int xpValue, {
|
||||
Duration? duration,
|
||||
}) async {
|
||||
// Skip if disabled
|
||||
if (!_enabled || !ConfigManager.instance.isOverlayEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if overlay app path is empty or doesn't exist
|
||||
if (_overlayAppPath.isEmpty || !await File(_overlayAppPath).exists()) {
|
||||
Logger.debug('Overlay app not found at: $_overlayAppPath, skipping XP overlay');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current cursor position
|
||||
final cursorPos = await _windowManager.getCursorPosition();
|
||||
@ -30,10 +89,14 @@ class CursorOverlayManager {
|
||||
final x = cursorPos['x']!;
|
||||
final y = cursorPos['y']!;
|
||||
|
||||
// Use specified duration or get from config
|
||||
final overlayDuration = duration ??
|
||||
Duration(seconds: ConfigManager.instance.getOverlayDuration());
|
||||
|
||||
Logger.info('Showing XP overlay: +$xpValue at position ($x, $y)');
|
||||
|
||||
// Spawn Flutter overlay process
|
||||
await _spawnOverlayProcess(x, y, xpValue, duration);
|
||||
await _spawnOverlayProcess(x, y, xpValue, overlayDuration);
|
||||
} catch (e) {
|
||||
Logger.error('Failed to show XP overlay', e);
|
||||
}
|
||||
@ -44,11 +107,31 @@ class CursorOverlayManager {
|
||||
/// [x] - X coordinate in pixels
|
||||
/// [y] - Y coordinate in pixels
|
||||
/// [xpValue] - XP amount to display
|
||||
/// [duration] - How long to show the overlay
|
||||
Future<void> showXPNumberAt(int x, int y, int xpValue, {Duration duration = const Duration(seconds: 3)}) async {
|
||||
/// [duration] - How long to show the overlay (default from config)
|
||||
Future<void> showXPNumberAt(
|
||||
int x,
|
||||
int y,
|
||||
int xpValue,
|
||||
{Duration? duration}
|
||||
) async {
|
||||
// Skip if disabled
|
||||
if (!_enabled || !ConfigManager.instance.isOverlayEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if overlay app path is empty or doesn't exist
|
||||
if (_overlayAppPath.isEmpty || !await File(_overlayAppPath).exists()) {
|
||||
Logger.debug('Overlay app not found at: $_overlayAppPath, skipping XP overlay');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use specified duration or get from config
|
||||
final overlayDuration = duration ??
|
||||
Duration(seconds: ConfigManager.instance.getOverlayDuration());
|
||||
|
||||
Logger.info('Showing XP overlay: +$xpValue at position ($x, $y)');
|
||||
await _spawnOverlayProcess(x, y, xpValue, duration);
|
||||
await _spawnOverlayProcess(x, y, xpValue, overlayDuration);
|
||||
} catch (e) {
|
||||
Logger.error('Failed to show XP overlay at position', e);
|
||||
}
|
||||
@ -70,6 +153,12 @@ class CursorOverlayManager {
|
||||
Future<void> _spawnOverlayProcess(int x, int y, int xpValue, Duration duration) async {
|
||||
final durationSeconds = duration.inSeconds;
|
||||
|
||||
// Safety check - don't proceed if path is empty
|
||||
if (_overlayAppPath.isEmpty) {
|
||||
Logger.warn('Overlay app path is empty, cannot spawn process');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if overlay app exists
|
||||
if (!await File(_overlayAppPath).exists()) {
|
||||
Logger.error('Overlay app not found at: $_overlayAppPath');
|
||||
@ -77,15 +166,26 @@ class CursorOverlayManager {
|
||||
}
|
||||
|
||||
try {
|
||||
// Log the command we're about to run
|
||||
final commandArgs = [x.toString(), y.toString(), xpValue.toString(), durationSeconds.toString()];
|
||||
Logger.debug('Executing: $_overlayAppPath ${commandArgs.join(' ')}');
|
||||
|
||||
// Spawn overlay process with arguments: x y xp_value duration_seconds
|
||||
final process = await Process.start(
|
||||
_overlayAppPath,
|
||||
[x.toString(), y.toString(), xpValue.toString(), durationSeconds.toString()],
|
||||
commandArgs,
|
||||
mode: ProcessStartMode.detached,
|
||||
);
|
||||
|
||||
Logger.debug('Spawned overlay process with PID: ${process.pid}');
|
||||
|
||||
// Collect stderr output to log if there's an error
|
||||
process.stderr.transform(utf8.decoder).listen((data) {
|
||||
if (data.isNotEmpty) {
|
||||
Logger.warn('Overlay process stderr: $data');
|
||||
}
|
||||
});
|
||||
|
||||
// Don't wait for the process to complete - let it run independently
|
||||
process.exitCode.then((exitCode) {
|
||||
if (exitCode != 0) {
|
||||
@ -98,19 +198,59 @@ class CursorOverlayManager {
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
Logger.error('Failed to spawn overlay process', e);
|
||||
Logger.error('Failed to spawn overlay process: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Test the overlay system by showing a test XP number
|
||||
///
|
||||
/// Performs additional validation and prints detailed diagnostic information
|
||||
Future<void> testOverlay({int testXP = 42}) async {
|
||||
Logger.info('Testing overlay system with XP value: $testXP');
|
||||
await showXPNumber(testXP, duration: const Duration(seconds: 5));
|
||||
|
||||
// Check if overlay is enabled in config
|
||||
final enabled = ConfigManager.instance.isOverlayEnabled();
|
||||
Logger.info('Overlay enabled in config: $enabled');
|
||||
|
||||
// Check if overlay app exists
|
||||
final appExists = _overlayAppPath.isNotEmpty && await File(_overlayAppPath).exists();
|
||||
Logger.info('Overlay app path: $_overlayAppPath (exists: $appExists)');
|
||||
|
||||
// Test cursor position detection
|
||||
final cursorPos = await getCurrentCursorPosition();
|
||||
if (cursorPos != null) {
|
||||
Logger.info('Cursor position detected at: (${cursorPos['x']}, ${cursorPos['y']})');
|
||||
} else {
|
||||
Logger.warn('Could not detect cursor position');
|
||||
}
|
||||
|
||||
// Check window manager type
|
||||
Logger.info('Window manager type: ${_windowManager.managerType}');
|
||||
|
||||
// Show test overlay if everything is ready
|
||||
if (enabled && appExists && cursorPos != null) {
|
||||
await showXPNumber(testXP, duration: const Duration(seconds: 5));
|
||||
Logger.info('Test overlay triggered successfully');
|
||||
} else {
|
||||
Logger.warn('Test overlay skipped due to validation failures');
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the overlay system is available
|
||||
/// Check if the overlay system is available and ready to use
|
||||
Future<bool> isAvailable() async {
|
||||
try {
|
||||
// Check if overlay is enabled in config
|
||||
if (!_enabled || !ConfigManager.instance.isOverlayEnabled()) {
|
||||
Logger.debug('Overlay system disabled by configuration');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if overlay app path is valid
|
||||
if (_overlayAppPath.isEmpty) {
|
||||
Logger.debug('Overlay app path is empty');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if window manager supports cursor position
|
||||
final cursorPos = await _windowManager.getCursorPosition();
|
||||
final windowManagerAvailable = cursorPos != null;
|
||||
@ -118,7 +258,11 @@ class CursorOverlayManager {
|
||||
// Check if overlay app exists
|
||||
final overlayAppAvailable = await File(_overlayAppPath).exists();
|
||||
|
||||
Logger.debug('Overlay system availability: windowManager=$windowManagerAvailable, overlayApp=$overlayAppAvailable');
|
||||
Logger.debug('Overlay system availability check: ' +
|
||||
'enabled=$_enabled, ' +
|
||||
'windowManager=$windowManagerAvailable, ' +
|
||||
'overlayAppExists=$overlayAppAvailable, ' +
|
||||
'overlayAppPath=$_overlayAppPath');
|
||||
|
||||
return windowManagerAvailable && overlayAppAvailable;
|
||||
} catch (e) {
|
||||
|
@ -39,6 +39,6 @@ _flutter.buildConfig = {"engineRevision":"18818009497c581ede5d8a3b8b833b81d00ceb
|
||||
|
||||
_flutter.loader.load({
|
||||
serviceWorkerSettings: {
|
||||
serviceWorkerVersion: "2823181200"
|
||||
serviceWorkerVersion: "3663276084"
|
||||
}
|
||||
});
|
||||
|
@ -22,7 +22,7 @@ const RESOURCES = {"assets/AssetManifest.bin": "693635b5258fe5f1cda720cf224f158c
|
||||
"icons/Icon-512.png": "96e752610906ba2a93c65f8abe1645f1",
|
||||
"icons/Icon-192.png": "ac9a721a12bbc803b44f645561ecb1e1",
|
||||
"main.dart.js": "7f6b340356b31753eb9d3f857246a6b6",
|
||||
"flutter_bootstrap.js": "5f2f5895eadd961a24b30c35e0e0b956",
|
||||
"flutter_bootstrap.js": "eb3490973c14b6084c78d6130df851d9",
|
||||
"canvaskit/canvaskit.js.symbols": "27361387bc24144b46a745f1afe92b50",
|
||||
"canvaskit/skwasm.wasm": "1c93738510f202d9ff44d36a4760126b",
|
||||
"canvaskit/canvaskit.js": "728b2d477d9b8c14593d4f9b82b484f3",
|
||||
|
@ -210,7 +210,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
|
@ -14,7 +14,7 @@ dependencies:
|
||||
shelf_static: ^1.1.2
|
||||
shelf_web_socket: ^2.0.0
|
||||
web_socket_channel: ^2.4.0
|
||||
# path: ^1.8.0
|
||||
path: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^5.0.0
|
||||
|
Loading…
Reference in New Issue
Block a user