diff --git a/lib/bademagic_module/models/speed.dart b/lib/bademagic_module/models/speed.dart index 517b0f46d..54029a84b 100644 --- a/lib/bademagic_module/models/speed.dart +++ b/lib/bademagic_module/models/speed.dart @@ -13,9 +13,8 @@ enum Speed { // Static method to get int value of speed from the Enum Speed static int getIntValue(Speed speed) { - String hexValue = speed.hexValue.substring(2, 3); - int intValue = int.parse(hexValue, radix: 10); - return intValue; + // Map Speed.one (index 0) -> 1, Speed.two (index 1) -> 2, etc. + return speed.index + 1; } // Static method to get Speed from hex value diff --git a/lib/bademagic_module/utils/badge_text_storage.dart b/lib/bademagic_module/utils/badge_text_storage.dart new file mode 100644 index 000000000..7618ecb8a --- /dev/null +++ b/lib/bademagic_module/utils/badge_text_storage.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:badgemagic/bademagic_module/utils/byte_array_utils.dart'; +import 'package:path_provider/path_provider.dart'; + +/// A utility class to store and retrieve the original text of badges +class BadgeTextStorage { + static const String TEXT_STORAGE_FILENAME = 'badge_original_texts.json'; + + /// Save the original text for a badge + static Future saveOriginalText( + String badgeFilename, String originalText) async { + try { + // Get the existing text storage or create a new one + Map textStorage = await _getTextStorage(); + + // Store the original text with the badge filename as the key + textStorage[badgeFilename] = originalText; + + // Save the updated storage + await _saveTextStorage(textStorage); + + logger.d('Saved original text for badge: $badgeFilename'); + } catch (e) { + logger.e('Error saving original text: $e'); + } + } + + /// Get the original text for a badge + static Future getOriginalText(String badgeFilename) async { + try { + // Get the existing text storage + Map textStorage = await _getTextStorage(); + + // Return the original text if it exists, otherwise return empty string + return textStorage[badgeFilename] ?? ''; + } catch (e) { + logger.e('Error getting original text: $e'); + return ''; + } + } + + /// Delete the original text for a badge + static Future deleteOriginalText(String badgeFilename) async { + try { + // Get the existing text storage + Map textStorage = await _getTextStorage(); + + // Remove the entry for the badge + textStorage.remove(badgeFilename); + + // Save the updated storage + await _saveTextStorage(textStorage); + + logger.d('Deleted original text for badge: $badgeFilename'); + } catch (e) { + logger.e('Error deleting original text: $e'); + } + } + + /// Get the text storage file + static Future> _getTextStorage() async { + try { + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/$TEXT_STORAGE_FILENAME'); + + // Create the file if it doesn't exist + if (!await file.exists()) { + await file.create(); + await file.writeAsString('{}'); + return {}; + } + + // Read the file and parse the JSON + final jsonString = await file.readAsString(); + if (jsonString.isEmpty) { + return {}; + } + + final Map jsonData = jsonDecode(jsonString); + + // Convert dynamic values to String + final Map textStorage = {}; + jsonData.forEach((key, value) { + textStorage[key] = value.toString(); + }); + + return textStorage; + } catch (e) { + logger.e('Error getting text storage: $e'); + return {}; + } + } + + /// Save the text storage to file + static Future _saveTextStorage(Map textStorage) async { + try { + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/$TEXT_STORAGE_FILENAME'); + + // Convert the map to JSON and save it + final jsonString = jsonEncode(textStorage); + await file.writeAsString(jsonString); + } catch (e) { + logger.e('Error saving text storage: $e'); + } + } +} diff --git a/lib/bademagic_module/utils/file_helper.dart b/lib/bademagic_module/utils/file_helper.dart index ee00a97dd..f6ca2cbee 100644 --- a/lib/bademagic_module/utils/file_helper.dart +++ b/lib/bademagic_module/utils/file_helper.dart @@ -278,8 +278,22 @@ class FileHelper { // Save JSON string to the file File file = File(filePath); await file.writeAsString(jsonString); - imageCacheProvider.savedBadgeCache - .add(MapEntry("$filename.json", jsonData)); + + // Update the cache properly - check if the badge already exists in the cache + final cacheKey = "$filename.json"; + final cache = imageCacheProvider.savedBadgeCache; + final existingIndex = cache.indexWhere((entry) => entry.key == cacheKey); + + if (existingIndex >= 0) { + // Replace the existing entry in the cache + logger.i('Updating existing badge in cache: $cacheKey'); + cache[existingIndex] = MapEntry(cacheKey, jsonData); + } else { + // Add as a new entry if it doesn't exist + logger.i('Adding new badge to cache: $cacheKey'); + cache.add(MapEntry(cacheKey, jsonData)); + } + logger.i('Data saved to $filePath'); } catch (e) { logger.i('Error saving data: $e'); @@ -315,9 +329,36 @@ class FileHelper { //function that takes JsonSData and returns the Data object Data jsonToData(Map jsonData) { - // Convert JSON data to Data object - Data data = Data.fromJson(jsonData); - return data; + try { + // Convert JSON data to Data object + Data data = Data.fromJson(jsonData); + return data; + } catch (e) { + // If there's an error with the 'messages' key missing, add it with default values + if (e.toString().contains("Missing \"messages\" key")) { + logger.w('Fixing missing "messages" key in badge data'); + + // Create a default message structure if missing + Map fixedJsonData = + Map.from(jsonData); + fixedJsonData['messages'] = [ + { + 'text': jsonData['text'] ?? ['00'], + 'flash': jsonData['flash'] ?? false, + 'marquee': jsonData['marquee'] ?? false, + 'speed': jsonData['speed'] ?? '0x70', // Default to Speed.one + 'mode': jsonData['mode'] ?? '0x00', // Default to Mode.left + 'invert': jsonData['invert'] ?? false + } + ]; + + return Data.fromJson(fixedJsonData); + } else { + // For other errors, rethrow + logger.e('Error parsing badge data: $e'); + rethrow; + } + } } Future shareBadgeData(String filename) async { diff --git a/lib/providers/saved_badge_provider.dart b/lib/providers/saved_badge_provider.dart index 0051f86f8..50f480ab1 100644 --- a/lib/providers/saved_badge_provider.dart +++ b/lib/providers/saved_badge_provider.dart @@ -1,13 +1,17 @@ import 'package:badgemagic/bademagic_module/models/data.dart'; +import 'dart:convert'; +import 'dart:io'; import 'package:badgemagic/bademagic_module/models/messages.dart'; import 'package:badgemagic/bademagic_module/models/mode.dart'; import 'package:badgemagic/bademagic_module/models/speed.dart'; +import 'package:badgemagic/bademagic_module/utils/badge_text_storage.dart'; import 'package:badgemagic/bademagic_module/utils/byte_array_utils.dart'; import 'package:badgemagic/bademagic_module/utils/converters.dart'; import 'package:badgemagic/bademagic_module/utils/file_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:badgemagic/providers/animation_badge_provider.dart'; import 'package:badgemagic/providers/imageprovider.dart'; -import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; Map speedMap = { @@ -55,8 +59,94 @@ class SavedBadgeProvider extends ChangeNotifier { speedMap[speed] ?? Speed.one, //needs speed dial provider modeValueMap[animation]!, ); - fileHelper.saveBadgeData( - data, filename, isInvert); //needs AniEffectProvider + + // Save the badge data to a file + fileHelper.saveBadgeData(data, filename, isInvert); + + // Store the original text separately using BadgeTextStorage + // This will allow us to retrieve it when editing + await BadgeTextStorage.saveOriginalText('$filename.json', message); + + logger.d('Saved badge with original text: $message'); + } + + /// Updates an existing badge with new data + /// This method is specifically for editing existing badges + /// @param filename The filename of the badge to update (without .json extension) + /// @param message The new message text for the badge + /// @param isFlash Whether flash effect is enabled + /// @param isMarquee Whether marquee effect is enabled + /// @param isInvert Whether invert effect is enabled + /// @param speed The speed value for the animation + /// @param animation The animation mode index + Future updateBadgeData(String filename, String message, bool isFlash, + bool isMarquee, bool isInvert, int? speed, int animation) async { + // Make sure filename doesn't have .json extension + String cleanFilename = filename; + if (cleanFilename.endsWith('.json')) { + cleanFilename = cleanFilename.substring(0, cleanFilename.length - 5); + } + + logger.i('Updating existing badge: $cleanFilename'); + + // Create the updated badge data + Data data = await getBadgeData( + message, + isFlash, + isMarquee, + isInvert, + speedMap[speed] ?? Speed.one, + modeValueMap[animation]!, + ); + + try { + // Get the document directory path using the imported path_provider package + final directory = await getApplicationDocumentsDirectory(); + final filePath = '${directory.path}/$cleanFilename.json'; + + // First verify the file exists before trying to update it + File file = File(filePath); + if (await file.exists()) { + logger.i('Found existing badge file to update: $filePath'); + + // Convert Data object to JSON string + Map jsonData = data.toJson(); + jsonData['messages'][0]['invert'] = isInvert; + String jsonString = jsonEncode(jsonData); + + // Overwrite the existing file + await file.writeAsString(jsonString); + + // Update the cache + final cacheKey = '$cleanFilename.json'; + final cache = fileHelper.imageCacheProvider.savedBadgeCache; + final existingIndex = + cache.indexWhere((entry) => entry.key == cacheKey); + + if (existingIndex >= 0) { + // Replace the existing entry in the cache + logger.i('Updating existing badge in cache: $cacheKey'); + cache[existingIndex] = MapEntry(cacheKey, jsonData); + } + + // Update the original text storage + await BadgeTextStorage.saveOriginalText('$cleanFilename.json', message); + + logger.i('Successfully updated badge: $cleanFilename'); + } else { + logger.e('Badge file not found for updating: $filePath'); + // If file doesn't exist, fall back to creating a new one + fileHelper.saveBadgeData(data, cleanFilename, isInvert); + await BadgeTextStorage.saveOriginalText('$cleanFilename.json', message); + } + } catch (e) { + logger.e('Error updating badge: $e'); + // Fall back to the regular save method if there's an error + fileHelper.saveBadgeData(data, cleanFilename, isInvert); + await BadgeTextStorage.saveOriginalText('$cleanFilename.json', message); + } + + logger.d('Updated badge with new text: $message'); } Future getBadgeData(String text, bool flash, bool marq, bool isInverted, @@ -77,29 +167,106 @@ class SavedBadgeProvider extends ChangeNotifier { void savedBadgeAnimation( Map data, AnimationBadgeProvider aniProvider) { //set the animations and the modes from the json file - logger.i(Speed.getIntValue(Speed.fromHex(data['messages'][0]['speed']))); - aniProvider.calculateDuration( - Speed.getIntValue(Speed.fromHex(data['messages'][0]['speed'])) + 1); - aniProvider.setAnimationMode(animationMap[ - Mode.getIntValue(Mode.fromHex(data['messages'][0]['mode']))]); - - if (data['messages'][0]['invert'] == true) { - aniProvider.addEffect(effectMap[0]); + try { + // Safely get the speed value + if (data.containsKey('messages') && + data['messages'] is List && + data['messages'].isNotEmpty && + data['messages'][0] is Map && + data['messages'][0].containsKey('speed')) { + // Get the speed value directly from the Speed enum without adding 1 + // The Speed.getIntValue already adds 1 to the index + int speedValue = + Speed.getIntValue(Speed.fromHex(data['messages'][0]['speed'])); + logger.i("Setting animation speed to: $speedValue"); + aniProvider.calculateDuration(speedValue); + } else { + // Default to speed 1 if data is missing + logger.w("Missing speed data, defaulting to speed 1"); + aniProvider.calculateDuration(1); + } + } catch (e) { + // Handle any errors and default to speed 1 + logger.e("Error setting animation speed: $e"); + aniProvider.calculateDuration(1); } - - if (data['messages'][0]['flash'] == true) { - aniProvider.addEffect(effectMap[1]); + // Safely set the animation mode + try { + if (data.containsKey('messages') && + data['messages'] is List && + data['messages'].isNotEmpty && + data['messages'][0] is Map && + data['messages'][0].containsKey('mode')) { + int modeValue = + Mode.getIntValue(Mode.fromHex(data['messages'][0]['mode'])); + aniProvider.setAnimationMode(animationMap[modeValue]); + } else { + // Default to left animation if mode is missing + logger.w("Missing mode data, defaulting to left animation"); + aniProvider.setAnimationMode(animationMap[0]); + } + } catch (e) { + // Handle any errors and default to left animation + logger.e("Error setting animation mode: $e"); + aniProvider.setAnimationMode(animationMap[0]); } - if (data['messages'][0]['marquee'] == true) { - aniProvider.addEffect(effectMap[2]); + // Safely handle effects + try { + if (data.containsKey('messages') && + data['messages'] is List && + data['messages'].isNotEmpty && + data['messages'][0] is Map) { + // Handle invert effect + if (data['messages'][0].containsKey('invert') && + data['messages'][0]['invert'] == true) { + aniProvider.addEffect(effectMap[0]); + } + + // Handle flash effect + if (data['messages'][0].containsKey('flash') && + data['messages'][0]['flash'] == true) { + aniProvider.addEffect(effectMap[1]); + } + + // Handle marquee effect + if (data['messages'][0].containsKey('marquee') && + data['messages'][0]['marquee'] == true) { + aniProvider.addEffect(effectMap[2]); + } + } + } catch (e) { + logger.e("Error setting effects: $e"); + // No default effects needed } logger.i("Effects set are = ${aniProvider.getCurrentEffect}"); - String hexString = data['messages'][0]['text'].join(); - List> binaryArray = hexStringToBool(hexString); - aniProvider.setNewGrid(binaryArray); + // Safely handle text data + try { + if (data.containsKey('messages') && + data['messages'] is List && + data['messages'].isNotEmpty && + data['messages'][0] is Map && + data['messages'][0].containsKey('text') && + data['messages'][0]['text'] is List) { + String hexString = data['messages'][0]['text'].join(); + List> binaryArray = hexStringToBool(hexString); + aniProvider.setNewGrid(binaryArray); + } else { + logger.w("Missing or invalid text data in badge"); + // Create a default empty grid if text data is missing + List> emptyGrid = + List.generate(8, (_) => List.generate(16, (_) => false)); + aniProvider.setNewGrid(emptyGrid); + } + } catch (e) { + logger.e("Error setting badge text: $e"); + // Create a default empty grid on error + List> emptyGrid = + List.generate(8, (_) => List.generate(16, (_) => false)); + aniProvider.setNewGrid(emptyGrid); + } } bool getIsSavedBadgeData() => isSavedBadgeData; diff --git a/lib/providers/snake_game_provider.dart b/lib/providers/snake_game_provider.dart new file mode 100644 index 000000000..c56a700f5 --- /dev/null +++ b/lib/providers/snake_game_provider.dart @@ -0,0 +1,335 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:badgemagic/bademagic_module/utils/converters.dart'; +import 'package:badgemagic/bademagic_module/models/messages.dart'; +import 'package:badgemagic/bademagic_module/models/data.dart'; +import 'package:badgemagic/bademagic_module/models/mode.dart'; +import 'package:badgemagic/bademagic_module/models/speed.dart'; +import 'package:badgemagic/bademagic_module/bluetooth/datagenerator.dart'; +import 'package:badgemagic/providers/badge_message_provider.dart'; +import 'package:badgemagic/bademagic_module/utils/toast_utils.dart'; + +// Direction enum for snake movement +enum Direction { up, down, left, right } + +// Position class to represent coordinates +class Position { + final int row; + final int col; + + Position(this.row, this.col); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Position && other.row == row && other.col == col; + } + + @override + int get hashCode => row.hashCode ^ col.hashCode; +} + +class SnakeGameProvider extends ChangeNotifier { + // Badge dimensions + static const int rows = 11; + static const int cols = 44; + + // Game state + bool _isGameRunning = false; + bool _isGameOver = false; + int _score = 0; + Direction _direction = Direction.right; + Timer? _gameTimer; + + // Game speed (milliseconds between moves - lower is faster) + int _gameSpeed = 300; + static const int minSpeed = 100; // Fastest speed (100ms) + static const int maxSpeed = 500; // Slowest speed (500ms) + static const int speedStep = 50; // Speed increment/decrement step + + // Live badge sync + bool _liveSync = false; + DateTime _lastSyncTime = DateTime.fromMillisecondsSinceEpoch(0); + static const int minSyncIntervalMs = 100; // 10 FPS max + + void setLiveSync(bool value) { + _liveSync = value; + notifyListeners(); + } + + bool get liveSync => _liveSync; + + // Snake body positions (list of coordinates) + List _snake = []; + + // Food position + Position? _food; + + // Grid representation of the badge + List> _gameGrid = + List.generate(rows, (i) => List.generate(cols, (j) => false)); + + // Getters + List> get gameGrid => _gameGrid; + bool get isGameRunning => _isGameRunning; + bool get isGameOver => _isGameOver; + int get score => _score; + int get gameSpeed => _gameSpeed; + + // Calculate speed percentage (0-100%) + int get speedPercentage => + ((maxSpeed - _gameSpeed) * 100 ~/ (maxSpeed - minSpeed)).clamp(0, 100); + + // Initialize the game + void initGame() { + // Clear the grid + _gameGrid = List.generate(rows, (i) => List.generate(cols, (j) => false)); + + // Reset score + _score = 0; + + // Reset direction + _direction = Direction.right; + + // Reset snake position (start with 3 segments) + _snake = [ + Position(2, 2), // Head + Position(2, 1), + Position(2, 0), + ]; + + // Generate initial food + _generateFood(); + + // Update the grid + _updateGrid(); + + // Cancel any existing timer + _gameTimer?.cancel(); + _gameTimer = null; + + // Reset game state + _isGameRunning = false; + _isGameOver = false; + + notifyListeners(); + } + + // Start the game + void startGame() { + if (!_isGameRunning) { + _isGameRunning = true; + + // Start the game loop with current speed + _gameTimer = Timer.periodic(Duration(milliseconds: _gameSpeed), (timer) { + _moveSnake(); + }); + + notifyListeners(); + } + } + + // Increase snake speed + void increaseSpeed() { + if (_gameSpeed > minSpeed) { + _gameSpeed -= speedStep; + _restartTimerWithNewSpeed(); + notifyListeners(); + } + } + + // Decrease snake speed + void decreaseSpeed() { + if (_gameSpeed < maxSpeed) { + _gameSpeed += speedStep; + _restartTimerWithNewSpeed(); + notifyListeners(); + } + } + + // Restart timer with new speed + void _restartTimerWithNewSpeed() { + if (_isGameRunning) { + _gameTimer?.cancel(); + _gameTimer = Timer.periodic(Duration(milliseconds: _gameSpeed), (timer) { + _moveSnake(); + }); + } + } + + // Pause the game + void pauseGame() { + if (_isGameRunning) { + _isGameRunning = false; + _gameTimer?.cancel(); + notifyListeners(); + } + } + + // End the game + void endGame() { + _isGameRunning = false; + _gameTimer?.cancel(); + notifyListeners(); + } + + // Change snake direction + void changeDirection(Direction newDirection) { + // Prevent 180-degree turns + if ((_direction == Direction.up && newDirection == Direction.down) || + (_direction == Direction.down && newDirection == Direction.up) || + (_direction == Direction.left && newDirection == Direction.right) || + (_direction == Direction.right && newDirection == Direction.left)) { + return; + } + + _direction = newDirection; + } + + // Move the snake + void _moveSnake() { + // Live sync: send badge update after move, throttled + if (_liveSync) { + final now = DateTime.now(); + if (now.difference(_lastSyncTime).inMilliseconds >= minSyncIntervalMs) { + _lastSyncTime = now; + _sendGridToBadge(); + } + } + + if (!_isGameRunning || _isGameOver) return; + + // Get the current head position + Position head = _snake.first; + + // Calculate new head position based on direction + Position newHead; + switch (_direction) { + case Direction.up: + newHead = Position(head.row - 1, head.col); + break; + case Direction.down: + newHead = Position(head.row + 1, head.col); + break; + case Direction.left: + newHead = Position(head.row, head.col - 1); + break; + case Direction.right: + newHead = Position(head.row, head.col + 1); + break; + } + + // Handle wraparound (if snake goes out of bounds) + Position wrappedHead = Position( + newHead.row < 0 ? rows - 1 : (newHead.row >= rows ? 0 : newHead.row), + newHead.col < 0 ? cols - 1 : (newHead.col >= cols ? 0 : newHead.col)); + newHead = wrappedHead; + + // Check if snake collides with itself (game over condition) + for (int i = 1; i < _snake.length; i++) { + if (newHead.row == _snake[i].row && newHead.col == _snake[i].col) { + _gameOver(); + return; + } + } + + // Check if snake ate food + bool ateFood = + _food != null && newHead.row == _food!.row && newHead.col == _food!.col; + + // Add new head to the snake + _snake.insert(0, newHead); + + // If snake didn't eat food, remove the tail + if (!ateFood) { + _snake.removeLast(); + } else { + // If snake ate food, increment score and generate new food + _score++; + _generateFood(); + } + + // Update the grid + _updateGrid(); + + notifyListeners(); + } + + // Handle game over + void _gameOver() { + _isGameRunning = false; + _isGameOver = true; + _gameTimer?.cancel(); + notifyListeners(); + } + + // Generate food at random position + void _generateFood() { + Random random = Random(); + int row, col; + + // Generate food at a position not occupied by the snake + do { + row = random.nextInt(rows); + col = random.nextInt(cols); + } while (_snake.contains(Position(row, col))); + + _food = Position(row, col); + } + + // Update the grid based on snake and food positions + void _updateGrid() { + // Clear grid + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + _gameGrid[i][j] = false; + } + } + // Draw snake + for (final pos in _snake) { + if (pos.row >= 0 && pos.row < rows && pos.col >= 0 && pos.col < cols) { + _gameGrid[pos.row][pos.col] = true; + } + } + // Draw food + if (_food != null) { + _gameGrid[_food!.row][_food!.col] = true; + } + } + + Future _sendGridToBadge() async { + try { + // Convert bool grid to int grid for badge + List> badgeGrid = + _gameGrid.map((row) => row.map((b) => b ? 1 : 0).toList()).toList(); + // Convert to hex + List hex = Converters.convertBitmapToLEDHex(badgeGrid, true); + // Create Message/Data for badge + Message msg = Message( + text: hex, + flash: false, + marquee: false, + speed: Speed.one, + mode: Mode.picture); + Data data = Data(messages: [msg]); + DataTransferManager manager = DataTransferManager(data); + await BadgeMessageProvider().transferData(manager); + // Optionally: ToastUtils().showToast('Live badge updated'); + } catch (e) { + ToastUtils().showErrorToast('Live badge update failed'); + } + } + + // Toggle live sync + void toggleLiveSync() { + _liveSync = !_liveSync; + notifyListeners(); + } + + @override + void dispose() { + _gameTimer?.cancel(); + super.dispose(); + } +} diff --git a/lib/providers/tetris_game_provider.dart b/lib/providers/tetris_game_provider.dart new file mode 100644 index 000000000..578f3be3b --- /dev/null +++ b/lib/providers/tetris_game_provider.dart @@ -0,0 +1,312 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:badgemagic/bademagic_module/utils/converters.dart'; +import 'package:badgemagic/bademagic_module/models/messages.dart'; +import 'package:badgemagic/bademagic_module/models/data.dart'; +import 'package:badgemagic/bademagic_module/models/mode.dart'; +import 'package:badgemagic/bademagic_module/models/speed.dart'; +import 'package:badgemagic/bademagic_module/bluetooth/datagenerator.dart'; +import 'package:badgemagic/providers/badge_message_provider.dart'; +import 'package:badgemagic/bademagic_module/utils/toast_utils.dart'; + +// Badge grid size +const int badgeRows = 11; +const int badgeCols = 44; + +// Tetromino shapes +final List>> tetrominoShapes = [ + // I + [ + [1, 1, 1, 1] + ], + // O + [ + [1, 1], + [1, 1] + ], + // T + [ + [0, 1, 0], + [1, 1, 1] + ], + // L + [ + [1, 0], + [1, 0], + [1, 1] + ], + // J + [ + [0, 1], + [0, 1], + [1, 1] + ], + // S + [ + [0, 1, 1], + [1, 1, 0] + ], + // Z + [ + [1, 1, 0], + [0, 1, 1] + ], +]; + +class TetrisGameProvider extends ChangeNotifier { + // Live badge sync + bool _liveSync = false; + DateTime _lastSyncTime = DateTime.fromMillisecondsSinceEpoch(0); + static const int minSyncIntervalMs = 100; // 10 FPS max + + void setLiveSync(bool value) { + _liveSync = value; + notifyListeners(); + } + + bool get liveSync => _liveSync; + + bool isPaused = false; + + List> grid = + List.generate(badgeRows, (_) => List.filled(badgeCols, 0)); + int score = 0; + bool isGameOver = false; + int currentShapeIndex = 0; + List> currentShape = []; + int shapeRow = 0; + int shapeCol = 0; + Timer? _timer; + static const int tickMillis = 400; + Random random = Random(); + + TetrisGameProvider() { + _spawnNewShape(); + _timer = Timer.periodic(Duration(milliseconds: tickMillis), (_) => tick()); + } + + void pause() { + if (!isPaused && !isGameOver) { + isPaused = true; + _timer?.cancel(); + notifyListeners(); + } + } + + void resume() { + if (isPaused && !isGameOver) { + isPaused = false; + _timer = + Timer.periodic(Duration(milliseconds: tickMillis), (_) => tick()); + notifyListeners(); + } + } + + void reset() { + grid = List.generate(badgeRows, (_) => List.filled(badgeCols, 0)); + score = 0; + isGameOver = false; + isPaused = false; + _timer?.cancel(); + _spawnNewShape(); + _timer = Timer.periodic(Duration(milliseconds: tickMillis), (_) => tick()); + notifyListeners(); + } + + void tick() { + if (isGameOver || isPaused) return; + if (!_moveShape(1, 0)) { + _mergeShapeToGrid(); + _clearLines(); + if (!_spawnNewShape()) { + isGameOver = true; + _timer?.cancel(); + } + } + if (_liveSync) { + final now = DateTime.now(); + if (now.difference(_lastSyncTime).inMilliseconds >= minSyncIntervalMs) { + _lastSyncTime = now; + _sendGridToBadge(); + } + } + notifyListeners(); + } + + bool _spawnNewShape() { + currentShapeIndex = random.nextInt(tetrominoShapes.length); + currentShape = tetrominoShapes[currentShapeIndex] + .map((row) => List.from(row)) + .toList(); + shapeRow = 0; + shapeCol = badgeCols ~/ 2 - currentShape[0].length ~/ 2; + if (!_canPlace(currentShape, shapeRow, shapeCol)) { + return false; + } + return true; + } + + bool _canPlace(List> shape, int r, int c) { + for (int i = 0; i < shape.length; i++) { + for (int j = 0; j < shape[i].length; j++) { + if (shape[i][j] == 0) continue; + int rr = r + i; + int cc = c + j; + if (rr < 0 || rr >= badgeRows || cc < 0 || cc >= badgeCols) { + return false; + } + if (grid[rr][cc] != 0) return false; + } + } + return true; + } + + void _mergeShapeToGrid() { + for (int i = 0; i < currentShape.length; i++) { + for (int j = 0; j < currentShape[i].length; j++) { + if (currentShape[i][j] == 1) { + int rr = shapeRow + i; + int cc = shapeCol + j; + if (rr >= 0 && rr < badgeRows && cc >= 0 && cc < badgeCols) { + grid[rr][cc] = 1; + } + } + } + } + } + + void _clearLines() { + for (int r = badgeRows - 1; r >= 0; r--) { + if (grid[r].every((cell) => cell == 1)) { + grid.removeAt(r); + grid.insert(0, List.filled(badgeCols, 0)); + score++; + r++; // Check same row again + } + } + } + + bool _moveShape(int dr, int dc) { + int newRow = shapeRow + dr; + int newCol = shapeCol + dc; + if (_canPlace(currentShape, newRow, newCol)) { + shapeRow = newRow; + shapeCol = newCol; + return true; + } + return false; + } + + void moveLeft() { + if (!isGameOver) { + _moveShape(0, -1); + if (_liveSync) _maybeSendGridToBadge(); + notifyListeners(); + } + } + + void moveRight() { + if (!isGameOver) { + _moveShape(0, 1); + notifyListeners(); + } + } + + void rotate() { + if (isGameOver) return; + List> rotated = _rotateMatrix(currentShape); + if (_canPlace(rotated, shapeRow, shapeCol)) { + currentShape = rotated; + if (_liveSync) _maybeSendGridToBadge(); + notifyListeners(); + } + } + + List> _rotateMatrix(List> mat) { + final n = mat.length; + final m = mat[0].length; + List> res = List.generate(m, (_) => List.filled(n, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + res[j][n - i - 1] = mat[i][j]; + } + } + return res; + } + + void drop() { + if (isGameOver) return; + while (_moveShape(1, 0)) {} + tick(); + if (_liveSync) _maybeSendGridToBadge(); + notifyListeners(); + } + + void restart() { + grid = List.generate(badgeRows, (_) => List.filled(badgeCols, 0)); + score = 0; + isGameOver = false; + _timer?.cancel(); + _spawnNewShape(); + _timer = Timer.periodic(Duration(milliseconds: tickMillis), (_) => tick()); + notifyListeners(); + } + + List> get displayGrid { + // Overlay current shape on grid for display + List> tempGrid = grid.map((row) => List.from(row)).toList(); + for (int i = 0; i < currentShape.length; i++) { + for (int j = 0; j < currentShape[i].length; j++) { + if (currentShape[i][j] == 1) { + int rr = shapeRow + i; + int cc = shapeCol + j; + if (rr >= 0 && rr < badgeRows && cc >= 0 && cc < badgeCols) { + tempGrid[rr][cc] = 2; + } + } + } + } + return tempGrid; + } + + void _maybeSendGridToBadge() { + final now = DateTime.now(); + if (now.difference(_lastSyncTime).inMilliseconds >= minSyncIntervalMs) { + _lastSyncTime = now; + _sendGridToBadge(); + } + } + + Future _sendGridToBadge() async { + try { + // Use displayGrid to include falling piece + List> badgeGrid = displayGrid; + List hex = Converters.convertBitmapToLEDHex(badgeGrid, true); + Message msg = Message( + text: hex, + flash: false, + marquee: false, + speed: Speed.one, + mode: Mode.picture); + Data data = Data(messages: [msg]); + DataTransferManager manager = DataTransferManager(data); + await BadgeMessageProvider().transferData(manager); + // Optionally: ToastUtils().showToast('Live badge updated'); + } catch (e) { + ToastUtils().showErrorToast('Live badge update failed'); + } + } + + // Toggle live sync + void toggleLiveSync() { + _liveSync = !_liveSync; + notifyListeners(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} diff --git a/lib/providers/tetris_high_score.dart b/lib/providers/tetris_high_score.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/lib/providers/tetris_high_score.dart @@ -0,0 +1 @@ + diff --git a/lib/view/game_selection_screen.dart b/lib/view/game_selection_screen.dart new file mode 100644 index 000000000..e7db9b4ea --- /dev/null +++ b/lib/view/game_selection_screen.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:badgemagic/view/gamescreen.dart'; +import 'package:badgemagic/view/tetris_game_screen.dart'; + +class GameSelectionScreen extends StatelessWidget { + const GameSelectionScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0, vertical: 40.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Choose your game:', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), + SizedBox(height: 40), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Snake Game Button + _GameChoiceButton( + icon: Icons.android, + label: 'Snake Game', + color: Colors.green, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GameScreen(), + ), + ); + }, + ), + SizedBox(width: 32), + // Tetris Game Button + _GameChoiceButton( + icon: Icons.extension, + label: 'Tetris Game', + color: Colors.deepPurple, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TetrisGameScreen(), + ), + ); + }, + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +class _GameChoiceButton extends StatelessWidget { + final IconData icon; + final String label; + final Color color; + final VoidCallback onTap; + const _GameChoiceButton( + {required this.icon, + required this.label, + required this.color, + required this.onTap}); + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: color, width: 2), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: color, size: 48), + SizedBox(height: 12), + Text(label, + style: TextStyle( + fontWeight: FontWeight.bold, color: color, fontSize: 16)), + ], + ), + ), + ); + } +} diff --git a/lib/view/gamescreen.dart b/lib/view/gamescreen.dart new file mode 100644 index 000000000..e11bb7334 --- /dev/null +++ b/lib/view/gamescreen.dart @@ -0,0 +1,480 @@ +import 'package:badgemagic/bademagic_module/utils/toast_utils.dart'; +import 'package:badgemagic/constants.dart'; +import 'package:badgemagic/providers/snake_game_provider.dart'; +import 'package:badgemagic/view/homescreen.dart'; +import 'package:badgemagic/view/widgets/common_scaffold_widget.dart'; +import 'package:badgemagic/virtualbadge/view/badge_paint.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; + +class GameScreen extends StatefulWidget { + const GameScreen({super.key}); + + @override + State createState() => _GameScreenState(); +} + +class _GameScreenState extends State { + // Snake game provider + final SnakeGameProvider _gameProvider = SnakeGameProvider(); + + @override + void initState() { + super.initState(); + _gameProvider.initGame(); + _gameProvider.addListener(_checkGameOver); + + // Start the game after a short delay + Future.delayed(const Duration(milliseconds: 500), () { + _gameProvider.startGame(); + }); + } + + void _checkGameOver() { + // Show game over dialog when game is over + if (_gameProvider.isGameOver) { + // Delay dialog slightly to allow UI to update + Future.delayed(Duration(milliseconds: 300), () { + _showGameOverDialog(_gameProvider.score); + }); + } + } + + void _showGameOverDialog(int score) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + 'Game Over!', + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + textAlign: TextAlign.center, + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.sentiment_dissatisfied, + size: 50.sp, + color: Colors.orange, + ), + SizedBox(height: 16.h), + Text( + 'Your Score: $score', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 8.h), + Text( + 'Snake touched its body!', + style: TextStyle( + fontSize: 16.sp, + color: Colors.grey[700], + ), + textAlign: TextAlign.center, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _gameProvider.initGame(); + _gameProvider.startGame(); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + decoration: BoxDecoration( + color: colorPrimary, + borderRadius: BorderRadius.circular(8.r), + ), + child: Text( + 'Play Again', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16.sp, + ), + ), + ), + ), + ], + actionsAlignment: MainAxisAlignment.center, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + ); + }, + ); + } + + @override + void dispose() { + // Clean up when the screen is disposed + _gameProvider.removeListener(_checkGameOver); + _gameProvider.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _gameProvider, + child: CommonScaffold( + index: 0, // Same index as home to highlight the same drawer item + title: 'Snake Game', + body: SafeArea( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Game score and speed display + Consumer( + builder: (context, gameProvider, child) { + return Padding( + padding: + EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Score display + Text( + 'Score: ${gameProvider.score}', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: colorPrimary, + ), + ), + + // Speed display + Row( + children: [ + Text( + 'Speed: ', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + ), + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: colorPrimary, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '${gameProvider.speedPercentage}%', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + ], + ), + ); + }, + ), + + // Snake game badge display + Padding( + padding: EdgeInsets.symmetric(vertical: 8.h), + child: AspectRatio( + aspectRatio: 3.2, + child: Consumer( + builder: (context, gameProvider, child) { + return CustomPaint( + painter: BadgePaint(grid: gameProvider.gameGrid), + ); + }, + ), + ), + ), + + // Fixed height spacing + SizedBox(height: 20.h), + + // Game controller UI + Container( + padding: EdgeInsets.all(24.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Up button + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildDirectionButton( + Icons.arrow_upward, Direction.up), + ], + ), + SizedBox(height: 16.h), + // Left, Right buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildDirectionButton( + Icons.arrow_back, Direction.left), + SizedBox(width: 80.w), + _buildDirectionButton( + Icons.arrow_forward, Direction.right), + ], + ), + SizedBox(height: 16.h), + // Down button + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildDirectionButton( + Icons.arrow_downward, Direction.down), + ], + ), + + // Speed control buttons + SizedBox(height: 16.h), + Consumer( + builder: (context, gameProvider, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Speed: ', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8.w), + // Decrease speed button + InkWell( + onTap: () { + gameProvider.decreaseSpeed(); + ToastUtils().showToast("Speed decreased"); + }, + child: Container( + width: 36.w, + height: 36.w, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(18.r), + ), + child: + Icon(Icons.remove, color: Colors.black), + ), + ), + SizedBox(width: 12.w), + // Speed indicator + Container( + width: 100.w, + height: 8.h, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(4.r), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: + gameProvider.speedPercentage / 100, + child: Container( + decoration: BoxDecoration( + color: colorPrimary, + borderRadius: BorderRadius.circular(4.r), + ), + ), + ), + ), + SizedBox(width: 12.w), + // Increase speed button + InkWell( + onTap: () { + gameProvider.increaseSpeed(); + ToastUtils().showToast("Speed increased"); + }, + child: Container( + width: 36.w, + height: 36.w, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(Icons.add, color: Colors.black), + ), + ), + ], + ); + }, + ), + + // Game control buttons + SizedBox(height: 16.h), + Consumer( + builder: (context, gameProvider, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Reset button + ElevatedButton( + onPressed: () { + gameProvider.initGame(); + gameProvider.startGame(); + ToastUtils().showToast("Game Reset"); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + padding: EdgeInsets.symmetric( + horizontal: 16.w, vertical: 8.h), + ), + child: Text('Reset', + style: TextStyle(color: Colors.white)), + ), + SizedBox(width: 16.w), + // Pause/Resume button + ElevatedButton( + onPressed: () { + if (gameProvider.isGameRunning) { + gameProvider.pauseGame(); + ToastUtils().showToast("Game Paused"); + } else { + gameProvider.startGame(); + ToastUtils().showToast("Game Resumed"); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: gameProvider.isGameRunning + ? Colors.red + : Colors.green, + padding: EdgeInsets.symmetric( + horizontal: 16.w, vertical: 8.h), + ), + child: Text( + gameProvider.isGameRunning + ? 'Pause' + : 'Resume', + style: TextStyle(color: Colors.white), + ), + ), + ], + ); + }, + ), + ], + ), + ), + + // Spacer at the bottom for balance + SizedBox(height: 40.h), + + // Navigation buttons at the bottom + Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, -1), + ), + ], + ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildNavButton(false, 'Badge', Icons.badge), + _buildNavButton(true, 'Game', Icons.gamepad), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + // Build navigation button + Widget _buildNavButton(bool isSelected, String label, IconData icon) { + return InkWell( + onTap: () { + if (!isSelected) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => HomeScreen()), + (route) => false, + ); + } + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 8.h), + decoration: BoxDecoration( + color: + isSelected ? colorPrimary.withOpacity(0.1) : Colors.transparent, + borderRadius: BorderRadius.circular(20.r), + ), + child: Row( + children: [ + Icon( + icon, + color: isSelected ? colorPrimary : Colors.grey, + ), + SizedBox(width: 8.w), + Text( + label, + style: TextStyle( + color: isSelected ? colorPrimary : Colors.grey, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ), + ), + ); + } + + // Build direction button for the game controller + Widget _buildDirectionButton(IconData icon, Direction direction) { + return Consumer( + builder: (context, gameProvider, child) { + return Material( + elevation: 4, + borderRadius: BorderRadius.circular(16.r), + color: colorPrimary, + child: InkWell( + onTap: () { + // Change snake direction + gameProvider.changeDirection(direction); + }, + borderRadius: BorderRadius.circular(16.r), + child: Container( + width: 70.w, + height: 70.w, + alignment: Alignment.center, + child: Icon( + icon, + color: Colors.white, + size: 32, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/view/homescreen.dart b/lib/view/homescreen.dart index a8d1a4dfa..d554bd1dd 100644 --- a/lib/view/homescreen.dart +++ b/lib/view/homescreen.dart @@ -1,344 +1,659 @@ -import 'dart:async'; - -import 'package:badgemagic/bademagic_module/utils/byte_array_utils.dart'; -import 'package:badgemagic/bademagic_module/utils/converters.dart'; -import 'package:badgemagic/bademagic_module/utils/image_utils.dart'; -import 'package:badgemagic/bademagic_module/utils/toast_utils.dart'; -import 'package:badgemagic/badge_effect/flash_effect.dart'; -import 'package:badgemagic/badge_effect/invert_led_effect.dart'; -import 'package:badgemagic/badge_effect/marquee_effect.dart'; -import 'package:badgemagic/constants.dart'; -import 'package:badgemagic/providers/animation_badge_provider.dart'; -import 'package:badgemagic/providers/badge_message_provider.dart'; -import 'package:badgemagic/providers/imageprovider.dart'; -import 'package:badgemagic/providers/speed_dial_provider.dart'; -import 'package:badgemagic/view/special_text_field.dart'; -import 'package:badgemagic/view/widgets/common_scaffold_widget.dart'; -import 'package:badgemagic/view/widgets/homescreentabs.dart'; -import 'package:badgemagic/view/widgets/save_badge_dialog.dart'; -import 'package:badgemagic/view/widgets/speedial.dart'; -import 'package:badgemagic/view/widgets/vectorview.dart'; -import 'package:badgemagic/virtualbadge/view/animated_badge.dart'; -import 'package:extended_text_field/extended_text_field.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get_it/get_it.dart'; -import 'package:provider/provider.dart'; - -class HomeScreen extends StatefulWidget { - const HomeScreen({super.key}); - - @override - State createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State - with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { - late final TabController _tabController; - AnimationBadgeProvider animationProvider = AnimationBadgeProvider(); - late SpeedDialProvider speedDialProvider; - BadgeMessageProvider badgeData = BadgeMessageProvider(); - ImageUtils imageUtils = ImageUtils(); - InlineImageProvider inlineImageProvider = - GetIt.instance(); - bool isPrefixIconClicked = false; - int textfieldLength = 0; - String previousText = ''; - final TextEditingController inlineimagecontroller = - GetIt.instance.get().getController(); - bool isDialInteracting = false; - String errorVal = ""; - - @override - void initState() { - inlineimagecontroller.addListener(handleTextChange); - _setPortraitOrientation(); - WidgetsBinding.instance.addPostFrameCallback((_) { - inlineImageProvider.setContext(context); - }); - _startImageCaching(); - speedDialProvider = SpeedDialProvider(animationProvider); - super.initState(); - - _tabController = TabController(length: 3, vsync: this); - } - - void handleTextChange() { - final currentText = inlineimagecontroller.text; - final selection = inlineimagecontroller.selection; - - if (previousText.length > currentText.length) { - final deletionIndex = selection.baseOffset; - - final regex = RegExp(r'<<\d+>>'); - final matches = regex.allMatches(previousText); - - bool placeholderDeleted = false; - - for (final match in matches) { - if (deletionIndex > match.start && deletionIndex < match.end) { - inlineimagecontroller.text = - previousText.replaceRange(match.start, match.end, ''); - inlineimagecontroller.selection = - TextSelection.collapsed(offset: match.start); - placeholderDeleted = true; - break; - } - } - - if (!placeholderDeleted) { - previousText = inlineimagecontroller.text; - } - } else { - previousText = currentText; - } - } - - void _controllerListner() { - animationProvider.badgeAnimation(inlineImageProvider.getController().text, - Converters(), animationProvider.isEffectActive(InvertLEDEffect())); - } - - @override - void dispose() { - inlineimagecontroller.removeListener(handleTextChange); - animationProvider.stopAnimation(); - inlineImageProvider.getController().removeListener(_controllerListner); - _tabController.dispose(); - super.dispose(); - } - - void _setPortraitOrientation() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - } - - Future _startImageCaching() async { - if (!inlineImageProvider.isCacheInitialized) { - await inlineImageProvider.generateImageCache(); - setState(() { - inlineImageProvider.isCacheInitialized = true; - }); - } - } - - @override - Widget build(BuildContext context) { - super.build(context); - InlineImageProvider inlineImageProvider = - Provider.of(context); - - return MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (context) => animationProvider, - ), - ChangeNotifierProvider( - create: (context) { - inlineImageProvider.getController().addListener(_controllerListner); - return speedDialProvider; - }, - ), - ], - child: DefaultTabController( - length: 3, - child: CommonScaffold( - index: 0, - title: 'Badge Magic', - body: SafeArea( - child: SingleChildScrollView( - physics: isDialInteracting - ? const NeverScrollableScrollPhysics() - : const AlwaysScrollableScrollPhysics(), - child: Column( - children: [ - AnimationBadge(), - Container( - margin: EdgeInsets.all(15.w), - child: Material( - color: drawerHeaderTitle, - borderRadius: BorderRadius.circular(10.r), - elevation: 4, - child: ExtendedTextField( - onChanged: (value) {}, - controller: inlineimagecontroller, - specialTextSpanBuilder: ImageBuilder(), - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.r), - ), - prefixIcon: IconButton( - onPressed: () { - setState(() { - isPrefixIconClicked = !isPrefixIconClicked; - }); - }, - icon: const Icon(Icons.tag_faces_outlined), - ), - focusedBorder: OutlineInputBorder( - borderRadius: - BorderRadius.all(Radius.circular(10.r)), - borderSide: BorderSide(color: colorPrimary), - ), - ), - ), - ), - ), - Visibility( - visible: isPrefixIconClicked, - child: Container( - height: 170.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.r), - color: Colors.grey[200]), - margin: EdgeInsets.symmetric(horizontal: 15.w), - padding: EdgeInsets.symmetric( - vertical: 10.h, horizontal: 10.w), - child: VectorGridView())), - TabBar( - indicatorSize: TabBarIndicatorSize.tab, - labelColor: Colors.black, - unselectedLabelColor: mdGrey400, - indicatorColor: colorPrimary, - controller: _tabController, - splashFactory: InkRipple.splashFactory, - overlayColor: WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.pressed)) { - return dividerColor; - } - return null; - }, - ), - tabs: const [ - Tab(text: 'Speed'), - Tab(text: 'Animation'), - Tab(text: 'Effects'), - ], - ), - SizedBox( - height: 250.h, // Adjust the height dynamically - child: TabBarView( - physics: const NeverScrollableScrollPhysics(), - controller: _tabController, - children: [ - GestureDetector( - onPanDown: (_) { - // Enter interaction mode to stop main scrolling - setState(() => isDialInteracting = true); - }, - onPanCancel: () { - // Exit interaction mode if interaction is cancelled - setState(() => isDialInteracting = false); - }, - onPanEnd: (_) { - // Re-enable main scroll when done interacting - setState(() => isDialInteracting = false); - }, - child: RadialDial()), - AnimationTab(), - EffectTab(), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.symmetric(vertical: 20.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - if (inlineimagecontroller.text - .trim() - .isEmpty) { - ToastUtils().showErrorToast( - "Please enter a message"); - return; - } - logger.i( - 'Save button clicked, showing dialog : ${animationProvider.isEffectActive(FlashEffect())}'); - showDialog( - context: this.context, - builder: (context) { - return SaveBadgeDialog( - speed: speedDialProvider, - animationProvider: animationProvider, - textController: inlineImageProvider - .getController(), - isInverse: - animationProvider.isEffectActive( - InvertLEDEffect()), - ); - }); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 33.w, vertical: 8.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2.r), - color: mdGrey400, - ), - child: const Text('Save'), - ), - ), - ], - ), - ), - SizedBox( - width: 100.w, - ), - Container( - padding: EdgeInsets.symmetric(vertical: 20.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - badgeData.checkAndTransfer( - inlineImageProvider.getController().text, - animationProvider - .isEffectActive(FlashEffect()), - animationProvider - .isEffectActive(MarqueeEffect()), - animationProvider - .isEffectActive(InvertLEDEffect()), - speedDialProvider.getOuterValue(), - modeValueMap[animationProvider - .getAnimationIndex()], - null, - false); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 20.w, vertical: 8.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2.r), - color: mdGrey400, - ), - child: const Text('Transfer'), - ), - ), - ], - ), - ), - ], - ) - ], - ), - ), - ), - scaffoldKey: const Key(homeScreenTitleKey), - )), - ); - } - - @override - bool get wantKeepAlive => true; -} +import 'dart:async'; +import 'package:badgemagic/bademagic_module/utils/badge_text_storage.dart'; +import 'package:badgemagic/bademagic_module/utils/byte_array_utils.dart'; +import 'package:badgemagic/bademagic_module/utils/converters.dart'; +import 'package:badgemagic/bademagic_module/utils/file_helper.dart'; +import 'package:badgemagic/bademagic_module/utils/image_utils.dart'; +import 'package:badgemagic/bademagic_module/utils/toast_utils.dart'; +import 'package:badgemagic/bademagic_module/models/speed.dart'; +import 'package:badgemagic/badge_effect/flash_effect.dart'; +import 'package:badgemagic/badge_effect/invert_led_effect.dart'; +import 'package:badgemagic/badge_effect/marquee_effect.dart'; +import 'package:badgemagic/constants.dart'; +import 'package:badgemagic/providers/animation_badge_provider.dart'; +import 'package:badgemagic/providers/badge_message_provider.dart' + hide modeValueMap, speedMap; +import 'package:badgemagic/providers/imageprovider.dart'; +import 'package:badgemagic/providers/saved_badge_provider.dart'; +import 'package:badgemagic/providers/speed_dial_provider.dart'; +import 'package:badgemagic/view/special_text_field.dart'; +import 'package:badgemagic/view/widgets/common_scaffold_widget.dart'; +import 'package:badgemagic/view/widgets/homescreentabs.dart'; +import 'package:badgemagic/view/widgets/save_badge_dialog.dart'; +import 'package:badgemagic/view/widgets/speedial.dart'; +import 'package:badgemagic/view/widgets/vectorview.dart'; +import 'package:badgemagic/virtualbadge/view/animated_badge.dart'; +import 'package:badgemagic/view/game_selection_screen.dart'; +import 'package:extended_text_field/extended_text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get_it/get_it.dart'; +import 'package:provider/provider.dart'; + +class HomeScreen extends StatefulWidget { + // Add parameters for saved badge data when editing + final Map? savedBadgeData; + final String? savedBadgeFilename; + + const HomeScreen({ + super.key, + this.savedBadgeData, + this.savedBadgeFilename, + }); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State + with + TickerProviderStateMixin, + AutomaticKeepAliveClientMixin, + WidgetsBindingObserver { + // 0 = Badge tab, 1 = Game tab + int _selectedIndex = 0; + + late final TabController _tabController; + AnimationBadgeProvider animationProvider = AnimationBadgeProvider(); + late SpeedDialProvider speedDialProvider; + BadgeMessageProvider badgeData = BadgeMessageProvider(); + ImageUtils imageUtils = ImageUtils(); + InlineImageProvider inlineImageProvider = + GetIt.instance(); + bool isPrefixIconClicked = false; + int textfieldLength = 0; + String previousText = ''; + final TextEditingController inlineimagecontroller = + GetIt.instance.get().getController(); + bool isDialInteracting = false; + String errorVal = ""; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + inlineimagecontroller.addListener(handleTextChange); + _setPortraitOrientation(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + inlineImageProvider.setContext(context); + + // Apply saved badge data if we're editing a saved badge + if (widget.savedBadgeData != null) { + await _applySavedBadgeData(); + } + }); + _startImageCaching(); + speedDialProvider = SpeedDialProvider(animationProvider); + super.initState(); + + _tabController = TabController(length: 3, vsync: this); + } + + // Method to apply saved badge data when editing a badge + Future _applySavedBadgeData() async { + final savedData = widget.savedBadgeData!; + final fileHelper = FileHelper(); + final savedBadgeProvider = SavedBadgeProvider(); + + // Set the text from the saved badge + final badgeDataModel = fileHelper.jsonToData(savedData); + final message = badgeDataModel.messages[0]; + + // When we save a badge, we store the original text using BadgeTextStorage + // Now we need to retrieve that text to show in the text field + String badgeText = ""; + try { + if (widget.savedBadgeFilename != null) { + // Get the original text from BadgeTextStorage + badgeText = + await BadgeTextStorage.getOriginalText(widget.savedBadgeFilename!); + + // If we couldn't find the original text, use the filename as a fallback + if (badgeText.isEmpty) { + badgeText = widget.savedBadgeFilename! + .substring(0, widget.savedBadgeFilename!.length - 5); + // If the filename is a timestamp, use a generic text + if (badgeText.contains(":") && badgeText.contains("-")) { + badgeText = "Hello"; // Default text for timestamp filenames + } + } + } + } catch (e) { + logger.e("Failed to retrieve original badge text: $e"); + badgeText = "Hello"; // Default fallback + } + + // Set the text in the controller + inlineimagecontroller.text = badgeText; + + // Set animation effects + if (message.flash) { + animationProvider.addEffect(effectMap[1]); // Flash effect + } + if (message.marquee) { + animationProvider.addEffect(effectMap[2]); // Marquee effect + } + + // Set inversion if applicable + if (savedData.containsKey('invert') && savedData['invert'] == true) { + animationProvider.addEffect(effectMap[0]); // Invert effect + } + + // Set animation mode + int modeValue = 0; // Default to left animation + try { + // Handle different mode formats - could be enum or int + if (message.mode is int) { + modeValue = message.mode as int; + } else { + // Try to extract the mode value from the enum + String modeString = message.mode.toString(); + // If it's in format "Mode.left", extract just the mode name + if (modeString.contains('.')) { + String modeName = modeString.split('.').last; + // Map mode name to value + switch (modeName.toLowerCase()) { + case 'left': + modeValue = 0; + break; + case 'right': + modeValue = 1; + break; + case 'up': + modeValue = 2; + break; + case 'down': + modeValue = 3; + break; + case 'fixed': + modeValue = 4; + break; + case 'snowflake': + modeValue = 5; + break; + case 'picture': + modeValue = 6; + break; + case 'animation': + modeValue = 7; + break; + default: + modeValue = 0; // Default to left + } + } else { + // Try parsing as int + modeValue = int.tryParse(modeString) ?? 0; + } + } + } catch (e) { + // If parsing fails, default to left animation (0) + logger.e("Failed to parse mode value: $e"); + } + animationProvider.setAnimationMode(animationMap[modeValue]); + + // Set speed using Speed.getIntValue to ensure correct dial value + try { + int speedDialValue = 1; // Default + // Use the static helper method to get the correct dial value + speedDialValue = Speed.getIntValue(message.speed); + logger.i("Setting speed dial to: $speedDialValue from ${message.speed}"); + speedDialProvider.setDialValue(speedDialValue); + } catch (e) { + logger.e("Failed to set speed dial value: $e"); + speedDialProvider.setDialValue(1); // Fallback to default + } + + // Store the filename for saving back to the same file + savedBadgeProvider.setSavedBadgeDataMap(savedData); + savedBadgeProvider.setIsSavedBadgeData(true); + + // Notify that we're editing an existing badge + ToastUtils().showToast( + "Editing badge: ${widget.savedBadgeFilename!.substring(0, widget.savedBadgeFilename!.length - 5)}"); + } + + void handleTextChange() { + final currentText = inlineimagecontroller.text; + final selection = inlineimagecontroller.selection; + + if (previousText.length > currentText.length) { + final deletionIndex = selection.baseOffset; + + final regex = RegExp(r'<<\d+>>'); + final matches = regex.allMatches(previousText); + + bool placeholderDeleted = false; + + for (final match in matches) { + if (deletionIndex > match.start && deletionIndex < match.end) { + inlineimagecontroller.text = + previousText.replaceRange(match.start, match.end, ''); + inlineimagecontroller.selection = + TextSelection.collapsed(offset: match.start); + placeholderDeleted = true; + break; + } + } + + if (!placeholderDeleted) { + previousText = inlineimagecontroller.text; + } + } else { + previousText = currentText; + } + } + + void _controllerListner() { + animationProvider.badgeAnimation( + inlineImageProvider.getController().text, + Converters(), + animationProvider.isEffectActive(InvertLEDEffect()), + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + inlineimagecontroller.removeListener(handleTextChange); + animationProvider.stopAnimation(); + inlineImageProvider.getController().removeListener(_controllerListner); + _tabController.dispose(); + super.dispose(); + } + + void _setPortraitOrientation() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + } + + Future _startImageCaching() async { + if (!inlineImageProvider.isCacheInitialized) { + await inlineImageProvider.generateImageCache(); + setState(() { + inlineImageProvider.isCacheInitialized = true; + }); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + InlineImageProvider inlineImageProvider = + Provider.of(context); + + // Depending on _selectedIndex, we either show the badge‐editing UI (index 0) + // or the game selection UI (index 1). + final Widget bodyContent = (_selectedIndex == 0) + ? _buildBadgeBody(inlineImageProvider) + : GameSelectionScreen(); + + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => animationProvider, + ), + ChangeNotifierProvider( + create: (context) { + inlineImageProvider.getController().addListener(_controllerListner); + return speedDialProvider; + }, + ), + ], + child: DefaultTabController( + length: 3, + child: CommonScaffold( + index: _selectedIndex, + title: 'Badge Magic', + scaffoldKey: const Key(homeScreenTitleKey), + body: SafeArea( + child: Column( + children: [ + // Main content area (badge UI or game screen) + Expanded(child: bodyContent), + // Bottom navigation always shows + _buildBottomNavigation(), + ], + ), + ), + ), + ), + ); + } + + /// Builds the badge-editing UI (when _selectedIndex == 0). + Widget _buildBadgeBody(InlineImageProvider inlineImageProvider) { + return SingleChildScrollView( + physics: isDialInteracting + ? const NeverScrollableScrollPhysics() + : const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + // The animated badge preview + const AnimationBadge(), + + // Text‐entry area with inline-image support + Container( + margin: EdgeInsets.all(15.w), + child: Material( + color: drawerHeaderTitle, + borderRadius: BorderRadius.circular(10.r), + elevation: 4, + child: ExtendedTextField( + onChanged: (value) {}, + controller: inlineimagecontroller, + specialTextSpanBuilder: ImageBuilder(), + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.r), + ), + prefixIcon: IconButton( + onPressed: () { + setState(() { + isPrefixIconClicked = !isPrefixIconClicked; + }); + }, + icon: const Icon(Icons.tag_faces_outlined), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10.r)), + borderSide: BorderSide(color: colorPrimary), + ), + ), + ), + ), + ), + + // If the emoji/inlined‐image palette is open + Visibility( + visible: isPrefixIconClicked, + child: Container( + height: 170.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.r), + color: Colors.grey[200], + ), + margin: EdgeInsets.symmetric(horizontal: 15.w), + padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 10.w), + child: const VectorGridView(), + ), + ), + + // TabBar (Speed / Animation / Effects) + TabBar( + indicatorSize: TabBarIndicatorSize.tab, + labelColor: Colors.black, + unselectedLabelColor: mdGrey400, + indicatorColor: colorPrimary, + controller: _tabController, + splashFactory: InkRipple.splashFactory, + overlayColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.pressed)) { + return dividerColor; + } + return null; + }, + ), + tabs: const [ + Tab(text: 'Speed'), + Tab(text: 'Animation'), + Tab(text: 'Effects'), + ], + ), + + // The TabBarView that shows the dials / toggles + SizedBox( + height: 250.h, + child: TabBarView( + physics: const NeverScrollableScrollPhysics(), + controller: _tabController, + children: [ + GestureDetector( + onPanDown: (_) { + // Enter interaction mode to stop scrolling + setState(() => isDialInteracting = true); + }, + onPanCancel: () { + setState(() => isDialInteracting = false); + }, + onPanEnd: (_) { + setState(() => isDialInteracting = false); + }, + child: const RadialDial(), + ), + const AnimationTab(), + const EffectTab(), + ], + ), + ), + + // Save / Transfer buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Save Button + Container( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: Row( + children: [ + GestureDetector( + onTap: () async { + if (inlineImageProvider.getController().text.isEmpty) { + ToastUtils().showToast("Please enter a message"); + return; + } + + // If we're editing an existing badge, update it + if (widget.savedBadgeFilename != null) { + final savedBadgeProvider = SavedBadgeProvider(); + String baseFilename = widget.savedBadgeFilename!; + if (baseFilename.endsWith('.json')) { + baseFilename = baseFilename.substring( + 0, baseFilename.length - 5); + } + + await savedBadgeProvider.updateBadgeData( + baseFilename, + inlineImageProvider.getController().text, + animationProvider.isEffectActive(FlashEffect()), + animationProvider.isEffectActive(MarqueeEffect()), + animationProvider.isEffectActive(InvertLEDEffect()), + speedDialProvider.getOuterValue(), + animationProvider.getAnimationIndex() ?? 1, + ); + ToastUtils().showToast("Badge Updated Successfully"); + } else { + // Show dialog for new badge + showDialog( + context: context, + builder: (context) { + return SaveBadgeDialog( + speed: speedDialProvider, + animationProvider: animationProvider, + textController: + inlineImageProvider.getController(), + isInverse: animationProvider + .isEffectActive(InvertLEDEffect()), + ); + }, + ); + } + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 33.w, vertical: 8.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + color: mdGrey400, + ), + child: const Text('Save'), + ), + ), + ], + ), + ), + + SizedBox(width: 100.w), + + // Transfer Button + Container( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: Row( + children: [ + GestureDetector( + onTap: () { + badgeData.checkAndTransfer( + inlineImageProvider.getController().text, + animationProvider.isEffectActive(FlashEffect()), + animationProvider.isEffectActive(MarqueeEffect()), + animationProvider.isEffectActive(InvertLEDEffect()), + speedDialProvider.getOuterValue(), + modeValueMap[animationProvider.getAnimationIndex()], + null, + false, + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 20.w, vertical: 8.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + color: mdGrey400, + ), + child: const Text('Transfer'), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ); + } + + /// Builds the bottom navigation row. Tapping “Badge” sets index=0; tapping “Game” sets index=1. + Widget _buildBottomNavigation() { + // Determine whether each button is selected + final bool isBadgeSelected = (_selectedIndex == 0); + final bool isGameSelected = (_selectedIndex == 1); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, -1), + ), + ], + ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Badge button + InkWell( + onTap: () { + if (_selectedIndex != 0) { + setState(() { + _selectedIndex = 0; + }); + } + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 8.h), + decoration: BoxDecoration( + color: isBadgeSelected + ? colorPrimary.withOpacity(0.1) + : Colors.transparent, + borderRadius: BorderRadius.circular(20.r), + ), + child: Row( + children: [ + Icon( + Icons.badge, + color: isBadgeSelected ? colorPrimary : Colors.grey, + ), + SizedBox(width: 8.w), + Text( + 'Badge', + style: TextStyle( + color: isBadgeSelected ? colorPrimary : Colors.grey, + fontWeight: isBadgeSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + ), + + // Game button + InkWell( + onTap: () { + if (_selectedIndex != 1) { + setState(() { + _selectedIndex = 1; + }); + } + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 8.h), + decoration: BoxDecoration( + color: isGameSelected + ? colorPrimary.withOpacity(0.1) + : Colors.transparent, + borderRadius: BorderRadius.circular(20.r), + ), + child: Row( + children: [ + Icon( + Icons.gamepad, + color: isGameSelected ? colorPrimary : Colors.grey, + ), + SizedBox(width: 8.w), + Text( + 'Game', + style: TextStyle( + color: isGameSelected ? colorPrimary : Colors.grey, + fontWeight: isGameSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + @override + bool get wantKeepAlive => true; + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + inlineimagecontroller.clear(); + previousText = ''; + animationProvider.stopAllAnimations.call(); // If method exists + animationProvider.initializeAnimation.call(); // If method exists + if (mounted) setState(() {}); + } else if (state == AppLifecycleState.paused || + state == AppLifecycleState.inactive) { + animationProvider.stopAnimation(); + } + } +} diff --git a/lib/view/tetris_game_screen.dart b/lib/view/tetris_game_screen.dart new file mode 100644 index 000000000..3f7696405 --- /dev/null +++ b/lib/view/tetris_game_screen.dart @@ -0,0 +1,242 @@ +import 'package:badgemagic/view/homescreen.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/tetris_game_provider.dart'; +import '../constants.dart'; +import 'package:badgemagic/virtualbadge/view/badge_paint.dart'; + +class TetrisGameScreen extends StatelessWidget { + const TetrisGameScreen({super.key}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => TetrisGameProvider(), + child: Scaffold( + appBar: AppBar( + title: Text('Tetris Game'), + backgroundColor: colorPrimary, + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: Consumer( + builder: (context, provider, child) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(height: 12), + Text('Score: ${provider.score}', + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.bold)), + SizedBox(height: 8), + // Badge display for Tetris (16x44) + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: AspectRatio( + aspectRatio: 44 / 16, // 44 columns, 16 rows + child: CustomPaint( + painter: BadgePaint( + grid: provider.displayGrid + .map((row) => + row.map((cell) => cell != 0).toList()) + .toList(), + ), + ), + ), + ), + if (provider.isGameOver) + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Text('Game Over', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.red)), + SizedBox(height: 8), + ElevatedButton( + onPressed: provider.restart, + style: ElevatedButton.styleFrom( + backgroundColor: colorPrimary), + child: Text('Restart', + style: TextStyle(color: Colors.white)), + ), + ], + ), + ), + if (!provider.isGameOver) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _TetrisControlButton( + icon: Icons.arrow_left, + onTap: provider.moveLeft, + ), + SizedBox(width: 16), + _TetrisControlButton( + icon: Icons.rotate_right, + onTap: provider.rotate, + ), + SizedBox(width: 16), + _TetrisControlButton( + icon: Icons.arrow_right, + onTap: provider.moveRight, + ), + SizedBox(width: 16), + _TetrisControlButton( + icon: Icons.arrow_downward, + onTap: provider.drop, + ), + ], + ), + ), + // Pause/Resume and Reset buttons + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: provider.isGameOver + ? null + : provider.isPaused + ? provider.resume + : provider.pause, + icon: Icon(provider.isPaused + ? Icons.play_arrow + : Icons.pause), + label: Text( + provider.isPaused ? 'Resume' : 'Pause'), + style: ElevatedButton.styleFrom( + backgroundColor: provider.isPaused + ? Colors.green + : Colors.red, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: 20, vertical: 10), + ), + ), + SizedBox(width: 24), + ElevatedButton.icon( + onPressed: provider.reset, + icon: Icon(Icons.refresh), + label: Text('Reset'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: 20, vertical: 10), + ), + ), + ], + ), + ), + ], + ); + }, + ), + ), + // Bottom navigation bar + _buildBottomNavigation(context), + ], + ), + ), + ), + ); + } + + // Bottom navigation bar for Tetris game + Widget _buildBottomNavigation(BuildContext context) { + final bool isBadgeSelected = false; + final bool isGameSelected = true; + return Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, -1), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildNavButton(isBadgeSelected, 'Badge', Icons.badge, context), + _buildNavButton(isGameSelected, 'Game', Icons.gamepad, context), + ], + ), + ), + ); + } + + Widget _buildNavButton( + bool isSelected, String label, IconData icon, BuildContext context) { + return InkWell( + onTap: () { + if (!isSelected) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => HomeScreen()), + (route) => false, + ); + } + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), + decoration: BoxDecoration( + color: + isSelected ? colorPrimary.withOpacity(0.1) : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + Icon( + icon, + color: isSelected ? colorPrimary : Colors.grey, + ), + SizedBox(width: 8), + Text( + label, + style: TextStyle( + color: isSelected ? colorPrimary : Colors.grey, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ), + ), + ); + } +} + +class _TetrisControlButton extends StatelessWidget { + final IconData icon; + final VoidCallback onTap; + const _TetrisControlButton({required this.icon, required this.onTap}); + @override + Widget build(BuildContext context) { + return Material( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(24), + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: onTap, + child: Container( + width: 48, + height: 48, + alignment: Alignment.center, + child: Icon(icon, size: 32, color: Colors.black87), + ), + ), + ); + } +} diff --git a/lib/view/widgets/save_badge_card.dart b/lib/view/widgets/save_badge_card.dart index 121291a92..bfaa97c62 100644 --- a/lib/view/widgets/save_badge_card.dart +++ b/lib/view/widgets/save_badge_card.dart @@ -8,7 +8,7 @@ import 'package:badgemagic/providers/animation_badge_provider.dart'; import 'package:badgemagic/providers/badge_message_provider.dart'; import 'package:badgemagic/providers/badge_slot_provider..dart'; import 'package:badgemagic/providers/saved_badge_provider.dart'; -import 'package:badgemagic/view/draw_badge_screen.dart'; +import 'package:badgemagic/view/homescreen.dart'; import 'package:badgemagic/view/widgets/badge_delete_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -28,6 +28,40 @@ class SaveBadgeCard extends StatelessWidget { required this.refreshBadgesCallback, }); + // Helper methods to safely access badge data properties + bool _safeGetFlashValue(Map data) { + try { + return file.jsonToData(data).messages[0].flash; + } catch (e) { + // If there's an error, default to false + return false; + } + } + + bool _safeGetMarqueeValue(Map data) { + try { + return file.jsonToData(data).messages[0].marquee; + } catch (e) { + // If there's an error, default to false + return false; + } + } + + bool _safeGetInvertValue(Map data) { + try { + if (data.containsKey('messages') && + data['messages'] is List && + data['messages'].isNotEmpty && + data['messages'][0] is Map) { + return data['messages'][0]['invert'] ?? false; + } + return false; + } catch (e) { + // If there's an error, default to false + return false; + } + } + @override Widget build(BuildContext context) { BadgeMessageProvider badge = BadgeMessageProvider(); @@ -96,21 +130,20 @@ class SaveBadgeCard extends StatelessWidget { color: Colors.black, ), onPressed: () { - List> data = hexStringToBool(file - .jsonToData(badgeData.value) - .messages[0] - .text - .join()) - .map((e) => e.map((e) => e ? 1 : 0).toList()) - .toList(); - Navigator.of(context).push( + // Navigate to HomeScreen with the badge data + // First, get the saved badge data + Map savedData = badgeData.value; + String badgeFilename = badgeData.key; + + // Navigate to HomeScreen and replace the current route + Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( - builder: (context) => DrawBadge( - filename: badgeData.key, - isSavedCard: true, - badgeGrid: data, + builder: (context) => HomeScreen( + savedBadgeData: savedData, + savedBadgeFilename: badgeFilename, ), ), + (route) => false, // Remove all previous routes ); }, ), @@ -164,7 +197,7 @@ class SaveBadgeCard extends StatelessWidget { Row( children: [ Visibility( - visible: file.jsonToData(badgeData.value).messages[0].flash, + visible: _safeGetFlashValue(badgeData.value), child: Container( padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), @@ -187,8 +220,7 @@ class SaveBadgeCard extends StatelessWidget { width: 8.w, ), Visibility( - visible: - file.jsonToData(badgeData.value).messages[0].marquee, + visible: _safeGetMarqueeValue(badgeData.value), child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), @@ -211,7 +243,7 @@ class SaveBadgeCard extends StatelessWidget { width: 8.w, ), Visibility( - visible: badgeData.value['messages'][0]['invert'] ?? false, + visible: _safeGetInvertValue(badgeData.value), child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),