Added xp-overlay feature

This commit is contained in:
Nate Anderson 2025-06-17 20:08:39 -06:00
parent 4c07690e85
commit dda6bfff42
9 changed files with 246 additions and 22 deletions

View File

@ -28,7 +28,9 @@
gtk3
pcre
libepoxy
# For drift
glib.dev
sysprof
# For drift / sqlite3 dart
sqlite
# For xp overlay
gtk-layer-shell

View File

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

View File

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

View File

@ -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) {

View File

@ -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) {

View File

@ -39,6 +39,6 @@ _flutter.buildConfig = {"engineRevision":"18818009497c581ede5d8a3b8b833b81d00ceb
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: "2823181200"
serviceWorkerVersion: "3663276084"
}
});

View File

@ -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",

View File

@ -210,7 +210,7 @@ packages:
source: hosted
version: "2.2.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"

View File

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