diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index ae8d69ceb..d83be6982 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -10,6 +10,16 @@ env: ANDROID_EMULATOR_ARCH: x86_64 IPHONE_DEVICE_MODEL: iPhone 16 Pro Max IPAD_DEVICE_MODEL: iPad Pro 13-inch (M4) + BARECHECK_GITHUB_APP_TOKEN: ${{ secrets.BARECHECK_GITHUB_APP_TOKEN }} + ENCRYPTED_F10B5E0E5262_IV: ${{ secrets.ENCRYPTED_F10B5E0E5262_IV }} + ENCRYPTED_F10B5E0E5262_KEY: ${{ secrets.ENCRYPTED_F10B5E0E5262_KEY }} + ENCRYPTED_IOS_IV: ${{ secrets.ENCRYPTED_IOS_IV }} + ENCRYPTED_IOS_KEY: ${{ secrets.ENCRYPTED_IOS_KEY }} + STORE_PASS: ${{ secrets.STORE_PASS }} + ALIAS: ${{ secrets.ALIAS }} + KEY_PASS: ${{ secrets.KEY_PASS }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} jobs: common: @@ -19,7 +29,7 @@ jobs: VERSION_NAME: ${{ steps.flutter-version.outputs.VERSION_NAME }} VERSION_CODE: ${{ steps.flutter-version.outputs.VERSION_CODE }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - name: Common Workflow uses: ./.github/actions/common @@ -71,7 +81,7 @@ jobs: needs: common runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - name: Prepare Build Keys if: ${{ github.repository == 'fossasia/badgemagic-app' }} @@ -121,8 +131,8 @@ jobs: echo "Copying new build files" - find ../build/app/outputs/flutter-apk -type f \( -name '*release.apk' -o -name '*release.aab' \) -exec cp -v {} . \; - find ../build/app/outputs/bundle -type f \( -name '*release.apk' -o -name '*release.aab' \) -exec cp -v {} . \; + find ../build/app/outputs/flutter-apk -type f \( -name '*.apk' -o -name '*.aab' \) -exec cp -v {} . \; + find ../build/app/outputs/bundle -type f \( -name '*.apk' -o -name '*.aab' \) -exec cp -v {} . \; ls @@ -163,7 +173,7 @@ jobs: with: xcode-version: latest-stable - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - name: Prepare Build Keys if: ${{ github.repository == 'fossasia/badgemagic-app' }} @@ -207,7 +217,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download repository - uses: actions/checkout@v5 + uses: actions/checkout@v4 - name: Run Release Drafter id: run-release-drafter @@ -226,9 +236,8 @@ jobs: screenshots-android: name: Screenshots (Android) runs-on: ubuntu-latest - timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - name: Android Screenshot Workflow uses: ./.github/actions/screenshot-android @@ -236,152 +245,19 @@ jobs: ANDROID_EMULATOR_API: ${{ env.ANDROID_EMULATOR_API }} ANDROID_EMULATOR_ARCH: ${{ env.ANDROID_EMULATOR_ARCH }} - screenshots-iphone: - name: Screenshots (iPhone) + screenshots-ios: + name: Screenshots (iOS) runs-on: macos-latest - timeout-minutes: 30 steps: - - name: Set up Xcode - uses: maxim-lobanov/setup-xcode@v1.6.0 - with: - xcode-version: latest-stable - - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - - name: iPhone Screenshot Workflow - uses: ./.github/actions/screenshot-iphone - with: - IPHONE_DEVICE_MODEL: ${{ env.IPHONE_DEVICE_MODEL }} - - screenshots-ipad: - name: Screenshots (iPad) - runs-on: macos-latest - timeout-minutes: 30 - steps: - name: Set up Xcode uses: maxim-lobanov/setup-xcode@v1.6.0 with: xcode-version: latest-stable - - uses: actions/checkout@v5 - - - name: iPad Screenshot Workflow - uses: ./.github/actions/screenshot-ipad - with: - IPAD_DEVICE_MODEL: ${{ env.IPAD_DEVICE_MODEL }} - - debian: - name: Debian Flutter Build - needs: common - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: Debian Workflow - uses: ./.github/actions/debian - with: - VERSION_NAME: ${{ needs.common.outputs.VERSION_NAME }} - VERSION_CODE: ${{ needs.common.outputs.VERSION_CODE }} - - - name: Upload Debian Build - uses: actions/upload-artifact@v4 - with: - name: Debian Build - path: build/linux/x64/release/bundle/ - - - name: Push Debian Build to app branch - shell: bash - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - - git clone --branch=app https://${{ github.repository_owner }}:${{ github.token }}@github.com/${{ github.repository }} app - cd app - branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} - cp -r ../build/linux/x64/release/bundle/* . - git checkout --orphan temporary - git add --all - git commit -m "[Auto] Update Debian build from $branch ($(date +%Y-%m-%d.%H:%M:%S))" - git branch -D app - git branch -m app - git push --force origin app - - macos: - name: macOS Flutter Build - needs: common - runs-on: macos-latest - steps: - - uses: actions/checkout@v5 - - - name: macOS Workflow - uses: ./.github/actions/macos - with: - VERSION_NAME: ${{ needs.common.outputs.VERSION_NAME }} - VERSION_CODE: ${{ needs.common.outputs.VERSION_CODE }} - - - name: Upload macOS Build - uses: actions/upload-artifact@v4 - with: - name: macOS Build - path: build/macos/Build/Products/Release/ - - - name: Push macOS Build to app branch - shell: bash - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - - git clone --branch=app https://${{ github.repository_owner }}:${{ github.token }}@github.com/${{ github.repository }} app - cd app - branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} - - # Copy all macOS build artifacts to root - cp -r ../build/macos/Build/Products/Release/* . - - git checkout --orphan temporary - git add --all - git commit -m "[Auto] Update macOS build from $branch ($(date +%Y-%m-%d.%H:%M:%S))" - git branch -D app - git branch -m app - git push --force origin app - - - windows: - name: Windows Flutter Build - needs: common - runs-on: windows-latest - steps: - - uses: actions/checkout@v5 - - - name: Windows Workflow - uses: ./.github/actions/windows + - name: iOS Screenshot Workflow + uses: ./.github/actions/screenshot-ios with: - VERSION_NAME: ${{ needs.common.outputs.VERSION_NAME }} - VERSION_CODE: ${{ needs.common.outputs.VERSION_CODE }} - - - name: Upload Windows Build - uses: actions/upload-artifact@v4 - with: - name: Windows Build - path: build/windows/x64/runner/Release/ - - - name: Push Windows Build to app branch - shell: bash - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - - git clone --branch=app https://${{ github.repository_owner }}:${{ github.token }}@github.com/${{ github.repository }} app - cd app - branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} - - # Copy all Windows build artifacts to root - cp -r ../build/windows/x64/runner/Release/* . - - git checkout --orphan temporary - git add --all - git commit -m "[Auto] Update Windows build from $branch ($(date +%Y-%m-%d.%H:%M:%S))" - git branch -D app - git branch -m app - git push --force origin app - + IPHONE_DEVICE_MODEL: ${{ env.IPHONE_DEVICE_MODEL }} + IPAD_DEVICE_MODEL: ${{ env.IPAD_DEVICE_MODEL }} \ No newline at end of file diff --git a/lib/bademagic_module/models/data.dart b/lib/bademagic_module/models/data.dart index 179573220..3ed4193af 100644 --- a/lib/bademagic_module/models/data.dart +++ b/lib/bademagic_module/models/data.dart @@ -2,11 +2,16 @@ import 'messages.dart'; class Data { final List messages; - Data({required this.messages}); + final int? height; + final int? width; + + Data({required this.messages, this.height, this.width}); // Convert Data object to JSON Map toJson() => { 'messages': messages.map((message) => message.toJson()).toList(), + if (height != null) 'height': height, + if (width != null) 'width': width, }; // Convert JSON to Data object @@ -32,6 +37,10 @@ class Data { List messageList = messagesFromJson.map((message) => Message.fromJson(message)).toList(); - return Data(messages: messageList); + return Data( + messages: messageList, + height: json['height'] as int?, + width: json['width'] as int?, + ); } } diff --git a/lib/bademagic_module/models/screen_size.dart b/lib/bademagic_module/models/screen_size.dart new file mode 100644 index 000000000..3d4525e8e --- /dev/null +++ b/lib/bademagic_module/models/screen_size.dart @@ -0,0 +1,26 @@ +class ScreenSize { + final int width; + final int height; + final String name; + + const ScreenSize( + {required this.width, required this.height, required this.name}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ScreenSize && + runtimeType == other.runtimeType && + width == other.width && + height == other.height && + name == other.name; + + @override + int get hashCode => width.hashCode ^ height.hashCode ^ name.hashCode; +} + +const List supportedScreenSizes = [ + ScreenSize(width: 44, height: 11, name: "Small (44x11)"), + ScreenSize(width: 64, height: 16, name: "Medium (64x16)"), + ScreenSize(width: 128, height: 32, name: "Large (128x32)"), +]; diff --git a/lib/bademagic_module/utils/byte_array_utils.dart b/lib/bademagic_module/utils/byte_array_utils.dart index 67bd759f1..280a75948 100644 --- a/lib/bademagic_module/utils/byte_array_utils.dart +++ b/lib/bademagic_module/utils/byte_array_utils.dart @@ -29,8 +29,7 @@ List hexStringToByteArray(String hexString) { return data; } -List> hexStringToBool(String hexString) { - int rows = 11; +List> hexStringToBool(String hexString, int rows) { if (hexString.length % 2 != 0 || !isValidHex(hexString)) { throw ArgumentError("Invalid hex string: $hexString"); } @@ -39,23 +38,18 @@ List> hexStringToBool(String hexString) { int rowIndex = 0; for (int i = 0; i < hexString.length; i += 2) { - // Convert the hex string into a byte (int) int byte = int.parse(hexString.substring(i, i + 2), radix: 16); - - // Convert the byte into a binary representation and then into booleans for (int bit = 7; bit >= 0; bit--) { boolArray[rowIndex].add(((byte >> bit) & 1) == 1); } - - // Move to the next row after filling current one rowIndex = (rowIndex + 1) % rows; } return boolArray; } -List> byteArrayToBinaryArray(List byteArray) { - List> binaryArray = List.generate(11, (_) => []); +List> byteArrayToBinaryArray(List byteArray, int rows) { + List> binaryArray = List.generate(rows, (_) => []); int rowIndex = 0; for (int byte in byteArray) { @@ -66,32 +60,27 @@ List> byteArrayToBinaryArray(List byteArray) { binaryArray[rowIndex].addAll(binaryRepresentation); - rowIndex = (rowIndex + 1) % 11; + rowIndex = (rowIndex + 1) % rows; } - logger.d( - "binaryArray: $binaryArray"); // Use print instead of logger for standalone example + logger.d("binaryArray: $binaryArray"); return binaryArray; } String hexToBin(String hex) { - // Convert hex to binary string String binaryString = BigInt.parse(hex, radix: 16).toRadixString(2); - - // Pad the binary string with leading zeros if necessary to ensure it's a multiple of 8 bits int paddingLength = (8 - (binaryString.length % 8)) % 8; binaryString = binaryString.padLeft(binaryString.length + paddingLength, '0'); logger.d("binaryString: $binaryString"); return binaryString; } -List> binaryStringTo2DList(String binaryString) { - int maxHeight = 11; +List> binaryStringTo2DList(String binaryString, int maxHeight) { List> binary2DList = List.generate(maxHeight, (_) => []); for (int x = 0; x < binaryString.length; x++) { int a = 0; - for (int y = a; y < 11; y++) { + for (int y = a; y < maxHeight; y++) { for (int z = 0; z < 8; z++) { binary2DList[y].add(int.parse(binaryString[x++])); if (x >= binaryString.length) { @@ -102,6 +91,9 @@ List> binaryStringTo2DList(String binaryString) { break; } } + if (x >= binaryString.length) { + break; + } } logger.d("binary2DList: $binary2DList"); return binary2DList; diff --git a/lib/bademagic_module/utils/converters.dart b/lib/bademagic_module/utils/converters.dart index eb7842252..08b307642 100644 --- a/lib/bademagic_module/utils/converters.dart +++ b/lib/bademagic_module/utils/converters.dart @@ -1,6 +1,8 @@ +import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/bademagic_module/utils/byte_array_utils.dart'; import 'package:badgemagic/bademagic_module/utils/data_to_bytearray_converter.dart'; import 'package:badgemagic/bademagic_module/utils/file_helper.dart'; @@ -9,6 +11,7 @@ import 'package:badgemagic/providers/font_provider.dart'; import 'package:badgemagic/providers/imageprovider.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; +import 'package:logger/logger.dart'; String getFontKey( String fontFamily, double fontSize, FontWeight weight, bool italic) { @@ -21,9 +24,30 @@ class Converters { DataToByteArrayConverter converter = DataToByteArrayConverter(); ImageUtils imageUtils = ImageUtils(); FileHelper fileHelper = FileHelper(); + final Logger logger = Logger(); static final Map>> _characterCache = {}; + Future> messageTohex( + String message, bool isInverted, int rows, ScreenSize screenSize, + {bool scale = true}) async { + if (message.isEmpty) return []; + + final fontProvider = GetIt.instance(); + final usingCustomFont = fontProvider.selectedFont != null; + + List hexStrings = usingCustomFont + ? await _processCustomFontMessage( + message, fontProvider.selectedTextStyle, screenSize, scale) + : await _processDefaultFont(message, screenSize, scale); + + if (isInverted) { + return _processInversion(hexStrings, screenSize); + } + + return hexStrings; + } + List _matrixToHex(List> matrix) { return List.generate(matrix.length, (i) { final binary = matrix[i].map((b) => b ? '1' : '0').join(); @@ -34,95 +58,68 @@ class Converters { Future> renderTextToMatrix( String message, TextStyle textStyle, { - int rows = 11, + required int targetWidth, + required int targetHeight, required bool hasDescender, // for characters like j, g, p, q, y }) async { - // Generate combined cache key using font properties and message final fontKey = getFontKey( textStyle.fontFamily ?? 'default', textStyle.fontSize ?? 14.0, textStyle.fontWeight ?? FontWeight.normal, textStyle.fontStyle == FontStyle.italic, ); - final cacheKey = '$fontKey-$message'; + final cacheKey = '$fontKey-$message-$targetWidth-$targetHeight'; - // Check character cache if (_characterCache.containsKey(cacheKey)) { - //print("Cache hit for $cacheKey"); return { 'matrix': _characterCache[cacheKey]!, }; } - int cols = 1; - int scale = 1; - // Calculate canvas size - TextPainter widthCheckPainter = TextPainter( + // Calculate font size to fit within target dimensions + double fontSize = textStyle.fontSize ?? 14.0; + TextPainter sizeCheckPainter = TextPainter( text: TextSpan( - text: message, - style: textStyle.copyWith( - color: Colors.black, fontSize: (textStyle.fontSize ?? 14) * scale), - ), + text: message, style: textStyle.copyWith(fontSize: fontSize)), textDirection: TextDirection.ltr, ); - widthCheckPainter.layout(); - final rawWidth = widthCheckPainter.width; - // Check if character needs more width + sizeCheckPainter.layout(); + + // Scale font size to fit target dimensions while maintaining aspect ratio + double scaleX = targetWidth / sizeCheckPainter.width; + double scaleY = targetHeight / sizeCheckPainter.height; + double scale = min(scaleX, scaleY); - // Dynamic column calculation - final actualCols = (rawWidth / scale).ceil().clamp(1, 16); + fontSize = fontSize * scale; + final scaledStyle = textStyle.copyWith(fontSize: fontSize); - //print("Actual cols: $actualCols"); - cols = actualCols; + // Create final text painter with scaled font + final TextPainter textPainter = TextPainter( + text: TextSpan(text: message, style: scaledStyle), + textDirection: TextDirection.ltr, + ); + textPainter.layout(); - // Calculate final dimensions - final int width = cols * scale; - final int height = rows * scale; + final int width = targetWidth; + final int height = targetHeight; - // Create single PictureRecorder and Canvas final ui.PictureRecorder recorder = ui.PictureRecorder(); final Canvas canvas = Canvas( recorder, Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble())); - // Fill background final Paint bgPaint = Paint()..color = Colors.white; canvas.drawRect( Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()), bgPaint); - // Create text painter with final dimensions - final TextPainter textPainter = TextPainter( - text: TextSpan( - text: message, - style: textStyle.copyWith( - color: Colors.black, fontSize: (textStyle.fontSize ?? 14) * scale), - ), - textDirection: TextDirection.ltr, - ); + // Center the text in the canvas + final double textWidth = textPainter.width; + final double textHeight = textPainter.height; + final double offsetX = (width - textWidth) / 2; + final double offsetY = hasDescender + ? (height - textHeight) - 2 // Leave space for descenders + : (height - textHeight) / 2; - textPainter.layout(maxWidth: width.toDouble()); - Offset offset; - if (hasDescender) { - // For descender characters, align so descender can use bottom row - final baselinePosition = height - 2; // Leave 1 unit at bottom - offset = Offset( - 0, - baselinePosition - - textPainter - .computeDistanceToActualBaseline(TextBaseline.alphabetic), - ); - } else { - // For normal characters, ensure bottom padding of 1 unit - offset = Offset( - 0, - (height - 1) - // Leave 1 unit at bottom - textPainter - .computeDistanceToActualBaseline(TextBaseline.alphabetic), - ); - } - - //print("height: $height, offset: $offset"); - - textPainter.paint(canvas, offset); + textPainter.paint(canvas, Offset(offsetX, offsetY)); final ui.Picture picture = recorder.endRecording(); final ui.Image image = await picture.toImage(width, height); @@ -135,9 +132,9 @@ class Converters { final Uint8List data = byteData.buffer.asUint8List(); List> matrix = - List.generate(rows, (_) => List.generate(cols, (_) => false)); - for (int row = 0; row < rows; row++) { - for (int col = 0; col < cols; col++) { + List.generate(height, (_) => List.generate(width, (_) => false)); + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { final int pixelIndex = (row * width + col) * 4; if (pixelIndex + 3 < data.length) { @@ -151,16 +148,14 @@ class Converters { } } - // Cache the result for future use _characterCache[cacheKey] = matrix; return {'matrix': matrix}; } Future> _processCustomFontMessage( - String text, TextStyle style) async { + String text, TextStyle style, ScreenSize size, bool scale) async { try { List> segments = []; - // Parse text into segments String currentText = ''; int i = 0; while (i < text.length) { @@ -181,9 +176,8 @@ class Converters { segments.add({'type': 'text', 'content': currentText}); } - List> combinedMatrix = List.generate(11, (_) => []); + List> combinedMatrix = List.generate(size.height, (_) => []); - // Process each segment for (var segment in segments) { if (segment['type'] == 'text') { String text = segment['content']; @@ -191,14 +185,15 @@ class Converters { String char = text[i]; bool hasDescender = "ypgqj".contains(char); final matrixData = await renderTextToMatrix(char, style, - rows: 11, hasDescender: hasDescender); + targetWidth: size.width, + targetHeight: size.height, + hasDescender: hasDescender); List> charMatrix = matrixData['matrix']; - for (int row = 0; row < 11; row++) { + for (int row = 0; row < size.height; row++) { combinedMatrix[row].addAll(charMatrix[row]); } } } else if (segment['type'] == 'image') { - // Process bitmap int index = segment['index']; var key = controllerData.imageCache.keys.toList()[index]; List hexStrings; @@ -212,12 +207,12 @@ class Converters { image.map((list) => list.cast()).toList(); hexStrings = convertBitmapToLEDHex(imageData, true); } else { - hexStrings = - await imageUtils.generateLedHex(controllerData.vectors[index]); + hexStrings = await imageUtils.generateLedHexWithSize( + controllerData.vectors[index], size.width, size.height); } for (var hex in hexStrings) { - for (int i = 0; i < 11; i++) { + for (int i = 0; i < size.height; i++) { String hexByte = hex.substring(i * 2, (i * 2) + 2); int value = int.parse(hexByte, radix: 16); for (int bit = 0; bit < 8; bit++) { @@ -245,8 +240,8 @@ class Converters { for (int seg = 0; seg < segmentsCount; seg++) { final startCol = seg * 8; final endCol = startCol + 8; - final segmentMatrix = List.generate( - 11, (row) => combinedMatrix[row].sublist(startCol, endCol)); + final segmentMatrix = List.generate(size.height, + (row) => combinedMatrix[row].sublist(startCol, endCol)); final List hexBytes = _matrixToHex(segmentMatrix); final String segmentHex = hexBytes.join(); @@ -261,26 +256,8 @@ class Converters { } } - Future> messageTohex(String message, bool isInverted) async { - if (message.isEmpty) return []; - - final fontProvider = GetIt.instance(); - final usingCustomFont = fontProvider.selectedFont != null; - - // Process message in custom font mode or default mode - List hexStrings = usingCustomFont - ? await _processCustomFontMessage( - message, fontProvider.selectedTextStyle) - : await _processDefaultFont(message); - - if (isInverted) { - return _processInversion(hexStrings); - } - - return hexStrings; - } - - Future> _processDefaultFont(String text) async { + Future> _processDefaultFont( + String text, ScreenSize size, bool scale) async { List> segments = []; String currentText = ''; @@ -307,11 +284,19 @@ class Converters { for (var segment in segments) { if (segment['type'] == 'text') { String text = segment['content']; - hexStrings.addAll(text - .split('') - .where((char) => converter.charCodes.containsKey(char)) - .map((char) => converter.charCodes[char]!) - .toList()); + for (int i = 0; i < text.length; i++) { + String ch = text[i]; + if (!converter.charCodes.containsKey(ch)) continue; + String hex = converter.charCodes[ch]!; + + if (!scale) { + hexStrings.add(hex); + } else { + var scaledBitmap = + _scaleCharacterToBadgeSize(hex, size.width, size.height); + hexStrings.addAll(convertBitmapToLEDHex(scaledBitmap, true)); + } + } } else if (segment['type'] == 'image') { int index = segment['index']; var key = controllerData.imageCache.keys.toList()[index]; @@ -323,17 +308,51 @@ class Converters { image.map((list) => list.cast()).toList(); hexStrings.addAll(convertBitmapToLEDHex(imageData, true)); } else { - hexStrings.addAll( - await imageUtils.generateLedHex(controllerData.vectors[index])); + hexStrings.addAll(await imageUtils.generateLedHexWithSize( + controllerData.vectors[index], size.width, size.height)); } } } return hexStrings; } - List _processInversion(List hexStrings) { + List _processInversion( + List hexStrings, ScreenSize screenSize) { final inverted = invertHex(hexStrings.join()).split(''); - return padHexString(inverted); + return padHexString(inverted, screenSize); + } + + List> _scaleCharacterToBadgeSize( + String hex, int width, int height) { + var bitmap = _hexStringToBitmap(hex); + int scaledWidth = (width * 0.12).round().clamp(6, width ~/ 2); + return _scaleTextCharacterToBadgeSize(bitmap, scaledWidth, height); + } + + List> _scaleTextCharacterToBadgeSize( + List> bitmap, int targetW, int targetH) { + if (bitmap.isEmpty || bitmap[0].isEmpty) { + return List.generate(targetH, (_) => List.filled(targetW, 0)); + } + + return List.generate(targetH, (y) { + int srcY = + (y * bitmap.length / targetH).floor().clamp(0, bitmap.length - 1); + return List.generate(targetW, (x) { + int srcX = (x * bitmap[0].length / targetW) + .floor() + .clamp(0, bitmap[0].length - 1); + return bitmap[srcY][srcX]; + }); + }); + } + + List> _hexStringToBitmap(String hex) { + const int width = 8, height = 11; + return List.generate(height, (row) { + int byteVal = int.parse(hex.substring(row * 2, row * 2 + 2), radix: 16); + return List.generate(width, (col) => (byteVal >> (7 - col)) & 1); + }); } //function to convert the bitmap to the LED hex format @@ -440,26 +459,54 @@ class Converters { return allHexs; // Return list of hexadecimal strings } - static String invertHex(String hex) { - StringBuffer invertedHex = StringBuffer(); - for (int i = 0; i < hex.length; i++) { - String invertedHexDigit = - (~int.parse(hex[i], radix: 16) & 0xF).toRadixString(16).toUpperCase(); - invertedHex.write(invertedHexDigit); + static String invertHex(String hex) => hex + .split('') + .map((c) => + (~int.parse(c, radix: 16) & 0xF).toRadixString(16).toUpperCase()) + .join(); + + List padHexString(List hex, ScreenSize screenSize) { + var boolGrid = hexStringToBool(hex.join(), screenSize.height) + .map((row) => row.map((e) => e ? 1 : 0).toList()) + .toList(); + + for (var row in boolGrid) { + row.insert(0, 1); + row.add(1); } - return invertedHex.toString(); - } - List padHexString(List hexString) { - List> hexArray = hexStringToBool(hexString.join()).map((e) { - return e.map((e) => e ? 1 : 0).toList(); - }).toList(); + return convertBitmapToLEDHex(boolGrid, true); + } - for (int i = 0; i < hexArray.length; i++) { - hexArray[i].insert(0, 1); - hexArray[i].add(1); + static List> textToBitmapFixedWidth( + String msg, + int height, + DataToByteArrayConverter conv, + ) { + const int w = 8, h = 11, spacing = 2; + if (msg.isEmpty) return List.generate(height, (_) => []); + + int totalWidth = msg.length * (w + spacing) - spacing; + var bitmap = List.generate(height, (_) => List.filled(totalWidth, false)); + + for (int i = 0; i < msg.length; i++) { + var hex = conv.charCodes[msg[i]]; + if (hex == null) continue; + + var charBitmap = List.generate(h, (row) { + int byte = int.parse(hex.substring(row * 2, row * 2 + 2), radix: 16); + return List.generate(w, (col) => ((byte >> (7 - col)) & 1) == 1); + }); + + int offsetX = i * (w + spacing); + for (int row = 0; row < height; row++) { + int srcRow = ((row * h) / height).floor().clamp(0, h - 1); + for (int col = 0; col < w; col++) { + bitmap[row][offsetX + col] = charBitmap[srcRow][col]; + } + } } - return convertBitmapToLEDHex(hexArray, true); + return bitmap; } } diff --git a/lib/bademagic_module/utils/image_utils.dart b/lib/bademagic_module/utils/image_utils.dart index 79d0986d1..c4d0a5760 100644 --- a/lib/bademagic_module/utils/image_utils.dart +++ b/lib/bademagic_module/utils/image_utils.dart @@ -83,16 +83,25 @@ class ImageUtils { final ui.Canvas canvas = Canvas(recorder, Rect.fromPoints(Offset.zero, Offset(targetWidth, targetHeight))); + // Calculate scale factors double scaleX = targetWidth / inputImage.width; double scaleY = targetHeight / inputImage.height; + // Use the smaller scale to ensure the image fits within bounds double scale = scaleX < scaleY ? scaleX : scaleY; - double dx = (targetWidth - (inputImage.width * scale)) / 2; - double dy = (targetHeight - (inputImage.height * scale)) / 2; + // Center the scaled image + double scaledWidth = inputImage.width * scale; + double scaledHeight = inputImage.height * scale; + double dx = (targetWidth - scaledWidth) / 2; + double dy = (targetHeight - scaledHeight) / 2; + + // Fill background with transparent + canvas.drawRect(Rect.fromLTWH(0, 0, targetWidth, targetHeight), + Paint()..color = Colors.transparent); + canvas.translate(dx, dy); canvas.scale(scale, scale); - canvas.drawImage(inputImage, Offset.zero, Paint()); final ui.Image imgByteData = await recorder @@ -226,6 +235,30 @@ class ImageUtils { return Converters.convertBitmapToLEDHex(pixelArray, true); } + Future> generateLedHexWithSize( + String asset, int targetWidth, int targetHeight) async { + await _loadSVG(asset); + ui.Image image = + await picture.toImage(originalWidth.toInt(), originalHeight.toInt()); + + final ui.Image scaledImage = + await _scaleSVG(image, targetHeight.toDouble(), targetWidth.toDouble()); + final ui.Image trimmedImage = await _trimSVG(scaledImage); + final Uint8List? byteArray = await _convertImageToByteArray(trimmedImage); + final List> pixelArray = _convertUint8ListTo2DList( + byteArray!, trimmedImage.width, trimmedImage.height); + + for (int x = 0; x < pixelArray.length; x++) { + for (int y = 0; y < pixelArray[x].length; y++) { + if (pixelArray[x][y] != 0) { + pixelArray[x][y] = 1; + } + } + } + + return Converters.convertBitmapToLEDHex(pixelArray, true); + } + List convertGifFramesToLEDHex(Uint8List gifBytes) { final gifImage = img.decodeGif(gifBytes); if (gifImage == null) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 697877483..199f7beeb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -79,6 +79,7 @@ "beatingHearts": "Beating Hearts", "fireworks": "Fireworks", "equalizer": "Equalizer", + "cycle": "Cycle", "switchToSpecialAnimation": "Switch to Special Animation?", "specialAnimationWarning": "Selecting this animation will overwrite your current text.", "copyText": "Copy text", @@ -210,5 +211,7 @@ "badgeNameHint": "Badge name", "triangle": "Triangle", "clipartSavedSuccessfully": "Clipart saved successfully", - "badgeScanMode": "Badge Scan Mode" + "badgeScanMode": "Badge Scan Mode", + "selectScreenSize": "Select Screen Size", + "enterValidBadgeName": "Enter a valid badge name" } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index aa0a7d393..bb6370776 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -71,6 +71,7 @@ "beatingHearts": "धड़कते दिल", "fireworks": "पटाखे", "equalizer": "इक्वलाइज़र", + "cycle": "साइकिल", "switchToSpecialAnimation": "स्पेशल एनिमेशन लगाएं?", "specialAnimationWarning": "यह एनिमेशन चुनने से आपका टेक्स्ट बदल जाएगा।", "copyText": "टेक्स्ट कॉपी करें", @@ -199,5 +200,7 @@ "scanSettingsSaved": "स्कैन सेटिंग्स सेव हो गईं", "saveSettings": "सेटिंग्स सेव करें", "connectToAnyBadge": "किसी भी बैज से कनेक्ट करें", - "badgeNameHint": "बैज का नाम" -} \ No newline at end of file + "badgeNameHint": "बैज का नाम", + "selectScreenSize": "स्क्रीन साइज चुनें", + "enterValidBadgeName": "एक वैध बैज नाम दर्ज करें" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b568c6910..3f8dd8174 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -542,6 +542,12 @@ abstract class AppLocalizations { /// **'Equalizer'** String get equalizer; + /// No description provided for @cycle. + /// + /// In en, this message translates to: + /// **'Cycle'** + String get cycle; + /// No description provided for @switchToSpecialAnimation. /// /// In en, this message translates to: @@ -1147,6 +1153,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Badge Scan Mode'** String get badgeScanMode; + + /// No description provided for @selectScreenSize. + /// + /// In en, this message translates to: + /// **'Select Screen Size'** + String get selectScreenSize; + + /// No description provided for @enterValidBadgeName. + /// + /// In en, this message translates to: + /// **'Enter a valid badge name'** + String get enterValidBadgeName; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e07c0c489..c65fd4aaf 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -237,6 +237,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get equalizer => 'Equalizer'; + @override + String get cycle => 'Cycle'; + @override String get switchToSpecialAnimation => 'Switch to Special Animation?'; @@ -549,4 +552,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get badgeScanMode => 'Badge Scan Mode'; + + @override + String get selectScreenSize => 'Select Screen Size'; + + @override + String get enterValidBadgeName => 'Enter a valid badge name'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index ea891420a..0c0e8cf86 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -235,6 +235,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get equalizer => 'इक्वलाइज़र'; + @override + String get cycle => 'साइकिल'; + @override String get switchToSpecialAnimation => 'स्पेशल एनिमेशन लगाएं?'; @@ -546,4 +549,10 @@ class AppLocalizationsHi extends AppLocalizations { @override String get badgeScanMode => 'बैज स्कैन मोड'; + + @override + String get selectScreenSize => 'स्क्रीन साइज चुनें'; + + @override + String get enterValidBadgeName => 'एक वैध बैज नाम दर्ज करें'; } diff --git a/lib/main.dart b/lib/main.dart index f953de1a2..9c2a12443 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,21 +1,22 @@ +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/providers/font_provider.dart'; import 'package:badgemagic/providers/BadgeScanProvider.dart'; import 'package:badgemagic/providers/getitlocator.dart'; import 'package:badgemagic/providers/imageprovider.dart'; import 'package:badgemagic/view/about_us_screen.dart'; -import 'package:badgemagic/view/draw_badge_screen.dart'; import 'package:badgemagic/view/homescreen.dart'; import 'package:badgemagic/view/save_badge_screen.dart'; import 'package:badgemagic/view/saved_clipart.dart'; import 'package:badgemagic/view/settings_screen.dart'; +import 'package:badgemagic/view/draw_badge_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'l10n/app_localizations.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; -import 'globals/globals.dart' as globals; import 'services/localization_service.dart'; +import 'globals/globals.dart' as globals; Future main() async { setupLocator(); @@ -23,8 +24,6 @@ Future main() async { // Initialize global localization service for usage outside of widgets final localizationService = getIt(); - // Keep initial UI in English for integration tests that tap by English text - // Apply saved locale on the next frame so visible strings change after first paint final saved = await localizationService.loadSavedLocale(); appLocale.value = const Locale('en'); await localizationService.init(appLocale.value ?? const Locale('en')); @@ -72,6 +71,7 @@ class MyApp extends StatelessWidget { if (locale != null) { getIt().updateLocale(locale); } + return MaterialApp( scaffoldMessengerKey: globals.scaffoldMessengerKey, debugShowCheckedModeBanner: false, @@ -102,7 +102,8 @@ class MyApp extends StatelessWidget { initialRoute: '/', routes: { '/': (context) => const HomeScreen(), - '/drawBadge': (context) => const DrawBadge(), + '/drawBadge': (context) => + DrawBadge(selectedSize: supportedScreenSizes.first), '/savedBadge': (context) => const SaveBadgeScreen(), '/savedClipart': (context) => const SavedClipart(), '/aboutUs': (context) => const AboutUsScreen(), diff --git a/lib/providers/animation_badge_provider.dart b/lib/providers/animation_badge_provider.dart index 366a1de4a..e022aab16 100644 --- a/lib/providers/animation_badge_provider.dart +++ b/lib/providers/animation_badge_provider.dart @@ -24,6 +24,8 @@ import 'package:badgemagic/badge_animation/ani_diagonal.dart'; import 'package:badgemagic/badge_animation/ani_emergency.dart'; import 'package:badgemagic/badge_animation/ani_beating_hearts.dart'; import 'package:badgemagic/badge_animation/ani_fireworks.dart'; +import 'package:badgemagic/badge_animation/ani_equalizer.dart'; // Equalizer +import 'package:badgemagic/badge_animation/ani_cycle.dart'; // Cycle import 'package:badgemagic/badge_animation/animation_abstract.dart'; import 'package:badgemagic/badge_effect/badgeeffectabstract.dart'; import 'package:badgemagic/badge_effect/flash_effect.dart'; @@ -31,8 +33,7 @@ import 'package:badgemagic/badge_effect/invert_led_effect.dart'; import 'package:badgemagic/badge_effect/marquee_effect.dart'; import 'package:badgemagic/constants.dart'; import 'package:flutter/material.dart'; -import 'package:badgemagic/badge_animation/ani_equalizer.dart'; // new import of EqualizerAnimation -import 'package:badgemagic/badge_animation/ani_cycle.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; Map animationMap = { 0: LeftAnimation(), @@ -44,19 +45,19 @@ Map animationMap = { 6: SnowFlakeAnimation(), 7: PictureAnimation(), 8: LaserAnimation(), - 9: PacmanClassicAnimation(), // Pacman - 10: LeftChevronAnimation(), // Chevron left - 11: DiamondAnimation(), // Diamond - 12: BrokenHeartsAnimation(), // Broken Hearts - 13: CupidAnimation(), // Cupid - 14: FeetAnimation(), // Feet - 15: FishAnimation(), // Fish - 16: DiagonalAnimation(), // Diagonal - 17: EmergencyAnimation(), // Emergency - 18: BeatingHeartsAnimation(), // Beating Hearts - 19: FireworksAnimation(), // Fireworks - 20: EqualizerAnimation(), // Digital Rain - 21: CycleAnimation(), // Cycle + 9: PacmanClassicAnimation(), + 10: LeftChevronAnimation(), + 11: DiamondAnimation(), + 12: BrokenHeartsAnimation(), + 13: CupidAnimation(), + 14: FeetAnimation(), + 15: FishAnimation(), + 16: DiagonalAnimation(), + 17: EmergencyAnimation(), + 18: BeatingHeartsAnimation(), + 19: FireworksAnimation(), + 20: EqualizerAnimation(), + 21: CycleAnimation(), }; Map effectMap = { @@ -71,47 +72,36 @@ class AnimationBadgeProvider extends ChangeNotifier { int _animationIndex = 0; int _animationSpeed = aniSpeedStrategy(0); Timer? _timer; + bool _isDisposed = false; - //List that contains the state of each cell of the badge for home view - List> _paintGrid = - List.generate(11, (i) => List.generate(44, (j) => false)); + List> _paintGrid = []; + List> _newGrid = []; + final List>> _frames = []; + int _currentFrame = 0; BadgeAnimation _currentAnimation = LeftAnimation(); - final Set _currentEffect = {}; - //function to get the state of the cell List> getPaintGrid() => _paintGrid; + List> getNewGrid() => _newGrid; - // Helper: returns true if a special animation (custom) is selected bool isSpecialAnimationSelected() { int idx = getAnimationIndex() ?? 0; - // Add all special animation indices here (including Equalizer at 20 and Cycle at 20): return [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21].contains(idx); } - // Call this to reset to text animation (LeftAnimation) void resetToTextAnimation() { setAnimationMode(LeftAnimation()); } - //function to calculate duration for the animation void calculateDuration(int speed) { + if (_isDisposed) return; + int idx = getAnimationIndex() ?? 0; int newSpeed; - if (idx == 9 || - idx == 10 || - idx == 11 || - idx == 12 || - idx == 20 || - idx == 21) { - //added EqualizerAnimation - // Use slower mapping for custom animations - // (aniSpeedStrategy already uses the slower mapping if you want, or you can hardcode) - newSpeed = aniSpeedStrategy(speed - 1); // keep as is, or adjust if needed + if ([9, 10, 11, 12, 20, 21].contains(idx)) { + newSpeed = aniSpeedStrategy(speed - 1); } else { - // Use original (faster) mapping for text/standard animations - // For original: aniBaseSpeed = 200000us, minSpeed = 25000us (example) const int originalBase = 200000; const int minSpeed = 25000; newSpeed = originalBase - ((speed - 1) * (originalBase - minSpeed) ~/ 8); @@ -123,14 +113,17 @@ class AnimationBadgeProvider extends ChangeNotifier { } } - List> _newGrid = - List.generate(11, (i) => List.generate(44, (j) => false)); - - //getter for newGrid - List> getNewGrid() => _newGrid; + void initGrids(ScreenSize size) { + if (_isDisposed) return; + _paintGrid = List.generate( + size.height, (_) => List.generate(size.width, (_) => false)); + _newGrid = List.generate( + size.height, (_) => List.generate(size.width, (_) => false)); + notifyListeners(); + } - //setter for newGrid void setNewGrid(List> grid) { + if (_isDisposed) return; _newGrid = grid; _animationIndex = 0; notifyListeners(); @@ -138,19 +131,21 @@ class AnimationBadgeProvider extends ChangeNotifier { Set get getCurrentEffect => _currentEffect; - /// Clears all currently active effects void clearAllEffects() { + if (_isDisposed) return; _currentEffect.clear(); notifyListeners(); } void addEffect(BadgeEffect? effect) { + if (_isDisposed) return; _currentEffect.add(effect); logger.i("Effect Added: $effect : $_currentEffect"); notifyListeners(); } void removeEffect(BadgeEffect? effect) { + if (_isDisposed) return; _currentEffect.remove(effect); notifyListeners(); } @@ -160,32 +155,38 @@ class AnimationBadgeProvider extends ChangeNotifier { } void initializeAnimation() { - if (_timer == null) { - startTimer(); - } + if (_isDisposed) return; + if (_timer == null || !_timer!.isActive) startTimer(); } - //function to stop timer and reset the animationIndex void stopAnimation() { logger.d("Timer stopped ${_timer?.tick.toString()}"); _timer?.cancel(); - + _timer = null; _animationIndex = 0; } void stopAllAnimations() { - // Stop any ongoing timer and reset the animation index stopAnimation(); _currentAnimation = LeftAnimation(); - // Reset the grids to all false values - _paintGrid = List.generate(11, (i) => List.generate(44, (j) => false)); - _newGrid = List.generate(11, (i) => List.generate(44, (j) => false)); + _paintGrid = []; + _newGrid = []; logger.d("All animations stopped"); } void startTimer() { - _timer = - Timer.periodic(Duration(microseconds: _animationSpeed), (Timer timer) { + if (_isDisposed) return; + if (_newGrid.isEmpty || _newGrid[0].isEmpty) { + logger.w("Cannot start animation timer: _newGrid is empty"); + return; + } + _timer?.cancel(); + + _timer = Timer.periodic(Duration(microseconds: _animationSpeed), (timer) { + if (_isDisposed) { + timer.cancel(); + return; + } renderGrid(getNewGrid()); if (_currentAnimation is CupidAnimation) { int frameLimit = @@ -198,12 +199,10 @@ class AnimationBadgeProvider extends ChangeNotifier { } void setAnimationMode(BadgeAnimation? animation) { - // Always reset the animation index and set the new animation + if (_isDisposed) return; _animationIndex = 0; _currentAnimation = animation ?? LeftAnimation(); - // Stop the timer if running _timer?.cancel(); - // Start the timer for the new animation startTimer(); notifyListeners(); logger.i("Animation Mode set to: $_currentAnimation and timer restarted"); @@ -220,34 +219,92 @@ class AnimationBadgeProvider extends ChangeNotifier { } bool isAnimationActive(BadgeAnimation? badgeAnimation) { - bool isActive = _currentAnimation == badgeAnimation; - return isActive; + return _currentAnimation == badgeAnimation; } - void badgeAnimation( - String message, Converters converters, bool isInverted) async { + void badgeAnimation(String message, Converters converters, bool isInverted, + ScreenSize screenSize) async { + if (_isDisposed) return; + bool isSpecial = isSpecialAnimationSelected(); if (message.isEmpty && !isSpecial) { stopAllAnimations(); - List> emptyGrid = - List.generate(11, (i) => List.generate(44, (j) => false)); + List> emptyGrid = List.generate(screenSize.height, + (i) => List.generate(screenSize.width, (j) => false)); _newGrid = emptyGrid; _paintGrid = emptyGrid; notifyListeners(); return; } - if (_timer == null || !_timer!.isActive) { - startTimer(); + + if (_timer == null || !_timer!.isActive) startTimer(); + + List> fullBitmap; + + if (message.contains('<<') && message.contains('>>')) { + List hexStrings = await converters.messageTohex( + message, isInverted, screenSize.height, screenSize, + scale: true); + fullBitmap = _hexStringsToBitmap(hexStrings, screenSize); + } else { + List hexStrings = await converters.messageTohex( + message, isInverted, screenSize.height, screenSize, + scale: true); + fullBitmap = _hexStringsToBitmap(hexStrings, screenSize); + } + + setNewGrid(fullBitmap); + } + + List> _hexStringsToBitmap( + List hexStrings, ScreenSize screenSize) { + if (hexStrings.isEmpty) { + return List.generate(screenSize.height, + (_) => List.generate(screenSize.width, (_) => false)); } - List hexString = await converters.messageTohex(message, isInverted); - List> binaryArray = hexStringToBool(hexString.join()); - setNewGrid(binaryArray); + + int totalWidth = hexStrings.length * 8; + List> bitmap = + List.generate(screenSize.height, (_) => List.filled(totalWidth, false)); + + for (int hexIndex = 0; hexIndex < hexStrings.length; hexIndex++) { + String hexString = hexStrings[hexIndex]; + int charsPerRow = 2; + + for (int row = 0; + row < screenSize.height && row * charsPerRow < hexString.length; + row++) { + int byteStart = row * charsPerRow; + int byteEnd = byteStart + charsPerRow; + + if (byteEnd <= hexString.length) { + String rowHex = hexString.substring(byteStart, byteEnd); + int byteVal = int.parse(rowHex, radix: 16); + + for (int bit = 0; bit < 8; bit++) { + int col = hexIndex * 8 + bit; + if (col < totalWidth) { + bitmap[row][col] = ((byteVal >> (7 - bit)) & 1) == 1; + } + } + } + } + } + return bitmap; } void renderGrid(List> newGrid) { + if (_isDisposed) return; + if (_paintGrid.isEmpty || _paintGrid[0].isEmpty) return; + int badgeWidth = _paintGrid[0].length; int badgeHeight = _paintGrid.length; + if (_frames.isNotEmpty) { + _currentFrame = (_currentFrame + 1) % _frames.length; + newGrid = _frames[_currentFrame]; + } + var canvas = List.generate( badgeHeight, (i) => List.generate(badgeWidth, (j) => false)); @@ -259,10 +316,27 @@ class AnimationBadgeProvider extends ChangeNotifier { } _paintGrid = canvas; - notifyListeners(); + if (!_isDisposed) notifyListeners(); + } + + @override + void notifyListeners() { + if (!_isDisposed) super.notifyListeners(); + } + + @override + void dispose() { + _isDisposed = true; + _timer?.cancel(); + _timer = null; + _currentEffect.clear(); + _frames.clear(); + _paintGrid.clear(); + _newGrid.clear(); + super.dispose(); + logger.d("AnimationBadgeProvider disposed"); } - /// Handles animation transfer selection logic for the current animation index. Future handleAnimationTransfer({ required BadgeMessageProvider badgeData, required InlineImageProvider inlineImageProvider, @@ -270,12 +344,15 @@ class AnimationBadgeProvider extends ChangeNotifier { required bool flash, required bool marquee, required bool invert, - required BuildContext context, + required int badgeHeight, + required int badgeWidth, }) async { + if (_isDisposed) return; + final int aniIndex = getAnimationIndex() ?? 0; final int selectedSpeed = speedDialProvider.getOuterValue(); + if (aniIndex == 9) { - // Pacman await transferPacmanAnimation(badgeData, selectedSpeed); } else if (aniIndex == 10) { await transferChevronAnimation(badgeData, selectedSpeed); @@ -314,7 +391,8 @@ class AnimationBadgeProvider extends ChangeNotifier { modeValueMap[aniIndex], null, false, - context, + badgeHeight, + badgeWidth, ); } } diff --git a/lib/providers/badge_message_provider.dart b/lib/providers/badge_message_provider.dart index 4492adda9..e92c6f422 100644 --- a/lib/providers/badge_message_provider.dart +++ b/lib/providers/badge_message_provider.dart @@ -1,6 +1,7 @@ -import 'dart:io'; +import 'dart:async'; import 'package:badgemagic/bademagic_module/bluetooth/base_ble_state.dart'; import 'package:badgemagic/bademagic_module/bluetooth/datagenerator.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/bademagic_module/utils/converters.dart'; import 'package:badgemagic/bademagic_module/utils/file_helper.dart'; import 'package:badgemagic/bademagic_module/utils/toast_utils.dart'; @@ -17,8 +18,9 @@ import 'package:badgemagic/utils/custom_transfers/transfers.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:get_it/get_it.dart'; import 'package:logger/logger.dart'; -import 'package:provider/provider.dart'; // Import the new EqualizerAnimation +import 'package:provider/provider.dart'; +// Mode and Speed mapping Map modeValueMap = { 0: Mode.left, 1: Mode.right, @@ -29,12 +31,12 @@ Map modeValueMap = { 6: Mode.snowflake, 7: Mode.picture, 8: Mode.laser, - 9: Mode.pacman, // Add this line for Pacman - 10: Mode.chevronleft, // Chevron left mode (now defined in mode.dart) - 11: Mode.diamond, // Diamond animation mode - 12: Mode.brokenhearts, // Broken Hearts mode (use fixed or define if needed) - 13: Mode.cupid, // Cupid mode (use fixed or define if needed) - 14: Mode.feet, // Feet animation mode + 9: Mode.pacman, + 10: Mode.chevronleft, + 11: Mode.diamond, + 12: Mode.brokenhearts, + 13: Mode.cupid, + 14: Mode.feet, }; Map speedMap = { @@ -45,7 +47,7 @@ Map speedMap = { 5: Speed.five, 6: Speed.six, 7: Speed.seven, - 8: Speed.eight, // Add superfast for the highest speed + 8: Speed.eight, }; class BadgeMessageProvider { @@ -56,9 +58,15 @@ class BadgeMessageProvider { Converters converters = Converters(); Future getBadgeData(String text, bool flash, bool marq, Speed speed, - Mode mode, bool isInverted) async { - List message = await converters.messageTohex(text, isInverted); - Data data = Data(messages: [ + Mode mode, bool isInverted, int badgeHeight, int badgeWidth) async { + List message = await converters.messageTohex( + text, + isInverted, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: true, + ); + return Data(messages: [ Message( text: message, flash: flash, @@ -67,22 +75,32 @@ class BadgeMessageProvider { mode: mode, ) ]); - return data; } Future generateData( - String? text, - bool? flash, - bool? marq, - bool? inverted, - Speed? speed, - Mode? mode, - Map? jsonData) async { + String? text, + bool? flash, + bool? marq, + bool? inverted, + Speed? speed, + Mode? mode, + Map? jsonData, + int badgeHeight, + int badgeWidth, + ) async { if (jsonData != null) { return fileHelper.jsonToData(jsonData); } else { - return getBadgeData(text ?? '', flash ?? false, marq ?? false, - speed ?? Speed.one, mode ?? Mode.left, inverted ?? false); + return getBadgeData( + text ?? '', + flash ?? false, + marq ?? false, + speed ?? Speed.one, + mode ?? Mode.left, + inverted ?? false, + badgeHeight, + badgeWidth, + ); } } @@ -112,40 +130,25 @@ class BadgeMessageProvider { } Future checkAndTransfer( - String? text, - bool? flash, - bool? marq, - bool? isInverted, - int? speed, - Mode? mode, - Map? jsonData, - bool isSavedBadge, - BuildContext context, - {TextStyle? textStyle}) async { + String? text, + bool? flash, + bool? marq, + bool? isInverted, + int? speed, + Mode? mode, + Map? jsonData, + bool isSavedBadge, + int badgeHeight, + int badgeWidth, + ) async { if (await FlutterBluePlus.isSupported == false) { final l10n = GetIt.instance.get().l10n; ToastUtils().showErrorToast(l10n.error); return; } - if (controllerData.getController().text.isEmpty && isSavedBadge == false) { - // Allow empty text if Pacman or Fireworks mode is selected - // Fireworks: Mode.fixed and animation index 19 - bool isFireworks = false; - try { - // Try to get animation index from modeValueMap - int fireworksIndex = 19; - int cycleIndex = 20; - if (mode == Mode.fixed && - modeValueMap.containsKey(fireworksIndex) && - modeValueMap[fireworksIndex] == Mode.fixed) { - isFireworks = true; - } - if (mode == Mode.cycle && - modeValueMap.containsKey(cycleIndex) && - modeValueMap[cycleIndex] == Mode.cycle) {} - } catch (_) {} - if (mode != Mode.pacman && !isFireworks) { + if (controllerData.getController().text.isEmpty && !isSavedBadge) { + if (mode != Mode.pacman) { final l10n = GetIt.instance.get().l10n; ToastUtils().showErrorToast(l10n.pleaseEnterMessage); return; @@ -155,54 +158,15 @@ class BadgeMessageProvider { BluetoothAdapterState adapterState = await FlutterBluePlus.adapterState.first; if (adapterState != BluetoothAdapterState.on) { - if (Platform.isAndroid) { - final l10n = GetIt.instance.get().l10n; - ToastUtils().showToast(l10n.loading); - try { - await FlutterBluePlus.turnOn(); - } catch (e) { - ToastUtils().showErrorToast('Failed to enable Bluetooth: $e'); - logger.e('Bluetooth turnOn() failed: $e'); - return; - } - - try { - adapterState = await FlutterBluePlus.adapterState - .where((state) => state == BluetoothAdapterState.on) - .first - .timeout( - const Duration(seconds: 10), - onTimeout: () { - ToastUtils().showErrorToast('Bluetooth did not turn on in time.'); - throw Exception('Bluetooth enable timeout'); - }, - ); - } catch (e) { - logger.e('Error while waiting for Bluetooth to turn on: $e'); - return; - } - } else if (Platform.isIOS) { - final l10n = GetIt.instance.get().l10n; - ToastUtils().showErrorToast(l10n.error); - - try { - adapterState = await FlutterBluePlus.adapterState - .where((state) => state == BluetoothAdapterState.on) - .first - .timeout( - const Duration(seconds: 10), - onTimeout: () { - ToastUtils().showErrorToast('Bluetooth did not turn on in time.'); - throw Exception('Bluetooth enable timeout'); - }, - ); - } catch (e) { - logger.e('Error while waiting for Bluetooth to turn on: $e'); - return; - } - } else { - final l10n = GetIt.instance.get().l10n; - ToastUtils().showErrorToast(l10n.error); + try { + await FlutterBluePlus.turnOn(); + adapterState = await FlutterBluePlus.adapterState + .where((state) => state == BluetoothAdapterState.on) + .first + .timeout(const Duration(seconds: 10)); + } catch (e) { + ToastUtils().showErrorToast('Bluetooth enable failed: $e'); + logger.e('Bluetooth turnOn() failed: $e'); return; } } @@ -210,20 +174,9 @@ class BadgeMessageProvider { Data data; if (jsonData != null) { data = fileHelper.jsonToData(jsonData); - if (isSavedBadge && data.messages.isNotEmpty) { - final old = data.messages[0]; - final newMessage = Message( - text: old.text, // use the already-padded hex string - flash: old.flash, - marquee: old.marquee, - speed: old.speed, - mode: Mode.animation, // Force seamless marquee - ); - data = Data(messages: [newMessage, ...data.messages.skip(1)]); - } } else { - data = await generateData( - text, flash, marq, isInverted, speedMap[speed], mode, jsonData); + data = await generateData(text, flash, marq, isInverted, speedMap[speed], + mode, jsonData, badgeHeight, badgeWidth); } DataTransferManager manager = DataTransferManager(data); @@ -231,6 +184,7 @@ class BadgeMessageProvider { } } +// Animation-specific transfers Future transferFireworksAnimation( BadgeMessageProvider badgeDataProvider, int speedLevel) async { return customTransferFireworksAnimation( @@ -249,14 +203,12 @@ Future transferEmergencyAnimation( (manager) => badgeDataProvider.transferData(manager), speedLevel); } -/// Transfers the continuous diagonal V animation to the badge hardware. Future transferDiagonalAnimation( BadgeMessageProvider badgeDataProvider, int speedLevel) async { return customTransferDiagonalAnimation( (manager) => badgeDataProvider.transferData(manager), speedLevel); } -/// Transfers the Fish Kiss animation to the badge, even if the homescreen text box is empty. Future transferFishAnimation( BadgeMessageProvider badgeDataProvider, int speedLevel) async { return customTransferFishAnimation( @@ -310,5 +262,3 @@ Future transferCycleAnimation( return customTransferCycleAnimation( (manager) => badgeDataProvider.transferData(manager), speedLevel); } - -// helper moved to utils/custom_transfers/common.dart diff --git a/lib/providers/draw_badge_provider.dart b/lib/providers/draw_badge_provider.dart index 557021f79..af370b366 100644 --- a/lib/providers/draw_badge_provider.dart +++ b/lib/providers/draw_badge_provider.dart @@ -1,24 +1,24 @@ import 'package:flutter/material.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/badge_animation/ani_left.dart'; import 'package:badgemagic/badge_animation/animation_abstract.dart'; enum DrawShape { freehand, square, rectangle, circle, triangle } class DrawBadgeProvider extends ChangeNotifier { - final int rows = 11; - final int cols = 44; - - List> _drawViewGrid = - List.generate(11, (_) => List.generate(44, (_) => false)); - final List> _previewGrid = - List.generate(11, (_) => List.generate(44, (_) => false)); + List> _drawViewGrid = []; + List> _previewGrid = []; final List>> _undoStack = []; final List>> _redoStack = []; + ScreenSize _currentSize = supportedScreenSizes.first; bool isDrawing = true; DrawShape _selectedShape = DrawShape.freehand; BadgeAnimation currentAnimation = LeftAnimation(); + int get rows => _drawViewGrid.length; + int get cols => _drawViewGrid.isNotEmpty ? _drawViewGrid[0].length : 0; + // ========== GETTERS ========== List> getDrawViewGrid() { // Merge preview + permanent grid @@ -32,6 +32,7 @@ class DrawBadgeProvider extends ChangeNotifier { } DrawShape get selectedShape => _selectedShape; + ScreenSize getCurrentSize() => _currentSize; bool getIsDrawing() => isDrawing; bool get canUndo => _undoStack.isNotEmpty; bool get canRedo => _redoStack.isNotEmpty; @@ -47,53 +48,70 @@ class DrawBadgeProvider extends ChangeNotifier { notifyListeners(); } + void initGridWithSize(ScreenSize size) { + _currentSize = size; + _drawViewGrid = List.generate( + size.height, (_) => List.generate(size.width, (_) => false)); + _previewGrid = List.generate( + size.height, (_) => List.generate(size.width, (_) => false)); + notifyListeners(); + } + void setCell(int row, int col, bool value, {bool preview = false}) { if (row >= 0 && row < rows && col >= 0 && col < cols) { if (preview) { _previewGrid[row][col] = value; } else { _drawViewGrid[row][col] = value; - notifyListeners(); } + notifyListeners(); } } - void clearPreviewGrid() { - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - _previewGrid[i][j] = false; - } + void setDrawViewGrid(int row, int col) { + if (row >= 0 && row < rows && col >= 0 && col < cols) { + _drawViewGrid[row][col] = isDrawing; + notifyListeners(); } } - void commitGridUpdate() { + void resetDrawViewGrid() { + _pushToUndoStack(); + _drawViewGrid = + List.generate(rows, (_) => List.generate(cols, (_) => false)); + notifyListeners(); + } + + void updateDrawViewGrid(List> badgeData) { _pushToUndoStack(); for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { - if (_previewGrid[i][j]) { - _drawViewGrid[i][j] = _previewGrid[i][j]; - } + _drawViewGrid[i][j] = (i < badgeData.length && j < badgeData[i].length) + ? badgeData[i][j] + : false; } } - clearPreviewGrid(); notifyListeners(); } - void resetDrawViewGrid() { - _pushToUndoStack(); - _drawViewGrid = - List.generate(rows, (_) => List.generate(cols, (_) => false)); - notifyListeners(); + void clearPreviewGrid() { + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + _previewGrid[i][j] = false; + } + } } - void updateDrawViewGrid(List> badgeData) { + void commitGridUpdate() { _pushToUndoStack(); for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { - _drawViewGrid[i][j] = - (j < badgeData[0].length) ? badgeData[i][j] : false; + if (_previewGrid[i][j]) { + _drawViewGrid[i][j] = true; + } } } + clearPreviewGrid(); notifyListeners(); } @@ -135,8 +153,7 @@ class DrawBadgeProvider extends ChangeNotifier { } class GridPosition { - final int x; - final int y; - - GridPosition(this.x, this.y); + final int row; + final int col; + GridPosition(this.row, this.col); } diff --git a/lib/providers/saved_badge_provider.dart b/lib/providers/saved_badge_provider.dart index dd951b19c..840b8a0a2 100644 --- a/lib/providers/saved_badge_provider.dart +++ b/lib/providers/saved_badge_provider.dart @@ -3,6 +3,7 @@ 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/screen_size.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'; @@ -42,7 +43,7 @@ Map modeValueMap = { class SavedBadgeProvider extends ChangeNotifier { /// Applies saved badge data to the UI providers and controllers. /// Moves logic out of HomeScreen._applySavedBadgeData for better separation of concerns. - Future applySavedBadgeDataToUI({ + Future applySavedBadgeDataToUI({ required Map savedData, required String? savedBadgeFilename, required AnimationBadgeProvider animationProvider, @@ -159,6 +160,19 @@ class SavedBadgeProvider extends ChangeNotifier { // Notify that we're editing an existing badge ToastUtils().showToast( "Editing badge: ${savedBadgeFilename != null ? savedBadgeFilename.substring(0, savedBadgeFilename.length - 5) : ""}"); + + // Extract screen size from saved data + ScreenSize? savedScreenSize; + if (savedData.containsKey('height') && savedData.containsKey('width')) { + final height = savedData['height'] as int?; + final width = savedData['width'] as int?; + if (height != null && width != null) { + savedScreenSize = supportedScreenSizes.firstWhere( + (size) => size.height == height && size.width == width, + orElse: () => supportedScreenSizes.first); + } + } + return savedScreenSize; } Converters converters = Converters(); @@ -172,15 +186,25 @@ class SavedBadgeProvider extends ChangeNotifier { notifyListeners(); } - void saveBadgeData(String filename, String message, bool isFlash, - bool isMarquee, bool isInvert, int? speed, int animation) async { + void saveBadgeData( + String filename, + String message, + bool isFlash, + bool isMarquee, + bool isInvert, + int? speed, + int animation, + int badgeHeight, + int badgeWidth) async { Data data = await getBadgeData( message, - isFlash, //needs aniEffectProvider + isFlash, isMarquee, - isInvert, //needs Anieffect provider - speedMap[speed] ?? Speed.one, //needs speed dial provider + isInvert, + speedMap[speed] ?? Speed.one, modeValueMap[animation]!, + badgeHeight, + badgeWidth, ); // Save the badge data to a file @@ -202,8 +226,16 @@ class SavedBadgeProvider extends ChangeNotifier { /// @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 { + Future updateBadgeData( + String filename, + String message, + bool isFlash, + bool isMarquee, + bool isInvert, + int? speed, + int animation, + int badgeWidth, + int badgeHeight) async { // Make sure filename doesn't have .json extension String cleanFilename = filename; if (cleanFilename.endsWith('.json')) { @@ -220,6 +252,8 @@ class SavedBadgeProvider extends ChangeNotifier { isInvert, speedMap[speed] ?? Speed.one, modeValueMap[animation]!, + badgeHeight, + badgeWidth, ); try { @@ -272,9 +306,23 @@ class SavedBadgeProvider extends ChangeNotifier { logger.d('Updated badge with new text: $message'); } - Future getBadgeData(String text, bool flash, bool marq, bool isInverted, - Speed speed, Mode mode) async { - List message = await converters.messageTohex(text, isInverted); + Future getBadgeData( + String text, + bool flash, + bool marq, + bool isInverted, + Speed speed, + Mode mode, + int badgeHeight, + int badgeWidth // <-- add badgeHeight parameter + ) async { + List message = await converters.messageTohex( + text, + isInverted, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ); // <-- pass badgeHeight Data data = Data(messages: [ Message( text: message, @@ -283,12 +331,12 @@ class SavedBadgeProvider extends ChangeNotifier { speed: speed, mode: mode, ) - ]); + ], height: badgeHeight, width: badgeWidth); return data; } - void savedBadgeAnimation( - Map data, AnimationBadgeProvider aniProvider) { + void savedBadgeAnimation(Map data, + AnimationBadgeProvider aniProvider, int badgeHeight) { // Reset animation mode and effects to default to avoid leakage aniProvider.setAnimationMode(animationMap[0]); // Default to left aniProvider.clearAllEffects(); @@ -377,7 +425,7 @@ class SavedBadgeProvider extends ChangeNotifier { data['messages'][0].containsKey('text') && data['messages'][0]['text'] is List) { String hexString = data['messages'][0]['text'].join(); - List> binaryArray = hexStringToBool(hexString); + List> binaryArray = hexStringToBool(hexString, badgeHeight); aniProvider.setNewGrid(binaryArray); } else { logger.w("Missing or invalid text data in badge"); diff --git a/lib/view/draw_badge_screen.dart b/lib/view/draw_badge_screen.dart index 9565336a4..b9648138d 100644 --- a/lib/view/draw_badge_screen.dart +++ b/lib/view/draw_badge_screen.dart @@ -1,3 +1,5 @@ +import 'package:badgemagic/bademagic_module/models/screen_size.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/toast_utils.dart'; @@ -15,6 +17,7 @@ class DrawBadge extends StatefulWidget { final bool? isSavedCard; final bool? isSavedClipart; final List>? badgeGrid; + final ScreenSize selectedSize; const DrawBadge({ super.key, @@ -22,6 +25,7 @@ class DrawBadge extends StatefulWidget { this.isSavedCard = false, this.isSavedClipart = false, this.badgeGrid, + required this.selectedSize, }); @override @@ -29,13 +33,18 @@ class DrawBadge extends StatefulWidget { } class _DrawBadgeState extends State { - var drawToggle = DrawBadgeProvider(); + late DrawBadgeProvider drawToggle; bool _showShapeOptions = false; + final FileHelper fileHelper = FileHelper(); + final l10n = GetIt.instance.get().l10n; @override - void didChangeDependencies() { - super.didChangeDependencies(); - _setLandscapeOrientation(); + void initState() { + super.initState(); + drawToggle = DrawBadgeProvider(); + drawToggle.initGridWithSize(widget.selectedSize); + WidgetsBinding.instance + .addPostFrameCallback((_) => _setLandscapeOrientation()); } @override @@ -44,142 +53,144 @@ class _DrawBadgeState extends State { super.dispose(); } - void _resetPortraitOrientation() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); + Future _resetPortraitOrientation() async { + try { + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + } catch (e) { + logger.e('Error setting portrait orientation', error: e); + } } - void _setLandscapeOrientation() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft, - ]); + Future _setLandscapeOrientation() async { + try { + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeRight, + DeviceOrientation.landscapeLeft, + ]); + } catch (e) { + logger.e('Error setting landscape orientation', error: e); + } + } + + Future _saveImage() async { + try { + List> badgeGrid = drawToggle + .getDrawViewGrid() + .map((row) => row.map((cell) => cell ? 1 : 0).toList()) + .toList(); + List hexString = + Converters.convertBitmapToLEDHex(badgeGrid, false); + + if (widget.isSavedCard!) { + await fileHelper.updateBadgeText(widget.filename!, hexString); + } else if (widget.isSavedClipart!) { + await fileHelper.updateClipart(widget.filename!, badgeGrid); + } else { + await fileHelper.saveImage(drawToggle.getDrawViewGrid()); + } + + await fileHelper.generateClipartCache(); + ToastUtils().showToast(l10n.clipartSavedSuccessfully); + } catch (e) { + logger.e('Error saving image', error: e); + } } @override Widget build(BuildContext context) { - FileHelper fileHelper = FileHelper(); - final l10n = GetIt.instance.get().l10n; - return WillPopScope( onWillPop: () async { - _resetPortraitOrientation(); + await _resetPortraitOrientation(); return true; }, child: CommonScaffold( index: 1, title: l10n.appTitle, body: LayoutBuilder( - builder: (context, constraints) { - return Column( - key: const Key(drawBadgeScreen), - children: [ - const SizedBox(height: 8), + builder: (context, constraints) => Column( + key: const Key(drawBadgeScreen), + children: [ + const SizedBox(height: 8), - // Badge takes most of the available space - Expanded( - flex: 6, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: BMBadge( - providerInit: (provider) => drawToggle = provider, - badgeGrid: widget.badgeGrid - ?.map((e) => e.map((e) => e == 1).toList()) - .toList(), - ), + // Badge Preview + Expanded( + flex: 6, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: BMBadge( + providerInit: (provider) => drawToggle = provider, + badgeGrid: widget.badgeGrid + ?.map((row) => row.map((e) => e == 1).toList()) + .toList(), + selectedSize: widget.selectedSize, ), ), + ), + const SizedBox(height: 8), - const SizedBox(height: 8), - - // Control buttons - compact layout with closer spacing - Expanded( - flex: 2, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: _buildCompactButton( - true, Icons.edit, l10n.draw)), - const SizedBox(width: 2), - Flexible( - child: _buildCompactButton( - false, Icons.delete, l10n.erase)), - const SizedBox(width: 2), - Flexible(child: _buildResetButton()), - const SizedBox(width: 2), - Flexible(child: _buildSaveButton(fileHelper)), - const SizedBox(width: 2), - Flexible(child: _buildShapesToggleButton()), - const SizedBox(width: 2), - Flexible(child: _buildUndoButton()), - const SizedBox(width: 2), - Flexible(child: _buildRedoButton()), - ], - ), - const SizedBox(height: 8), - ], - ), - ), - // Shape options - only show when toggled, fixed height - if (_showShapeOptions) - Container( - height: 60, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( + // Control Buttons + Expanded( + flex: 2, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Semantics( - label: 'Free', - child: _buildCompactShapeCard(context, - DrawShape.freehand, Icons.gesture, l10n.free), - ), + Flexible( + child: _buildCompactButton( + true, Icons.edit, l10n.draw)), const SizedBox(width: 2), - Semantics( - label: 'Square', - child: _buildCompactShapeCard(context, - DrawShape.square, Icons.crop_square, l10n.square), - ), + Flexible( + child: _buildCompactButton( + false, Icons.delete, l10n.erase)), const SizedBox(width: 2), - Semantics( - label: 'Rect', - child: _buildCompactShapeCard( - context, - DrawShape.rectangle, - Icons.rectangle_outlined, - l10n.rectangle), - ), + Flexible(child: _buildResetButton()), const SizedBox(width: 2), - Semantics( - label: 'Circle', - child: _buildCompactShapeCard( - context, - DrawShape.circle, - Icons.circle_outlined, - l10n.circle), - ), + Flexible(child: _buildSaveButton()), const SizedBox(width: 2), - Semantics( - label: 'Triangle', - child: _buildCompactShapeCard( - context, - DrawShape.triangle, - Icons.change_history, - l10n.triangle), - ), + Flexible(child: _buildShapesToggleButton()), + const SizedBox(width: 2), + Flexible(child: _buildUndoButton()), + const SizedBox(width: 2), + Flexible(child: _buildRedoButton()), ], ), - ), + const SizedBox(height: 8), - const SizedBox(height: 8), - ], - ); - }, + // Shape Options + if (_showShapeOptions) + Container( + height: 60, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildCompactShapeCard( + DrawShape.freehand, Icons.gesture, l10n.free), + const SizedBox(width: 2), + _buildCompactShapeCard(DrawShape.square, + Icons.crop_square, l10n.square), + const SizedBox(width: 2), + _buildCompactShapeCard(DrawShape.rectangle, + Icons.rectangle_outlined, l10n.rectangle), + const SizedBox(width: 2), + _buildCompactShapeCard(DrawShape.circle, + Icons.circle_outlined, l10n.circle), + const SizedBox(width: 2), + _buildCompactShapeCard(DrawShape.triangle, + Icons.change_history, l10n.triangle), + ], + ), + ), + ], + ), + ), + ], + ), ), ), ); @@ -187,22 +198,11 @@ class _DrawBadgeState extends State { Widget _buildCompactButton(bool isDraw, IconData icon, String label) { final isSelected = drawToggle.isDrawing == isDraw; - return TextButton( - onPressed: () { - setState(() { - drawToggle.toggleIsDrawing(isDraw); - }); - }, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), - minimumSize: const Size(60, 40), - ), + onPressed: () => setState(() => drawToggle.toggleIsDrawing(isDraw)), child: Column( - mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: isSelected ? colorPrimary : Colors.black, size: 20), - const SizedBox(height: 2), Text(label, style: TextStyle( color: isSelected ? colorPrimary : Colors.black, @@ -212,170 +212,112 @@ class _DrawBadgeState extends State { ); } - Widget _buildResetButton() { - return TextButton( - onPressed: () { - setState(() { - drawToggle.resetDrawViewGrid(); - }); - }, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), - minimumSize: const Size(60, 40), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.refresh, color: Colors.black, size: 20), - const SizedBox(height: 2), - Text(GetIt.instance.get().l10n.reset, - style: const TextStyle(color: Colors.black, fontSize: 10)), - ], - ), - ); - } - - Widget _buildSaveButton(FileHelper fileHelper) { - return TextButton( - onPressed: () async { - List> badgeGrid = drawToggle - .getDrawViewGrid() - .map((e) => e.map((e) => e ? 1 : 0).toList()) - .toList(); - List hexString = - Converters.convertBitmapToLEDHex(badgeGrid, false); - - if (widget.isSavedCard!) { - await fileHelper.updateBadgeText(widget.filename!, hexString); - } else if (widget.isSavedClipart!) { - await fileHelper.updateClipart(widget.filename!, badgeGrid); - } else { - await fileHelper.saveImage(drawToggle.getDrawViewGrid()); - } - - fileHelper.generateClipartCache(); - ToastUtils().showToast(GetIt.instance - .get() - .l10n - .clipartSavedSuccessfully); - - if (mounted) { - Navigator.of(context).popUntil((route) => route.isFirst); - } - }, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), - minimumSize: const Size(60, 40), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.save, color: Colors.black, size: 20), - const SizedBox(height: 2), - Text(GetIt.instance.get().l10n.save, - style: const TextStyle(color: Colors.black, fontSize: 10)), - ], - ), - ); - } - - Widget _buildShapesToggleButton() { - return TextButton( - onPressed: () { - setState(() { - _showShapeOptions = !_showShapeOptions; - - // Reset to Freehand when hiding shape options - if (!_showShapeOptions) { - drawToggle.setShape(DrawShape.freehand); - } - }); - }, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), - minimumSize: const Size(60, 40), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.category, - color: _showShapeOptions ? colorPrimary : Colors.black, size: 20), - const SizedBox(height: 2), - Text('Shapes', // Using hardcoded string for semantic label - style: TextStyle( - color: _showShapeOptions ? colorPrimary : Colors.black, - fontSize: 10)), - ], - ), - ); - } + Widget _buildResetButton() => TextButton( + onPressed: () => setState(() => drawToggle.resetDrawViewGrid()), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), + minimumSize: const Size(60, 40), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.refresh, color: Colors.black, size: 20), + const SizedBox(height: 2), + Text(l10n.reset, + style: const TextStyle(color: Colors.black, fontSize: 10)), + ], + ), + ); - Widget _buildUndoButton() { - return AnimatedBuilder( - animation: drawToggle, - builder: (context, _) { - final bool canUndo = drawToggle.canUndo; - final Color buttonColor = canUndo ? Colors.black : Colors.grey; + Widget _buildSaveButton() => TextButton( + onPressed: () async { + await _saveImage(); + if (mounted) Navigator.of(context).popUntil((route) => route.isFirst); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), + minimumSize: const Size(60, 40), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.save, color: Colors.black, size: 20), + const SizedBox(height: 2), + Text(l10n.save, + style: const TextStyle(color: Colors.black, fontSize: 10)), + ], + ), + ); - return TextButton( - onPressed: canUndo - ? () { - drawToggle.undo(); - } - : null, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), - minimumSize: const Size(60, 40), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.undo, color: buttonColor, size: 20), - const SizedBox(height: 2), - Text('Undo', style: TextStyle(color: buttonColor, fontSize: 10)), - ], - ), - ); - }, - ); - } + Widget _buildShapesToggleButton() => TextButton( + onPressed: () { + setState(() { + _showShapeOptions = !_showShapeOptions; + if (!_showShapeOptions) drawToggle.setShape(DrawShape.freehand); + }); + }, + child: Column( + children: [ + Icon(Icons.category, + color: _showShapeOptions ? colorPrimary : Colors.black, + size: 20), + const SizedBox(height: 2), + Text('Shapes', + style: TextStyle( + color: _showShapeOptions ? colorPrimary : Colors.black, + fontSize: 10)), + ], + ), + ); - Widget _buildRedoButton() { - return AnimatedBuilder( - animation: drawToggle, - builder: (context, _) { - final bool canRedo = drawToggle.canRedo; - final Color buttonColor = canRedo ? Colors.black : Colors.grey; + Widget _buildUndoButton() => AnimatedBuilder( + animation: drawToggle, + builder: (context, _) { + final canUndo = drawToggle.canUndo; + return TextButton( + onPressed: canUndo ? drawToggle.undo : null, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.undo, + color: canUndo ? Colors.black : Colors.grey, size: 20), + const SizedBox(height: 2), + Text('Undo', + style: TextStyle( + color: canUndo ? Colors.black : Colors.grey, + fontSize: 10)), + ], + ), + ); + }, + ); - return TextButton( - onPressed: canRedo ? drawToggle.redo : null, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), - minimumSize: const Size(60, 40), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.redo, color: buttonColor, size: 20), - const SizedBox(height: 2), - Text('Redo', style: TextStyle(color: buttonColor, fontSize: 10)), - ], - ), - ); - }, - ); - } + Widget _buildRedoButton() => AnimatedBuilder( + animation: drawToggle, + builder: (context, _) { + final canRedo = drawToggle.canRedo; + return TextButton( + onPressed: canRedo ? drawToggle.redo : null, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.redo, + color: canRedo ? Colors.black : Colors.grey, size: 20), + const SizedBox(height: 2), + Text('Redo', + style: TextStyle( + color: canRedo ? Colors.black : Colors.grey, + fontSize: 10)), + ], + ), + ); + }, + ); - Widget _buildCompactShapeCard( - BuildContext context, DrawShape shape, IconData icon, String label) { + Widget _buildCompactShapeCard(DrawShape shape, IconData icon, String label) { final isSelected = drawToggle.selectedShape == shape; - return ElevatedButton( - onPressed: () { - setState(() { - drawToggle.setShape(shape); - }); - }, + onPressed: () => setState(() => drawToggle.setShape(shape)), style: ElevatedButton.styleFrom( foregroundColor: isSelected ? Colors.white : Colors.black, backgroundColor: isSelected ? colorPrimary : Colors.white, @@ -387,10 +329,8 @@ class _DrawBadgeState extends State { minimumSize: const Size(55, 40), ), child: Column( - mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 18), - const SizedBox(height: 2), Text(label, style: const TextStyle(fontSize: 9), overflow: TextOverflow.ellipsis), diff --git a/lib/view/homescreen.dart b/lib/view/homescreen.dart index fe461f1a1..ab0c0c793 100644 --- a/lib/view/homescreen.dart +++ b/lib/view/homescreen.dart @@ -5,20 +5,16 @@ 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/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/bademagic_module/models/speed.dart'; import 'package:badgemagic/constants.dart'; -import 'package:badgemagic/main.dart'; import 'package:badgemagic/providers/animation_badge_provider.dart'; import 'package:badgemagic/providers/badge_message_provider.dart' hide modeValueMap, speedMap; -import 'package:badgemagic/providers/font_provider.dart'; 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/services/localization_service.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'; @@ -33,10 +29,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get_it/get_it.dart'; import 'package:provider/provider.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:badgemagic/providers/font_provider.dart'; class HomeScreen extends StatefulWidget { - // Add parameters for saved badge data when editing final String? savedBadgeFilename; final int? initialSpeed; @@ -56,30 +53,28 @@ class _HomeScreenState extends State AutomaticKeepAliveClientMixin, WidgetsBindingObserver { late final TabController _tabController; - final AnimationBadgeProvider animationProvider = AnimationBadgeProvider(); + AnimationBadgeProvider animationProvider = AnimationBadgeProvider(); late SpeedDialProvider speedDialProvider; - final BadgeMessageProvider badgeData = BadgeMessageProvider(); - final ImageUtils imageUtils = ImageUtils(); - final InlineImageProvider inlineImageProvider = + 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 isPrefixIconClicked = false; bool isDialInteracting = false; - String previousText = ''; - String _cachedText = ''; String errorVal = ""; + late ScreenSize _selectedSize; @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); inlineimagecontroller.addListener(handleTextChange); _setPortraitOrientation(); speedDialProvider = SpeedDialProvider(animationProvider); - // If initialSpeed is provided, set it immediately if (widget.initialSpeed != null) { speedDialProvider.setDialValue(widget.initialSpeed!); } @@ -87,47 +82,51 @@ class _HomeScreenState extends State WidgetsBinding.instance.addPostFrameCallback((_) async { inlineImageProvider.setContext(context); - // Apply saved badge data if we're editing a saved badge if (widget.savedBadgeFilename != null) { await _loadBadgeDataFromDisk(widget.savedBadgeFilename!); } }); + _startImageCaching(); _tabController = TabController(length: 4, vsync: this); + _selectedSize = supportedScreenSizes.first; } - // Loads badge data from disk and populates controllers/providers for editing Future _loadBadgeDataFromDisk(String badgeFilename) async { try { final (badgeText, badgeData, savedData) = await BadgeLoaderHelper.loadBadgeDataAndText(badgeFilename); - // Set the text in the controller - inlineimagecontroller.text = badgeText; + if (savedData != null && + savedData.containsKey('height') && + savedData.containsKey('width')) { + final height = savedData['height'] as int?; + final width = savedData['width'] as int?; + if (height != null && width != null) { + final matchedSize = supportedScreenSizes.firstWhere( + (size) => size.height == height && size.width == width, + orElse: () => _selectedSize); + setState(() { + _selectedSize = matchedSize; + }); + } + } - // Set animation effects animationProvider.removeEffect(effectMap[0]); // Invert animationProvider.removeEffect(effectMap[1]); // Flash animationProvider.removeEffect(effectMap[2]); // Marquee - final message = badgeData.messages[0]; - if (message.flash) { - animationProvider.addEffect(effectMap[1]); - } - if (message.marquee) { - animationProvider.addEffect(effectMap[2]); - } + if (message.flash) animationProvider.addEffect(effectMap[1]); + if (message.marquee) animationProvider.addEffect(effectMap[2]); if (savedData != null && savedData.containsKey('invert') && savedData['invert'] == true) { animationProvider.addEffect(effectMap[0]); } - // Set animation mode int modeValue = BadgeLoaderHelper.parseAnimationMode(message.mode); animationProvider.setAnimationMode(animationMap[modeValue]); - // Set speed try { int speedDialValue = Speed.getIntValue(message.speed); speedDialProvider.setDialValue(speedDialValue); @@ -135,6 +134,7 @@ class _HomeScreenState extends State speedDialProvider.setDialValue(1); } + inlineimagecontroller.text = badgeText; ToastUtils().showToast( "Editing badge: ${badgeFilename.substring(0, badgeFilename.length - 5)}"); } catch (e) { @@ -159,6 +159,15 @@ class _HomeScreenState extends State } } + @override + void dispose() { + inlineimagecontroller.removeListener(handleTextChange); + animationProvider.stopAnimation(); + WidgetsBinding.instance.removeObserver(this); + _tabController.dispose(); + super.dispose(); + } + TextStyle _getFontStyle(String fontName) { const baseStyle = TextStyle(fontSize: 12); switch (fontName) { @@ -188,444 +197,382 @@ class _HomeScreenState extends State } } - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - inlineimagecontroller.removeListener(handleTextChange); - inlineimagecontroller.removeListener(_controllerListner); - animationProvider.stopAnimation(); - _tabController.dispose(); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(state); - if (state == AppLifecycleState.resumed) { - if (inlineimagecontroller.text.trim().isEmpty && - _cachedText.trim().isNotEmpty) { - inlineimagecontroller.text = _cachedText; - } - animationProvider.badgeAnimation( - inlineimagecontroller.text, - Converters(), - animationProvider.isEffectActive(InvertLEDEffect()), - ); - if (mounted) setState(() {}); - } else if (state == AppLifecycleState.paused) { - _cachedText = inlineimagecontroller.text; - animationProvider.stopAnimation(); - } else if (state == AppLifecycleState.inactive) { - animationProvider.stopAnimation(); - } - } - @override Widget build(BuildContext context) { super.build(context); InlineImageProvider inlineImageProvider = Provider.of(context); - return ValueListenableBuilder( - valueListenable: appLocale, - builder: (context, _, __) { - final l10n = GetIt.instance.get().l10n; - return MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (context) => animationProvider, - ), - ChangeNotifierProvider( - create: (context) { - inlineImageProvider - .getController() - .addListener(_controllerListner); - return speedDialProvider; - }, - ), - ], - child: DefaultTabController( - length: 4, - child: CommonScaffold( - index: 0, - title: l10n.appTitle, - body: SafeArea( - child: Stack( - children: [ - // Scrollable content - SingleChildScrollView( - physics: isDialInteracting - ? const NeverScrollableScrollPhysics() - : const AlwaysScrollableScrollPhysics(), - child: Column( - mainAxisSize: MainAxisSize.min, + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => animationProvider, + ), + ChangeNotifierProvider( + create: (context) { + inlineImageProvider.getController().addListener(_controllerListner); + return speedDialProvider; + }, + ), + ChangeNotifierProvider( + create: (context) => FontProvider(), + ), + ], + child: DefaultTabController( + length: 4, + child: CommonScaffold( + index: 0, + title: 'Badge Magic', + body: SafeArea( + child: SingleChildScrollView( + physics: isDialInteracting + ? const NeverScrollableScrollPhysics() + : const AlwaysScrollableScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ - AnimationBadge(), - Container( - margin: EdgeInsets.all(15.w), + AnimationBadge(selectedSize: _selectedSize), + Transform.translate( + offset: + Offset(-11, -6), // Move up to overlap slightly child: Material( - color: drawerHeaderTitle, - borderRadius: BorderRadius.circular(10.r), - elevation: 4, - child: ExtendedTextField( - onChanged: (value) {}, - controller: inlineimagecontroller, - specialTextSpanBuilder: ImageBuilder(), - style: Provider.of(context) - .selectedFont != - null - ? _getFontStyle( - Provider.of(context) - .selectedFont!) - .copyWith(fontSize: 14) - : const TextStyle(fontSize: 14), - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.r), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.r), - borderSide: BorderSide(color: colorPrimary), - ), - prefixIcon: IconButton( - onPressed: () { - setState(() { - isPrefixIconClicked = - !isPrefixIconClicked; - }); - }, - icon: const Icon(Icons.tag_faces_outlined), - ), - suffixIcon: Padding( - padding: EdgeInsets.only(right: 8.w), - child: Consumer( - builder: (context, fontProvider, _) { - return DropdownButtonHideUnderline( - child: DropdownButton( - value: fontProvider.selectedFont, - icon: const SizedBox.shrink(), - iconEnabledColor: mdGrey400, - style: TextStyle( - color: mdGrey400, - fontSize: 12.sp, - ), - hint: Text( - 'Font', - style: TextStyle( - fontSize: 12.sp, - color: mdGrey400, - ), - ), - items: [ - DropdownMenuItem( - value: null, - child: Text( - 'Default', - style: const TextStyle( - fontSize: 12, - ).copyWith( - color: Colors.black, - ), - ), - ), - ...fontProvider.availableFonts - .map((font) => - DropdownMenuItem( - value: font, - child: Text( - font, - style: _getFontStyle( - font, - ).copyWith( - color: Colors.black, - ), - ), - )) - ], - selectedItemBuilder: (context) { - final List options = [ - null, - ...fontProvider.availableFonts, - ]; - return options.map((opt) { - final String label = - opt ?? 'Default'; - return Row( - mainAxisSize: - MainAxisSize.min, - children: [ - Text( - label, - style: TextStyle( - color: mdGrey400, - fontSize: 12.sp, - ), - overflow: - TextOverflow.ellipsis, - ), - const Icon( - Icons.arrow_drop_down, - size: 16, - color: mdGrey400, - ), - ], - ); - }).toList(); - }, - onChanged: (String? newFont) { - fontProvider.changeFont(newFont); - animationProvider.badgeAnimation( - inlineimagecontroller.text, - Converters(), - animationProvider - .isEffectActive( - InvertLEDEffect()), - ); - }, - borderRadius: - BorderRadius.circular(8.r), - elevation: 2, - isDense: true, - ), - ); - }, - ), + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(5.r), + child: PopupMenuButton( + key: ValueKey(_selectedSize), + tooltip: "Select Screen Size", + initialValue: _selectedSize, + onSelected: (newSize) { + setState(() { + _selectedSize = newSize; + animationProvider.initGrids(_selectedSize); + animationProvider.badgeAnimation( + inlineImageProvider.getController().text, + Converters(), + animationProvider + .isEffectActive(InvertLEDEffect()), + _selectedSize, + ); + }); + }, + itemBuilder: (context) { + return supportedScreenSizes.map((size) { + return PopupMenuItem( + value: size, + child: Text(size.name, + style: const TextStyle(fontSize: 13)), + ); + }).toList(); + }, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 6.w, vertical: 3.h), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.aspect_ratio, + size: 16, color: Colors.black54), + SizedBox(width: 4.w), + Text( + _selectedSize.name, + style: const TextStyle( + fontSize: 10, + color: Colors.black87), + ), + ], ), ), ), ), ), - 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( - isScrollable: false, - indicatorSize: TabBarIndicatorSize.tab, - labelStyle: TextStyle( - fontSize: 12, fontWeight: FontWeight.w600), - unselectedLabelStyle: TextStyle( - fontSize: 12, fontWeight: FontWeight.w600), - labelColor: const Color.fromARGB(255, 12, 12, 12), - unselectedLabelColor: - const Color.fromARGB(255, 146, 121, 121), - indicatorColor: colorPrimary, - controller: _tabController, - splashFactory: InkRipple.splashFactory, - overlayColor: - MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.pressed) - ? dividerColor - : null, - ), - tabs: [ - Tab( - key: const ValueKey('tab_speed'), - text: l10n.speedTitle), - Tab( - key: const ValueKey('tab_transition'), - text: l10n.transitionTitle), - Tab( - key: const ValueKey('tab_effects'), - text: l10n.effectsTitle), - Tab( - key: const ValueKey('tab_animation'), - text: l10n.animation), - ], - ), - SizedBox( - height: 350.h, - child: TabBarView( - physics: const NeverScrollableScrollPhysics(), - controller: _tabController, - children: [ - GestureDetector( - onPanDown: (_) => - setState(() => isDialInteracting = true), - onPanCancel: () => - setState(() => isDialInteracting = false), - onPanEnd: (_) => - setState(() => isDialInteracting = false), - child: RadialDial(), + ], + ), + ], + ), + Container( + margin: + EdgeInsets.symmetric(horizontal: 15.w, vertical: 0.h), + child: Material( + color: drawerHeaderTitle, + borderRadius: BorderRadius.circular(10.r), + elevation: 4, + child: Consumer2( + builder: (context, fontProvider, animationProvider, _) { + return ExtendedTextField( + onChanged: (value) {}, + controller: inlineimagecontroller, + specialTextSpanBuilder: ImageBuilder(), + style: fontProvider.selectedFont != null + ? _getFontStyle(fontProvider.selectedFont!) + .copyWith(fontSize: 14) + : const TextStyle(fontSize: 14), + 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: + const BorderRadius.all(Radius.circular(10)), + borderSide: BorderSide(color: colorPrimary), + ), + suffixIcon: Padding( + padding: EdgeInsets.only(right: 8.w), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: fontProvider.selectedFont, + icon: const Icon(Icons.arrow_drop_down), + hint: Text( + 'Font', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text( + 'Default Font', + style: TextStyle(fontSize: 12), + ), + ), + ...fontProvider.availableFonts.map( + (font) => DropdownMenuItem( + value: font, + child: Text( + font, + style: _getFontStyle(font), + ), + ), + ), + ], + onChanged: (String? newFont) { + fontProvider.changeFont(newFont); + animationProvider.badgeAnimation( + inlineimagecontroller.text, + Converters(), + animationProvider + .isEffectActive(InvertLEDEffect()), + _selectedSize, + ); + }, + borderRadius: BorderRadius.circular(8.r), + elevation: 2, + isDense: true, + ), ), - const TransitionTab(), - const EffectTab(), - const AnimationTab(), - ], + ), ), - ), - - // Add a spacer so last content isn't hidden behind the floating buttons - SizedBox( - height: MediaQuery.of(context).padding.bottom + - 110.h), - ], + ); + }, ), ), - - // Floating bottom buttons (overlay) so they don't push or block content - Positioned( - left: 16.w, - right: 16.w, - bottom: 16.h, - child: Consumer( + ), + 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( + isScrollable: false, + indicatorSize: TabBarIndicatorSize.tab, + labelStyle: TextStyle(fontSize: 12), + unselectedLabelStyle: TextStyle(fontSize: 12), + labelColor: Colors.black, + unselectedLabelColor: mdGrey400, + indicatorColor: colorPrimary, + controller: _tabController, + splashFactory: InkRipple.splashFactory, + overlayColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.pressed) + ? dividerColor + : null, + ), + tabs: const [ + Tab(text: 'Speed'), + Tab(text: 'Animation'), + Tab(text: 'Transition'), + Tab(text: 'Effects'), + ], + ), + SizedBox( + height: 350.h, + child: TabBarView( + physics: const NeverScrollableScrollPhysics(), + controller: _tabController, + children: [ + GestureDetector( + onPanDown: (_) => + setState(() => isDialInteracting = true), + onPanCancel: () => + setState(() => isDialInteracting = false), + onPanEnd: (_) => + setState(() => isDialInteracting = false), + child: RadialDial(), + ), + TransitionTab(selectedSize: _selectedSize), + AnimationTab(selectedSize: _selectedSize), + EffectTab(selectedSize: _selectedSize), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Consumer( builder: (context, animationProvider, _) { final isSpecial = animationProvider.isSpecialAnimationSelected(); if (isSpecial) { - // Only Transfer button (for special animations) - return SizedBox( - height: 32.h, - child: GestureDetector( - onTap: () async { - await animationProvider - .handleAnimationTransfer( - badgeData: badgeData, - inlineImageProvider: inlineImageProvider, - speedDialProvider: speedDialProvider, - flash: animationProvider - .isEffectActive(FlashEffect()), - marquee: animationProvider - .isEffectActive(MarqueeEffect()), - invert: animationProvider - .isEffectActive(InvertLEDEffect()), - context: context, - ); - }, - child: Container( - alignment: Alignment.center, - padding: EdgeInsets.symmetric( - horizontal: 16.w, vertical: 8.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - color: mdGrey400, - ), - child: Text(l10n.transferButton), + return GestureDetector( + onTap: () async { + await animationProvider.handleAnimationTransfer( + badgeData: badgeData, + inlineImageProvider: inlineImageProvider, + speedDialProvider: speedDialProvider, + flash: animationProvider + .isEffectActive(FlashEffect()), + marquee: animationProvider + .isEffectActive(MarqueeEffect()), + invert: animationProvider + .isEffectActive(InvertLEDEffect()), + badgeHeight: _selectedSize.height, + badgeWidth: _selectedSize.width, + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 33.w, vertical: 8.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + color: mdGrey400, ), + child: const Text('Transfer'), ), ); } else { - // Save + Transfer buttons (side by side, expanded) return Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: GestureDetector( - onTap: () async { - if (inlineimagecontroller.text - .trim() - .isEmpty) { - ToastUtils().showToast( - "Please enter a message"); - return; + GestureDetector( + onTap: () async { + if (inlineimagecontroller.text + .trim() + .isEmpty) { + ToastUtils() + .showToast("Please enter a message"); + return; + } + + if (widget.savedBadgeFilename != null) { + SavedBadgeProvider savedBadgeProvider = + SavedBadgeProvider(); + String baseFilename = + widget.savedBadgeFilename!; + if (baseFilename.endsWith('.json')) { + baseFilename = baseFilename.substring( + 0, baseFilename.length - 5); } - if (widget.savedBadgeFilename != null) { - // Update existing badge - SavedBadgeProvider savedBadgeProvider = - SavedBadgeProvider(); - String baseFilename = - widget.savedBadgeFilename!; - if (baseFilename.endsWith('.json')) { - baseFilename = baseFilename.substring( - 0, baseFilename.length - 5); - } - - await savedBadgeProvider - .updateBadgeData( - baseFilename, - inlineimagecontroller.text, - animationProvider - .isEffectActive(FlashEffect()), - animationProvider - .isEffectActive(MarqueeEffect()), - animationProvider.isEffectActive( - InvertLEDEffect()), - speedDialProvider.getOuterValue(), - animationProvider - .getAnimationIndex() ?? - 1, - ); - - ToastUtils().showToast( - "Badge Updated Successfully"); - Navigator.pushNamedAndRemoveUntil( - context, - '/savedBadge', - (route) => false, - ); - } else { - // Save new badge dialog - showDialog( - context: context, - builder: (context) { - return SaveBadgeDialog( - speed: speedDialProvider, - animationProvider: - animationProvider, - textController: - inlineimagecontroller, - isInverse: animationProvider - .isEffectActive( - InvertLEDEffect()), - ); - }, - ); - } - }, - child: Container( - height: 32.h, - alignment: Alignment.center, - padding: EdgeInsets.symmetric( - horizontal: 16.w, vertical: 8.h), - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(8.r), - color: mdGrey400, - ), - child: Text(l10n.saveButton), - ), - ), - ), - SizedBox(width: 12.w), - Expanded( - child: GestureDetector( - onTap: () async { - await animationProvider - .handleAnimationTransfer( - badgeData: badgeData, - inlineImageProvider: - inlineImageProvider, - speedDialProvider: speedDialProvider, - flash: animationProvider + await savedBadgeProvider.updateBadgeData( + baseFilename, + inlineimagecontroller.text, + animationProvider .isEffectActive(FlashEffect()), - marquee: animationProvider + animationProvider .isEffectActive(MarqueeEffect()), - invert: animationProvider + animationProvider .isEffectActive(InvertLEDEffect()), + speedDialProvider.getOuterValue(), + animationProvider.getAnimationIndex() ?? + 1, + _selectedSize.height, + _selectedSize.width, + ); + + ToastUtils().showToast( + "Badge Updated Successfully"); + Navigator.pushNamedAndRemoveUntil(context, + '/savedBadge', (route) => false); + } else { + showDialog( context: context, + builder: (context) { + return SaveBadgeDialog( + speed: speedDialProvider, + animationProvider: + animationProvider, + textController: + inlineimagecontroller, + isInverse: animationProvider + .isEffectActive( + InvertLEDEffect()), + selectedSize: _selectedSize, + ); + }, ); - }, - child: Container( - height: 32.h, - alignment: Alignment.center, - padding: EdgeInsets.symmetric( - horizontal: 16.w, vertical: 8.h), - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(8.r), - color: mdGrey400, - ), - child: Text(l10n.transferButton), + } + }, + 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: 40.w), + GestureDetector( + onTap: () async { + await animationProvider + .handleAnimationTransfer( + badgeData: badgeData, + inlineImageProvider: inlineImageProvider, + speedDialProvider: speedDialProvider, + flash: animationProvider + .isEffectActive(FlashEffect()), + marquee: animationProvider + .isEffectActive(MarqueeEffect()), + invert: animationProvider + .isEffectActive(InvertLEDEffect()), + badgeHeight: _selectedSize.height, + badgeWidth: _selectedSize.width, + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 33.w, vertical: 8.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + color: mdGrey400, + ), + child: const Text('Transfer'), ), ), ], @@ -633,15 +580,15 @@ class _HomeScreenState extends State } }, ), - ), - ], - ), + ], + ), + ], ), - scaffoldKey: const Key(homeScreenTitleKey), ), ), - ); - }, + scaffoldKey: const Key(homeScreenTitleKey), + ), + ), ); } @@ -649,13 +596,12 @@ class _HomeScreenState extends State final currentText = inlineimagecontroller.text; final selection = inlineimagecontroller.selection; - // Always reset to text animation if a special animation is selected and user types if (animationProvider.isSpecialAnimationSelected() && currentText.isNotEmpty) { animationProvider.resetToTextAnimation(); animationProvider.badgeAnimation(currentText, Converters(), - animationProvider.isEffectActive(InvertLEDEffect())); - setState(() {}); // Ensure UI updates + animationProvider.isEffectActive(InvertLEDEffect()), _selectedSize); + setState(() {}); } if (previousText.length > currentText.length) { @@ -684,12 +630,27 @@ class _HomeScreenState extends State void _controllerListner() { animationProvider.badgeAnimation( - inlineImageProvider.getController().text, - Converters(), - animationProvider.isEffectActive(InvertLEDEffect()), - ); + inlineImageProvider.getController().text, + Converters(), + animationProvider.isEffectActive(InvertLEDEffect()), + _selectedSize); } @override bool get wantKeepAlive => true; + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + inlineimagecontroller.clear(); + previousText = ''; + animationProvider.stopAllAnimations(); + animationProvider.initializeAnimation(); + if (mounted) setState(() {}); + } else if (state == AppLifecycleState.paused || + state == AppLifecycleState.inactive) { + animationProvider.stopAnimation(); + } + } } diff --git a/lib/view/save_badge_screen.dart b/lib/view/save_badge_screen.dart index 6c210cc3c..52d1156fe 100644 --- a/lib/view/save_badge_screen.dart +++ b/lib/view/save_badge_screen.dart @@ -1,18 +1,18 @@ import 'package:badgemagic/bademagic_module/models/data.dart'; import 'package:badgemagic/bademagic_module/models/messages.dart'; -import 'package:badgemagic/bademagic_module/utils/byte_array_utils.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/bademagic_module/utils/converters.dart'; import 'package:badgemagic/bademagic_module/utils/file_helper.dart'; import 'package:badgemagic/bademagic_module/utils/toast_utils.dart'; import 'package:badgemagic/badge_animation/ani_animation.dart'; import 'package:badgemagic/badge_animation/ani_fixed.dart'; import 'package:badgemagic/constants.dart'; -import 'package:badgemagic/services/localization_service.dart'; 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/imageprovider.dart'; import 'package:badgemagic/providers/saved_badge_provider.dart'; +import 'package:badgemagic/services/localization_service.dart'; import 'package:badgemagic/view/widgets/common_scaffold_widget.dart'; import 'package:badgemagic/view/widgets/saved_badge_listview.dart'; import 'package:badgemagic/virtualbadge/view/animated_badge.dart'; @@ -31,17 +31,25 @@ class SaveBadgeScreen extends StatefulWidget { } class _SaveBadgeScreenState extends State { - List>> badgeData = []; + late ScreenSize _previewSize; InlineImageProvider imageProvider = GetIt.instance(); ToastUtils toastUtils = ToastUtils(); FileHelper fileHelper = FileHelper(); SavedBadgeProvider savedBadgeProvider = SavedBadgeProvider(); - AnimationBadgeProvider animationBadgeProvider = AnimationBadgeProvider(); + late AnimationBadgeProvider animationBadgeProvider; @override void initState() { - _setOrientation(); super.initState(); + _setOrientation(); + _previewSize = supportedScreenSizes.first; + animationBadgeProvider = AnimationBadgeProvider(); + } + + @override + void dispose() { + animationBadgeProvider.stopAnimation(); + super.dispose(); } void _setOrientation() { @@ -51,26 +59,27 @@ class _SaveBadgeScreenState extends State { ]); } - @override - void dispose() { - animationBadgeProvider.stopAnimation(); - super.dispose(); + void _updatePreviewSize(ScreenSize size) { + setState(() { + _previewSize = size; + }); } @override Widget build(BuildContext context) { final l10n = GetIt.instance.get().l10n; BadgeMessageProvider badgeMessageProvider = BadgeMessageProvider(); + return MultiProvider( providers: [ ChangeNotifierProvider.value( value: savedBadgeProvider, ), - ChangeNotifierProvider( - create: (context) => animationBadgeProvider, + ChangeNotifierProvider.value( + value: animationBadgeProvider, ), ChangeNotifierProvider( - create: (context) => BadgeSlotProvider(), + create: (_) => BadgeSlotProvider(), ), ], child: CommonScaffold( @@ -81,7 +90,6 @@ class _SaveBadgeScreenState extends State { onPressed: () async { final value = await fileHelper.importBadgeData(context); if (value) { - logger.d('value: $value'); toastUtils.showToast(l10n.badgeImportedSuccessfully); await fileHelper.getBadgeDataFiles(); setState(() {}); @@ -95,7 +103,7 @@ class _SaveBadgeScreenState extends State { Consumer( builder: (context, selectionProvider, _) { if (selectionProvider.selectedBadges.isEmpty) { - return SizedBox.shrink(); + return const SizedBox.shrink(); } return IconButton( icon: const Icon(Icons.delete, color: Colors.red), @@ -122,8 +130,7 @@ class _SaveBadgeScreenState extends State { ), ); if (confirm == true) { - final provider = Provider.of(context, - listen: false); + final provider = context.read(); final selectedBadges = selectionProvider.selectedBadges.toList(); for (final badgeKey in selectedBadges) { @@ -154,9 +161,7 @@ class _SaveBadgeScreenState extends State { height: 200.h, ), ), - SizedBox( - height: 20.h, - ), + SizedBox(height: 20.h), Text( 'No saved badges !', style: TextStyle( @@ -180,23 +185,25 @@ class _SaveBadgeScreenState extends State { children: [ Column( children: [ - AnimationBadge(), + AnimationBadge(selectedSize: _previewSize), Expanded( child: Selector( - selector: (context, selectionProvider) => - selectionProvider.selectedBadges.isNotEmpty, - builder: (context, isTransferEnabled, _) { - return BadgeListView( - isTransferEnabled: isTransferEnabled, - futureBadges: - Future.value(provider.savedBadgeCache), - refreshBadgesCallback: (value) { - provider.savedBadgeCache.remove(value); - setState(() {}); - return Future.value(); - }, - ); - }), + selector: (context, selectionProvider) => + selectionProvider.selectedBadges.isNotEmpty, + builder: (context, isTransferEnabled, _) { + return BadgeListView( + isTransferEnabled: isTransferEnabled, + futureBadges: + Future.value(provider.savedBadgeCache), + refreshBadgesCallback: (value) { + provider.savedBadgeCache.remove(value); + setState(() {}); + return Future.value(); + }, + onPreviewSizeChanged: _updatePreviewSize, + ); + }, + ), ), ], ), @@ -234,6 +241,7 @@ class _SaveBadgeScreenState extends State { while (badgeDataList.length < 8) { badgeDataList.add(Message(text: [])); } + if (badgeDataList .where( (msg) => msg.text.isNotEmpty) @@ -245,26 +253,32 @@ class _SaveBadgeScreenState extends State { animationBadgeProvider .setAnimationMode(FixedAnimation()); } + final fullText = badgeDataList .map((m) => m.text.join()) .join(" "); + animationBadgeProvider.badgeAnimation( fullText, Converters(), false, + _previewSize, ); + final data = Data(messages: badgeDataList); badgeMessageProvider.checkAndTransfer( - null, - null, - null, - null, - null, - null, - data.toJson(), - true, - context); + null, + null, + null, + null, + null, + null, + data.toJson(), + true, + _previewSize.height, + _previewSize.width, + ); } : null, style: ElevatedButton.styleFrom( diff --git a/lib/view/saved_clipart.dart b/lib/view/saved_clipart.dart index 551c27070..3c2d7c44e 100644 --- a/lib/view/saved_clipart.dart +++ b/lib/view/saved_clipart.dart @@ -1,3 +1,4 @@ +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/bademagic_module/utils/byte_array_utils.dart'; import 'package:badgemagic/bademagic_module/utils/file_helper.dart'; import 'package:badgemagic/constants.dart'; @@ -21,11 +22,13 @@ class SavedClipart extends StatefulWidget { class _SavedClipartState extends State { InlineImageProvider imageprovider = GetIt.instance(); FileHelper file = FileHelper(); + late final ScreenSize selectedSize; @override void initState() { _setOrientation(); super.initState(); + selectedSize = supportedScreenSizes.first; } void _setOrientation() { @@ -80,7 +83,8 @@ class _SavedClipartState extends State { imageprovider.removeFromCache(fileName); imageprovider.generateImageCache(); }, - ), // Use the separate ListView widget here + selectedSize: selectedSize, + ), ); } } diff --git a/lib/view/widgets/animation_container.dart b/lib/view/widgets/animation_container.dart index 6ea40f125..fd008f963 100644 --- a/lib/view/widgets/animation_container.dart +++ b/lib/view/widgets/animation_container.dart @@ -10,11 +10,14 @@ import 'package:badgemagic/providers/imageprovider.dart'; import 'package:badgemagic/view/widgets/special_animation_dialog.dart'; import 'package:badgemagic/bademagic_module/utils/converters.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; + class AniContainer extends StatefulWidget { final String? animation; final String animationName; final int index; final IconData? icon; + final ScreenSize screenSize; const AniContainer({ super.key, @@ -22,6 +25,7 @@ class AniContainer extends StatefulWidget { required this.animationName, required this.index, this.icon, + required this.screenSize, }); @override @@ -84,7 +88,8 @@ class _AniContainerState extends State { textController.clear(); animationCardState.setAnimationMode(badgeAnimation); // Force preview update for special animations - animationCardState.badgeAnimation('', Converters(), false); + animationCardState.badgeAnimation( + '', Converters(), false, widget.screenSize); } return; } diff --git a/lib/view/widgets/clipart_list_view.dart b/lib/view/widgets/clipart_list_view.dart index 34bdaab16..ab5d3b210 100644 --- a/lib/view/widgets/clipart_list_view.dart +++ b/lib/view/widgets/clipart_list_view.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/bademagic_module/utils/file_helper.dart'; import 'package:badgemagic/bademagic_module/utils/image_utils.dart'; import 'package:badgemagic/view/draw_badge_screen.dart'; @@ -11,6 +12,7 @@ class SavedClipartListView extends StatelessWidget { final Map>?> images; final FileHelper file = FileHelper(); final ImageUtils imageUtils = ImageUtils(); + final ScreenSize selectedSize; // <-- Add this parameter! final void Function(String) refreshClipartCallback; // Pass the filename @@ -18,6 +20,7 @@ class SavedClipartListView extends StatelessWidget { super.key, required this.images, required this.refreshClipartCallback, + required this.selectedSize, }); @override @@ -71,6 +74,7 @@ class SavedClipartListView extends StatelessWidget { filename: fileName, isSavedClipart: true, badgeGrid: images.values.elementAt(index), + selectedSize: selectedSize, ))); }, icon: const Icon(Icons.edit)), diff --git a/lib/view/widgets/effects_container.dart b/lib/view/widgets/effects_container.dart index 1aac65c58..774f898a9 100644 --- a/lib/view/widgets/effects_container.dart +++ b/lib/view/widgets/effects_container.dart @@ -1,3 +1,4 @@ +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/bademagic_module/utils/converters.dart'; import 'package:badgemagic/badge_effect/badgeeffectabstract.dart'; import 'package:badgemagic/badge_effect/invert_led_effect.dart'; @@ -14,12 +15,14 @@ class EffectContainer extends StatefulWidget { final String effect; final String effectName; final int index; - - const EffectContainer( - {super.key, - required this.effect, - required this.effectName, - required this.index}); + final ScreenSize selectedSize; + const EffectContainer({ + super.key, + required this.effect, + required this.effectName, + required this.index, + required this.selectedSize, + }); @override State createState() => _EffectContainerState(); @@ -61,18 +64,26 @@ class _EffectContainerState extends State { width: 110.w, child: GestureDetector( onTap: () { - effectCardState.isEffectActive(badgeEffect) + effectCardState.isEffectActive( + badgeEffect, + ) ? effectCardState.removeEffect(badgeEffect) : effectCardState.addEffect(badgeEffect); + effectCardState.badgeAnimation( imageProvider.getController().text, Converters(), - effectCardState.isEffectActive(InvertLEDEffect()), + effectCardState.isEffectActive( + InvertLEDEffect(), + ), + widget.selectedSize, ); }, child: Card( surfaceTintColor: Colors.white, - color: effectCardState.isEffectActive(badgeEffect) + color: effectCardState.isEffectActive( + badgeEffect, + ) ? colorAccent : drawerHeaderTitle, elevation: 5, diff --git a/lib/view/widgets/homescreentabs.dart b/lib/view/widgets/homescreentabs.dart index fd87a0d78..9889fc5ca 100644 --- a/lib/view/widgets/homescreentabs.dart +++ b/lib/view/widgets/homescreentabs.dart @@ -4,23 +4,18 @@ import 'package:get_it/get_it.dart'; import 'package:badgemagic/view/widgets/animation_container.dart'; import 'package:badgemagic/view/widgets/effects_container.dart'; import 'package:flutter/material.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; -//effects tab to show effects that the user can select class EffectTab extends StatefulWidget { - const EffectTab({ - super.key, - }); + final ScreenSize selectedSize; + + const EffectTab({super.key, required this.selectedSize}); @override State createState() => _EffectsTabState(); } class _EffectsTabState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { final l10n = GetIt.instance.get().l10n; @@ -31,25 +26,29 @@ class _EffectsTabState extends State { effect: effInvert, effectName: l10n.invertEffect, index: 0, + selectedSize: widget.selectedSize, ), EffectContainer( effect: effFlash, effectName: l10n.flashEffect, index: 1, + selectedSize: widget.selectedSize, ), EffectContainer( effect: effMarque, effectName: l10n.marqueeEffect, index: 2, + selectedSize: widget.selectedSize, ), ], ); } } -// Animation tab to show special animations class AnimationTab extends StatefulWidget { - const AnimationTab({super.key}); + final ScreenSize selectedSize; + + const AnimationTab({super.key, required this.selectedSize}); @override State createState() => _AnimationTabState(); @@ -62,92 +61,186 @@ class _AnimationTabState extends State { return SingleChildScrollView( child: Column( children: [ + // Original basic animations Row( children: [ AniContainer( - animation: null, - icon: Icons.sports_esports, // Pacman icon - animationName: l10n.pacman, - index: 9, + animation: aniLeft, + animationName: 'Left', + index: 0, + screenSize: widget.selectedSize, ), AniContainer( - animation: null, - icon: Icons.chevron_left, // Chevron icon - animationName: l10n.chevron, - index: 10, + animation: aniRight, + animationName: 'Right', + index: 1, + screenSize: widget.selectedSize, ), AniContainer( - animation: null, - icon: Icons.diamond, // Diamond icon - animationName: l10n.diamond, - index: 11, + animation: aniUp, + animationName: 'Up', + index: 2, + screenSize: widget.selectedSize, ), ], ), Row( children: [ AniContainer( - animation: null, - icon: Icons.heart_broken, // Broken Hearts icon - animationName: l10n.brokenHearts, - index: 12, + animation: aniDown, + animationName: 'Down', + index: 3, + screenSize: widget.selectedSize, ), AniContainer( - animation: null, - icon: Icons.favorite_border, // Cupid icon - animationName: l10n.cupid, - index: 13, + animation: aniFixed, + animationName: 'Fixed', + index: 4, + screenSize: widget.selectedSize, ), AniContainer( - animation: null, - icon: Icons.directions_walk, // Feet animation icon - animationName: l10n.feet, - index: 14, + animation: animation, + animationName: 'Animation', + index: 5, + screenSize: widget.selectedSize, ), ], ), Row( children: [ AniContainer( - animation: null, - icon: Icons.set_meal, // Fish icon - animationName: l10n.fishKiss, - index: 15, + animation: aniSnowflake, + animationName: 'Snowflake', + index: 6, + screenSize: widget.selectedSize, ), AniContainer( - animation: null, - icon: Icons.change_history, // V shape icon - animationName: l10n.diagonal, - index: 16, + animation: aniPicture, + animationName: 'Picture', + index: 7, + screenSize: widget.selectedSize, ), AniContainer( - animation: null, - icon: Icons.warning, // Emergency/alert icon - animationName: l10n.emergency, - index: 17, + animation: aniLaser, + animationName: 'Laser', + index: 8, + screenSize: widget.selectedSize, ), ], ), - Row( - children: [ - AniContainer( - animation: null, - icon: Icons.favorite, // Heart icon - animationName: l10n.beatingHearts, - index: 18, - ), - AniContainer( - animation: null, - icon: Icons.celebration, // Fireworks icon - animationName: l10n.fireworks, - index: 19, - ), - AniContainer( - animationName: l10n.equalizer, - index: 20, // This MUST match the index in your animationMap - icon: Icons.equalizer, - ) - ], + + // Extended icon-based animations + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + children: [ + AniContainer( + animation: null, + icon: Icons.sports_esports, + animationName: l10n.pacman, + index: 9, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.chevron_left, + animationName: l10n.chevron, + index: 10, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.diamond, + animationName: l10n.diamond, + index: 11, + screenSize: widget.selectedSize, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + children: [ + AniContainer( + animation: null, + icon: Icons.heart_broken, + animationName: l10n.brokenHearts, + index: 12, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.favorite_border, + animationName: l10n.cupid, + index: 13, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.directions_walk, + animationName: l10n.feet, + index: 14, + screenSize: widget.selectedSize, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + children: [ + AniContainer( + animation: null, + icon: Icons.set_meal, + animationName: l10n.fishKiss, + index: 15, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.change_history, + animationName: l10n.diagonal, + index: 16, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.warning, + animationName: l10n.emergency, + index: 17, + screenSize: widget.selectedSize, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + children: [ + AniContainer( + animation: null, + icon: Icons.favorite, + animationName: l10n.beatingHearts, + index: 18, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.celebration, + animationName: l10n.fireworks, + index: 19, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.equalizer, + animationName: l10n.equalizer, + index: 20, + screenSize: widget.selectedSize, + ), + ], + ), ), ], ), diff --git a/lib/view/widgets/save_badge_card.dart b/lib/view/widgets/save_badge_card.dart index 0f5e4d478..bbff00230 100644 --- a/lib/view/widgets/save_badge_card.dart +++ b/lib/view/widgets/save_badge_card.dart @@ -1,3 +1,4 @@ +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/bademagic_module/models/speed.dart'; import 'package:badgemagic/bademagic_module/utils/byte_array_utils.dart'; import 'package:badgemagic/bademagic_module/utils/converters.dart'; @@ -7,8 +8,9 @@ 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/badge_slot_provider..dart'; +import 'package:badgemagic/providers/imageprovider.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'; @@ -25,6 +27,7 @@ class SaveBadgeCard extends StatelessWidget { final bool isSelected; final VoidCallback? onLongPress; final VoidCallback? onTap; + final void Function(ScreenSize)? onPreviewSizeChanged; SaveBadgeCard({ super.key, @@ -33,283 +36,431 @@ class SaveBadgeCard extends StatelessWidget { this.isSelected = false, this.onLongPress, this.onTap, + this.onPreviewSizeChanged, }); + // Get the screen size from badge data, default to first supported size + ScreenSize getBadgeScreenSize() { + final data = badgeData.value; + if (data.containsKey('height') && data.containsKey('width')) { + final height = data['height'] as int?; + final width = data['width'] as int?; + if (height != null && width != null) { + return supportedScreenSizes.firstWhere( + (size) => size.height == height && size.width == width, + orElse: () => supportedScreenSizes.first); + } + } + return supportedScreenSizes.first; + } + + // 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(); - return Container( - width: 370.w, - padding: EdgeInsets.all(6.dg), - margin: EdgeInsets.all(10.dg), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(6.dg), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: const Offset(0, 3), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Wrapping the text with Flexible to ensure it doesn't overflow. - Flexible( - child: Padding( - padding: EdgeInsets.only( - right: 8 - .w), // Adding some padding to separate text and buttons. - child: Text( - badgeData.key.substring(0, badgeData.key.length - 5), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - softWrap: true, - overflow: TextOverflow - .ellipsis, // Use ellipsis to indicate overflowed text - maxLines: 1, // Limit to 1 line for a cleaner look - ), - ), - ), - Consumer( - builder: (context, provider, widget) => Row( - mainAxisSize: MainAxisSize.min, // Keep the row compact - children: [ - IconButton( - icon: Image.asset( - "assets/icons/t_play.png", - height: 20, - color: Colors.black, - ), - onPressed: () { - provider.savedBadgeAnimation( - badgeData.value, - Provider.of(context, - listen: false)); - }, - ), - IconButton( - icon: const Icon( - Icons.edit, - 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( - MaterialPageRoute( - builder: (context) => DrawBadge( - filename: badgeData.key, - isSavedCard: true, - badgeGrid: data, - ), - ), - ); - }, - ), - IconButton( - icon: Image.asset( - "assets/icons/t_updown.png", - height: 24.h, - color: Colors.black, - ), - onPressed: () { - logger.d("BadgeData: ${badgeData.value}"); - //We can Acrtually call a method to generate the data just by transffering the JSON data - //so we would not necessarily need the Providers. - badge.checkAndTransfer(null, null, null, null, null, - null, badgeData.value, true, context); - }, - ), - IconButton( - icon: const Icon( - Icons.share, - color: Colors.black, - ), - onPressed: () { - file.shareBadgeData(badgeData.key); - }, - ), - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.black, - ), - onPressed: () async { - //add a dialog for confirmation before deleting - await _showDeleteDialog(context).then((value) async { - if (value == true) { - file.deleteFile(badgeData.key); - toastUtils.showToast("Badge Deleted Successfully"); - await refreshBadgesCallback(badgeData); - } - }); - }, - ), - ], - ), + return GestureDetector( + onLongPress: onLongPress, + onTap: onTap, + child: Container( + width: 380.w, + padding: EdgeInsets.all(6.dg), + margin: EdgeInsets.all(10.dg), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6.dg), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), ), ], + border: + isSelected ? Border.all(color: colorPrimary, width: 2) : null, ), - SizedBox(height: 8.h), - Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Visibility( - visible: file.jsonToData(badgeData.value).messages[0].flash, - child: Container( - padding: - EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), - decoration: BoxDecoration( - color: colorPrimary, - borderRadius: BorderRadius.circular(100), - ), - child: Row( - children: [ - Image.asset( - "assets/icons/flash.png", - color: Colors.white, - height: 14.h, - ) - ], + // Wrapping the text with Flexible to ensure it doesn't overflow. + Flexible( + child: Padding( + padding: EdgeInsets.only( + right: 8 + .w), // Adding some padding to separate text and buttons. + child: Text( + badgeData.key.substring(0, badgeData.key.length - 5), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + softWrap: true, + overflow: TextOverflow + .ellipsis, // Use ellipsis to indicate overflowed text + maxLines: 1, // Limit to 1 line for a cleaner look ), ), ), - SizedBox( - width: 8.w, - ), - Visibility( - visible: - file.jsonToData(badgeData.value).messages[0].marquee, - child: Container( - padding: - EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), - decoration: BoxDecoration( - color: colorPrimary, - borderRadius: BorderRadius.circular(100), - ), - child: Row( - children: [ - Image.asset( - "assets/icons/square.png", - color: Colors.white, - height: 14.h, - ) - ], - ), + Consumer( + builder: (context, provider, widget) => Row( + mainAxisSize: MainAxisSize.min, // Keep the row compact + children: [ + IconButton( + icon: Image.asset( + "assets/icons/t_play.png", + height: 20, + color: Colors.black, + ), + onPressed: () { + provider.savedBadgeAnimation( + badgeData.value, + Provider.of(context, + listen: false), + getBadgeScreenSize().height, + ); + onPreviewSizeChanged?.call(getBadgeScreenSize()); + }, + ), + IconButton( + icon: const Icon( + Icons.edit, + color: Colors.black, + ), + onPressed: () async { + final shouldEdit = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Edit Badge'), + content: const Text( + 'Do you want to edit this badge?'), + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context, false), + child: const Text('No'), + ), + TextButton( + onPressed: () => + Navigator.pop(context, true), + child: const Text('Yes'), + ), + ], + ), + ); + + if (shouldEdit == true) { + String badgeFilename = badgeData.key; + + // Navigate to HomeScreen for editing + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => HomeScreen( + savedBadgeFilename: badgeFilename, + ), + ), + ); + } + }, + ), + IconButton( + icon: Image.asset( + "assets/icons/t_updown.png", + height: 24.h, + color: Colors.black, + ), + onPressed: () { + logger.d("BadgeData: ${badgeData.value}"); + //We can Actually call a method to generate the data just by transferring the JSON data + //so we would not necessarily need the Providers. + badge.checkAndTransfer( + null, + null, + null, + null, + null, + null, + badgeData.value, + true, + getBadgeScreenSize().height, + getBadgeScreenSize().width); + }, + ), + IconButton( + icon: const Icon( + Icons.share, + color: Colors.black, + ), + onPressed: () { + file.shareBadgeData(badgeData.key); + }, + ), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.black, + ), + onPressed: () async { + //add a dialog for confirmation before deleting + await _showDeleteDialog(context) + .then((value) async { + if (value == true) { + file.deleteFile(badgeData.key); + toastUtils + .showToast("Badge Deleted Successfully"); + await refreshBadgesCallback(badgeData); + } + }); + }, + ), + ], ), ), - SizedBox( - width: 8.w, - ), - Visibility( - visible: badgeData.value['messages'][0]['invert'] ?? false, - child: Container( - padding: - EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), - decoration: BoxDecoration( - color: colorPrimary, - borderRadius: BorderRadius.circular(100), - ), + ], + ), + SizedBox(height: 8.h), + // Solution: Wrap the Row in a SingleChildScrollView for horizontal scrolling + Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, child: Row( children: [ - Image.asset( - "assets/icons/t_invert.png", - color: Colors.white, - height: 14.h, - ) + Visibility( + visible: _safeGetFlashValue(badgeData.value), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, vertical: 4.h), + decoration: BoxDecoration( + color: colorPrimary, + borderRadius: BorderRadius.circular(100), + ), + child: Row( + children: [ + Image.asset( + "assets/icons/flash.png", + color: Colors.white, + height: 14.h, + ) + ], + ), + ), + ), + SizedBox(width: 8.w), + Visibility( + visible: _safeGetMarqueeValue(badgeData.value), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, vertical: 4.h), + decoration: BoxDecoration( + color: colorPrimary, + borderRadius: BorderRadius.circular(100), + ), + child: Row( + children: [ + Image.asset( + "assets/icons/square.png", + color: Colors.white, + height: 14.h, + ) + ], + ), + ), + ), + SizedBox(width: 8.w), + Visibility( + visible: _safeGetInvertValue(badgeData.value), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, vertical: 4.h), + decoration: BoxDecoration( + color: colorPrimary, + borderRadius: BorderRadius.circular(100), + ), + child: Row( + children: [ + Image.asset( + "assets/icons/t_invert.png", + color: Colors.white, + height: 14.h, + ) + ], + ), + ), + ), + SizedBox(width: 8.w), + GestureDetector( + onTap: () {}, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, vertical: 4.h), + decoration: BoxDecoration( + color: colorPrimary, + borderRadius: BorderRadius.circular(100), + ), + child: Row( + children: [ + Image.asset( + "assets/icons/t_double.png", + color: Colors.white, + height: 14.h, + ), + const SizedBox(width: 4), + Text( + Speed.getIntValue(file + .jsonToData(badgeData.value) + .messages[0] + .speed) + .toString(), + style: const TextStyle(color: Colors.white), + ), + ], + ), + ), + ), + SizedBox(width: 8.w), + GestureDetector( + onTap: () {}, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, vertical: 4.h), + decoration: BoxDecoration( + color: colorPrimary, + borderRadius: BorderRadius.circular(100), + ), + child: Text( + file + .jsonToData(badgeData.value) + .messages[0] + .mode + .toString() + .split('.') + .last + .toUpperCase(), + style: const TextStyle(color: Colors.white), + ), + ), + ), + SizedBox(width: 8.w), + GestureDetector( + onTap: () {}, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, vertical: 4.h), + decoration: BoxDecoration( + color: colorPrimary, + borderRadius: BorderRadius.circular(100), + ), + child: Text( + getBadgeScreenSize().name, + style: const TextStyle(color: Colors.white), + ), + ), + ), ], ), ), - ) - ], - ), - SizedBox(width: 8.w), - GestureDetector( - onTap: () {}, - child: Container( - padding: - EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), - decoration: BoxDecoration( - color: colorPrimary, - borderRadius: BorderRadius.circular(100), ), - child: Row( - children: [ - Image.asset( - "assets/icons/t_double.png", - color: Colors.white, - height: 14.h, - ), - const SizedBox(width: 4), - Text( - Speed.getIntValue( - file.jsonToData(badgeData.value).messages[0].speed, - ).toString(), - style: const TextStyle(color: Colors.white), - ) - ], - ), - ), - ), - SizedBox(width: 8.w), - GestureDetector( - onTap: () {}, - child: Container( - padding: - EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), - decoration: BoxDecoration( - color: colorPrimary, - borderRadius: BorderRadius.circular(100), - ), - child: Text( - file - .jsonToData(badgeData.value) - .messages[0] - .mode - .toString() - .split('.') - .last - .toUpperCase(), - style: const TextStyle(color: Colors.white), + Consumer( + builder: (context, selectionProvider, _) { + final isSelected = + selectionProvider.isSelected(badgeData.key); + return Switch( + value: isSelected, + onChanged: (selectionProvider.canSelectMore || + isSelected) + ? (value) { + // Check screen size compatibility + final provider = + Provider.of(context, + listen: false); + final selectedBadges = + selectionProvider.selectedBadges; + if (selectedBadges.isNotEmpty && !isSelected) { + // Check if any selected badge has different screen size + bool hasMismatch = false; + ScreenSize currentSize = getBadgeScreenSize(); + for (var key in selectedBadges) { + final selectedBadgeData = provider + .savedBadgeCache + .firstWhere( + (element) => element.key == key) + .value; + int? height = selectedBadgeData['height']; + int? width = selectedBadgeData['width']; + ScreenSize selectedSize; + if (height != null && width != null) { + selectedSize = + supportedScreenSizes.firstWhere( + (size) => + size.height == height && + size.width == width, + orElse: () => + supportedScreenSizes.first); + } else { + selectedSize = supportedScreenSizes.first; + } + if (selectedSize != currentSize) { + hasMismatch = true; + break; + } + } + if (hasMismatch) { + toastUtils.showToast( + 'Cannot select badges with different screen sizes.'); + return; + } + } + selectionProvider + .toggleSelection(badgeData.key); + } + : null, + activeThumbColor: colorPrimary, + ); + }, ), - ), - ), - const Spacer(), - Consumer( - builder: (context, selectionProvider, _) { - final isSelected = - selectionProvider.isSelected(badgeData.key); - return Switch( - value: isSelected, - onChanged: (selectionProvider.canSelectMore || isSelected) - ? (value) => - selectionProvider.toggleSelection(badgeData.key) - : null, - activeThumbColor: colorPrimary, - ); - }, - ), + ], + ) ], ), - ], - ), - ); + )); } Future _showDeleteDialog(BuildContext context) async { diff --git a/lib/view/widgets/save_badge_dialog.dart b/lib/view/widgets/save_badge_dialog.dart index 6b2046233..55c141a79 100644 --- a/lib/view/widgets/save_badge_dialog.dart +++ b/lib/view/widgets/save_badge_dialog.dart @@ -1,74 +1,82 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:badgemagic/bademagic_module/utils/toast_utils.dart'; import 'package:badgemagic/badge_effect/flash_effect.dart'; import 'package:badgemagic/badge_effect/marquee_effect.dart'; import 'package:badgemagic/providers/animation_badge_provider.dart'; import 'package:badgemagic/providers/saved_badge_provider.dart'; import 'package:badgemagic/providers/speed_dial_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'dart:io'; -import 'package:path_provider/path_provider.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/services/localization_service.dart'; import 'package:get_it/get_it.dart'; -class SaveBadgeDialog extends StatelessWidget { +class SaveBadgeDialog extends StatefulWidget { final SpeedDialProvider speed; final bool isInverse; - final AnimationBadgeProvider animationProvider; // Restore this field + final AnimationBadgeProvider animationProvider; final TextEditingController textController; + final ScreenSize selectedSize; const SaveBadgeDialog({ super.key, required this.textController, required this.isInverse, - required this.animationProvider, // Restore this parameter + required this.animationProvider, required this.speed, + required this.selectedSize, }); @override - Widget build(BuildContext context) { - final l10n = GetIt.instance.get().l10n; - SavedBadgeProvider savedBadgeProvider = SavedBadgeProvider(); - TextEditingController badgeNameController = TextEditingController(); - badgeNameController.text = '${l10n.badge} ${DateTime.now().toString()}'; + State createState() => _SaveBadgeDialogState(); +} - // Set up the initial selection to select all text when the dialog opens +class _SaveBadgeDialogState extends State { + late ScreenSize selectedSize; + late TextEditingController badgeNameController; + + @override + void initState() { + super.initState(); + selectedSize = widget.selectedSize; + final l10n = GetIt.instance.get().l10n; + badgeNameController = TextEditingController( + text: '${l10n.badge} ${DateTime.now()}', + ); + // Select all text on open badgeNameController.selection = TextSelection( baseOffset: 0, extentOffset: badgeNameController.text.length, ); + } + + @override + Widget build(BuildContext context) { + final l10n = GetIt.instance.get().l10n; + final savedBadgeProvider = SavedBadgeProvider(); + return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5.r), ), child: Container( - height: 150.h, // Increase height for TextField space - width: 300.w, // Increased width - padding: EdgeInsets.symmetric( - horizontal: 20.w, - vertical: 10.h), // Added padding for better layout + height: 300.h, + width: 300.w, + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, children: [ - Expanded( - flex: 1, - child: Text( - l10n.saveBadge, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), + Text( + l10n.saveBadge, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), + SizedBox(height: 10.h), Text( l10n.badgeName, style: const TextStyle( - fontWeight: FontWeight.w400, - color: Colors.red, - ), + fontWeight: FontWeight.w400, color: Colors.red), ), - const SizedBox(height: 10), TextField( controller: badgeNameController, autofocus: true, @@ -81,62 +89,90 @@ class SaveBadgeDialog extends StatelessWidget { ), ), ), + SizedBox(height: 10.h), + Text( + l10n.selectScreenSize, + style: const TextStyle( + fontWeight: FontWeight.w400, color: Colors.red), + ), + DropdownButton( + value: selectedSize, + isExpanded: true, + items: supportedScreenSizes.map((size) { + return DropdownMenuItem( + value: size, + child: Text(size.name), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() => selectedSize = value); + } + }, + ), + const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text( - l10n.cancel, - style: const TextStyle(color: Colors.red), - )), + onPressed: () => Navigator.pop(context), + child: Text(l10n.cancel, + style: const TextStyle(color: Colors.red)), + ), TextButton( onPressed: () async { - final directory = await getApplicationDocumentsDirectory(); - final trimmedBadgeName = badgeNameController.text.trim(); - final filePath = '${directory.path}/$trimmedBadgeName.json'; + final trimmedName = badgeNameController.text.trim(); + if (trimmedName.isEmpty) { + ToastUtils().showToast(l10n.enterValidBadgeName); + return; + } + + final dir = await getApplicationDocumentsDirectory(); + if (!mounted) return; + + final filePath = '${dir.path}/$trimmedName.json'; final file = File(filePath); - final files = directory.listSync(); - List caseInsensitiveMatches = []; + // Case-insensitive check + final files = dir.listSync(); + String? ciMatch; for (var f in files) { if (f is File) { - final filename = - f.path.split(Platform.pathSeparator).last; - if (filename.toLowerCase().endsWith('.json')) { - final baseName = - filename.substring(0, filename.length - 5).trim(); - if (baseName.toLowerCase() == - trimmedBadgeName.toLowerCase()) { - caseInsensitiveMatches.add(filename); + final name = f.path.split(Platform.pathSeparator).last; + if (name.toLowerCase().endsWith('.json')) { + final base = + name.substring(0, name.length - 5).trim(); + if (base.toLowerCase() == trimmedName.toLowerCase()) { + ciMatch = name; + break; } } } } - String? caseInsensitiveMatch = - caseInsensitiveMatches.isNotEmpty - ? caseInsensitiveMatches.first - : null; - bool caseSensitiveExists = await file.exists(); + final exists = await file.exists(); + if (!mounted) return; - if (caseSensitiveExists) { + if (exists || ciMatch != null) { final result = await showDialog( context: context, - builder: (context) => AlertDialog( - title: Text(l10n.badgeNameExists), - content: Text(l10n.badgeExistsMessage), + builder: (_) => AlertDialog( + title: Text(exists + ? l10n.badgeNameExists + : l10n.similarBadgeExists), + content: Text(exists + ? l10n.badgeExistsMessage + : l10n.similarBadgeExistsMessage( + ciMatch!.substring(0, ciMatch.length - 5))), actions: [ TextButton( - onPressed: () => Navigator.pop(context, 'rename'), - child: Text(l10n.cancel), - ), + onPressed: () => + Navigator.pop(context, 'rename'), + child: Text(l10n.cancel)), TextButton( - onPressed: () => Navigator.pop(context, 'update'), - child: Text(l10n.overwrite), - ), + onPressed: () => + Navigator.pop(context, 'update'), + child: Text(l10n.overwrite)), ], ), ); @@ -144,100 +180,42 @@ class SaveBadgeDialog extends StatelessWidget { ToastUtils().showToast(l10n.pleaseEnterNewBadgeName); return; } else if (result == 'update') { - savedBadgeProvider.saveBadgeData( - badgeNameController.text, - textController.text, - animationProvider.isEffectActive(FlashEffect()), - animationProvider.isEffectActive(MarqueeEffect()), - isInverse, - speed.getOuterValue(), - animationProvider.getAnimationIndex() ?? 1, - ); - ToastUtils().showToast(l10n.badgeUpdatedSuccessfully); - Future.delayed(const Duration(milliseconds: 100), () { - Navigator.of(context, rootNavigator: true) - .pushNamedAndRemoveUntil( - '/savedBadge', (route) => false); - }); - return; - } else { - return; - } - } else if (caseInsensitiveMatch != null) { - final result = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(l10n.similarBadgeExists), - content: Builder( - builder: (context) { - final badgeName = caseInsensitiveMatch.substring( - 0, caseInsensitiveMatch.length - 5); - final message = - l10n.similarBadgeExistsMessage(badgeName); - return Text(message); - }, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, 'rename'), - child: Text(l10n.cancel), - ), - TextButton( - onPressed: () => Navigator.pop(context, 'update'), - child: Text(l10n.overwrite), - ), - ], - ), - ); - if (result == 'rename') { - ToastUtils().showToast(l10n.pleaseEnterNewBadgeName); - return; - } else if (result == 'update') { - final existingFilePath = - '${directory.path}/$caseInsensitiveMatch'; - final existingFile = File(existingFilePath); - await existingFile.writeAsString(''); - savedBadgeProvider.saveBadgeData( - caseInsensitiveMatch.substring( - 0, caseInsensitiveMatch.length - 5), - textController.text, - animationProvider.isEffectActive(FlashEffect()), - animationProvider.isEffectActive(MarqueeEffect()), - isInverse, - speed.getOuterValue(), - animationProvider.getAnimationIndex() ?? 1, - ); - ToastUtils().showToast(l10n.badgeUpdatedSuccessfully); - Future.delayed(const Duration(milliseconds: 100), () { - Navigator.of(context, rootNavigator: true) - .pushNamedAndRemoveUntil( - '/savedBadge', (route) => false); - }); - return; + if (ciMatch != null) { + final existingFile = File('${dir.path}/$ciMatch'); + await existingFile.writeAsString(''); + if (!mounted) return; + } } else { return; } - } else { - savedBadgeProvider.saveBadgeData( - badgeNameController.text, - textController.text, - animationProvider.isEffectActive(FlashEffect()), - animationProvider.isEffectActive(MarqueeEffect()), - isInverse, - speed.getOuterValue(), - animationProvider.getAnimationIndex() ?? 1, + } + + // Save badge + savedBadgeProvider.saveBadgeData( + trimmedName, + widget.textController.text, + widget.animationProvider.isEffectActive(FlashEffect()), + widget.animationProvider.isEffectActive(MarqueeEffect()), + widget.isInverse, + widget.speed.getOuterValue(), + widget.animationProvider.getAnimationIndex() ?? 1, + selectedSize.height, + selectedSize.width, + ); + if (mounted) { + ToastUtils().showToast( + exists || ciMatch != null + ? l10n.badgeUpdatedSuccessfully + : l10n.badgeSavedSuccessfully, ); - ToastUtils().showToast(l10n.badgeSavedSuccessfully); Navigator.of(context).pop(); } }, - child: Text( - 'Save', - style: const TextStyle(color: Colors.red), - ), + child: + Text('Save', style: const TextStyle(color: Colors.red)), ), ], - ) + ), ], ), ), diff --git a/lib/view/widgets/saved_badge_listview.dart b/lib/view/widgets/saved_badge_listview.dart index 1b19a0477..1541e18bd 100644 --- a/lib/view/widgets/saved_badge_listview.dart +++ b/lib/view/widgets/saved_badge_listview.dart @@ -1,3 +1,4 @@ +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/view/widgets/save_badge_card.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -9,14 +10,15 @@ class BadgeListView extends StatelessWidget { final Future Function(MapEntry>) refreshBadgesCallback; final void Function()? onSelectionChanged; + final void Function(ScreenSize)? onPreviewSizeChanged; - const BadgeListView({ - super.key, - required this.isTransferEnabled, - required this.futureBadges, - required this.refreshBadgesCallback, - this.onSelectionChanged, - }); + const BadgeListView( + {super.key, + required this.isTransferEnabled, + required this.futureBadges, + required this.refreshBadgesCallback, + this.onSelectionChanged, + this.onPreviewSizeChanged}); @override Widget build(BuildContext context) { @@ -52,6 +54,7 @@ class BadgeListView extends StatelessWidget { if (onSelectionChanged != null) onSelectionChanged!(); } }, + onPreviewSizeChanged: onPreviewSizeChanged, ); }, ), diff --git a/lib/view/widgets/transitiontab.dart b/lib/view/widgets/transitiontab.dart index f80f25144..f8c4ddfba 100644 --- a/lib/view/widgets/transitiontab.dart +++ b/lib/view/widgets/transitiontab.dart @@ -1,12 +1,13 @@ -import 'package:badgemagic/constants.dart'; import 'package:badgemagic/services/localization_service.dart'; import 'package:badgemagic/view/widgets/animation_container.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; -// Transition tab to show basic animations class TransitionTab extends StatefulWidget { - const TransitionTab({super.key}); + final ScreenSize selectedSize; + + const TransitionTab({super.key, required this.selectedSize}); @override State createState() => _TransitionTabState(); @@ -24,67 +25,145 @@ class _TransitionTabState extends State { @override Widget build(BuildContext context) { final l10n = GetIt.instance.get().l10n; - return SingleChildScrollView( - child: Column( - children: [ - Row( - children: [ + final horizontalPadding = 8.0; + + return Scrollbar( + controller: _scrollController, + thumbVisibility: true, + thickness: 6.0, + radius: const Radius.circular(6), + child: SingleChildScrollView( + controller: _scrollController, + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Column( + children: [ + _buildTileRow([ + AniContainer( + animation: null, + icon: Icons.sports_esports, + animationName: l10n.pacman, + index: 9, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.chevron_left, + animationName: l10n.chevron, + index: 10, + screenSize: widget.selectedSize, + ), + AniContainer( + animation: null, + icon: Icons.diamond, + animationName: l10n.diamond, + index: 11, + screenSize: widget.selectedSize, + ), + ]), + _buildTileRow([ + AniContainer( + animation: null, + icon: Icons.heart_broken, + animationName: l10n.brokenHearts, + index: 12, + screenSize: widget.selectedSize, + ), AniContainer( - animation: aniLeft, - animationName: l10n.animationLeft, - index: 0, + animation: null, + icon: Icons.favorite_border, + animationName: l10n.cupid, + index: 13, + screenSize: widget.selectedSize, ), AniContainer( - animation: aniRight, - animationName: l10n.animationRight, - index: 1, + animation: null, + icon: Icons.directions_walk, + animationName: l10n.feet, + index: 14, + screenSize: widget.selectedSize, ), + ]), + _buildTileRow([ AniContainer( - animation: aniUp, - animationName: l10n.animationUp, - index: 2, + animation: null, + icon: Icons.set_meal, + animationName: l10n.fishKiss, + index: 15, + screenSize: widget.selectedSize, ), - ], - ), - Row( - children: [ AniContainer( - animation: aniDown, - animationName: l10n.animationDown, - index: 3, + animation: null, + icon: Icons.change_history, + animationName: l10n.diagonal, + index: 16, + screenSize: widget.selectedSize, ), AniContainer( - animation: aniFixed, - animationName: l10n.animationFixed, - index: 4, + animation: null, + icon: Icons.warning, + animationName: l10n.emergency, + index: 17, + screenSize: widget.selectedSize, ), + ]), + _buildTileRow([ AniContainer( - animation: animation, - animationName: l10n.animation, - index: 5, + animation: null, + icon: Icons.favorite, + animationName: l10n.beatingHearts, + index: 18, + screenSize: widget.selectedSize, ), - ], - ), - Row( - children: [ AniContainer( - animation: aniSnowflake, - animationName: l10n.animationSnowflake, - index: 6, + animation: null, + icon: Icons.celebration, + animationName: l10n.fireworks, + index: 19, + screenSize: widget.selectedSize, ), AniContainer( - animation: aniPicture, - animationName: l10n.animationPicture, - index: 7, + animation: null, + icon: Icons.equalizer, + animationName: l10n.equalizer, + index: 20, + screenSize: widget.selectedSize, ), + ]), + _buildTileRow([ AniContainer( - animation: aniLaser, - animationName: l10n.animationLaser, - index: 8, + animation: null, + icon: Icons.directions_bike, + animationName: l10n.cycle, + index: 21, + screenSize: widget.selectedSize, ), - ], - ), - ], + ]), + ], + ), + ), + ); + } + + Widget _buildTileRow(List tiles) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: tiles.length == 1 + ? MainAxisAlignment.center + : MainAxisAlignment.spaceEvenly, + children: tiles.map((tile) { + return tiles.length == 1 + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: tile, + ) + : Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: tile, + ), + ); + }).toList(), ), ); } diff --git a/lib/virtualbadge/view/animated_badge.dart b/lib/virtualbadge/view/animated_badge.dart index e28805e49..911616245 100644 --- a/lib/virtualbadge/view/animated_badge.dart +++ b/lib/virtualbadge/view/animated_badge.dart @@ -1,10 +1,13 @@ +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/providers/animation_badge_provider.dart'; import 'package:badgemagic/virtualbadge/view/badge_paint.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class AnimationBadge extends StatefulWidget { - const AnimationBadge({super.key}); + final ScreenSize selectedSize; + + const AnimationBadge({super.key, required this.selectedSize}); @override State createState() => _AnimationBadgeState(); @@ -15,10 +18,24 @@ class _AnimationBadgeState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().initializeAnimation(); + final provider = context.read(); + provider.initGrids(widget.selectedSize); + provider.initializeAnimation(); }); } + @override + void didUpdateWidget(covariant AnimationBadge oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedSize != oldWidget.selectedSize) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final provider = context.read(); + provider.initGrids(widget.selectedSize); + provider.initializeAnimation(); + }); + } + } + @override Widget build(BuildContext context) { final provider = context.watch(); diff --git a/lib/virtualbadge/view/badge_paint.dart b/lib/virtualbadge/view/badge_paint.dart index 782ab0127..9463be456 100644 --- a/lib/virtualbadge/view/badge_paint.dart +++ b/lib/virtualbadge/view/badge_paint.dart @@ -11,6 +11,10 @@ class BadgePaint extends CustomPainter { @override void paint(Canvas canvas, Size size) { + if (grid.isEmpty || grid[0].isEmpty) { + return; + } + // Padding for the rectangle MapEntry badgeOffsetBackground = badgeUtils.getBadgeOffsetBackground(size); diff --git a/lib/virtualbadge/view/draw_badge.dart b/lib/virtualbadge/view/draw_badge.dart index 7decd0149..fbcc99412 100644 --- a/lib/virtualbadge/view/draw_badge.dart +++ b/lib/virtualbadge/view/draw_badge.dart @@ -1,32 +1,54 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +// ignore_for_file: invalid_use_of_visible_for_testing_member + +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; +import 'package:badgemagic/bademagic_module/utils/badge_utils.dart'; import 'package:badgemagic/providers/draw_badge_provider.dart'; import 'package:badgemagic/virtualbadge/view/badge_paint.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class BMBadge extends StatefulWidget { final void Function(DrawBadgeProvider provider)? providerInit; final List>? badgeGrid; + final ScreenSize selectedSize; - const BMBadge({super.key, this.providerInit, this.badgeGrid}); + const BMBadge({ + super.key, + this.providerInit, + this.badgeGrid, + required this.selectedSize, + }); @override State createState() => _BMBadgeState(); } class _BMBadgeState extends State { - final drawProvider = DrawBadgeProvider(); + BadgeUtils badgeUtils = BadgeUtils(); + late DrawBadgeProvider drawProvider; Offset? dragStart; @override void initState() { super.initState(); - if (widget.providerInit != null) widget.providerInit!(drawProvider); + drawProvider = DrawBadgeProvider(); + drawProvider.initGridWithSize(widget.selectedSize); + + if (widget.providerInit != null) { + widget.providerInit!(drawProvider); + } if (widget.badgeGrid != null) { drawProvider.updateDrawViewGrid(widget.badgeGrid!); } } - double get _cellSize => MediaQuery.of(context).size.width / 44; + @override + void didUpdateWidget(covariant BMBadge oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedSize != oldWidget.selectedSize) { + drawProvider.initGridWithSize(widget.selectedSize); + } + } Offset _getLocalPosition(Offset globalPosition) { final renderBox = context.findRenderObject() as RenderBox; @@ -39,38 +61,76 @@ class _BMBadgeState extends State { } void _handlePanUpdate(DragUpdateDetails details) { - if (dragStart == null) return; // Safety check - - final localPosition = _getLocalPosition(details.globalPosition); - final shape = drawProvider.selectedShape; - - final start = drawProvider.getGridPosition(dragStart!, _cellSize); - final end = drawProvider.getGridPosition(localPosition, _cellSize); - - drawProvider.clearPreviewGrid(); - - switch (shape) { - case DrawShape.freehand: - _drawLine(start.x, start.y, end.x, end.y, preview: false); - dragStart = localPosition; // update for next stroke segment - break; - case DrawShape.square: - int size = ((end.x - start.x).abs() + (end.y - start.y).abs()) ~/ 2; - _drawSquare(start.x, start.y, size, preview: true); - break; - case DrawShape.rectangle: - int w = (end.y - start.y).abs() ~/ 2; - int h = (end.x - start.x).abs() ~/ 2; - _drawRectangle(start.x, start.y, h, w, preview: true); - break; - case DrawShape.circle: - int radius = ((end.x - start.x).abs() + (end.y - start.y).abs()) ~/ 2; - _drawCircle(start.x, start.y, radius, preview: true); - break; - case DrawShape.triangle: - int height = (end.x - start.x).abs(); - _drawTriangle(start.x, start.y, height, preview: true); - break; + final renderBox = context.findRenderObject() as RenderBox; + final localPosition = renderBox.globalToLocal(details.globalPosition); + + final rows = widget.selectedSize.height; + final cols = widget.selectedSize.width; + + // Background offsets + badge scaling + final badgeOffsetBackground = + badgeUtils.getBadgeOffsetBackground(renderBox.size); + final offsetHeightBadgeBackground = badgeOffsetBackground.key; + final offsetWidthBadgeBackground = badgeOffsetBackground.value; + + final badgeSize = badgeUtils.getBadgeSize(offsetHeightBadgeBackground, + offsetWidthBadgeBackground, renderBox.size); + final badgeHeight = badgeSize.key; + final badgeWidth = badgeSize.value; + + final cellSize = badgeWidth / cols; + + final cellStartCoordinate = badgeUtils.getCellStartCoordinate( + offsetWidthBadgeBackground, + offsetHeightBadgeBackground, + badgeWidth, + badgeHeight); + final cellStartX = cellStartCoordinate.key; + final cellStartY = cellStartCoordinate.value; + + final cellEndX = cellStartX + (cellSize * cols); + final cellEndY = cellStartY + ((cellSize * 0.93) * rows); + + if (localPosition.dx >= cellStartX && + localPosition.dy >= cellStartY && + localPosition.dx < cellEndX && + localPosition.dy < cellEndY * 1.1) { + final shape = drawProvider.selectedShape; + + final start = drawProvider.getGridPosition(dragStart!, cellSize); + final end = drawProvider.getGridPosition(localPosition, cellSize); + + drawProvider.clearPreviewGrid(); + + switch (shape) { + case DrawShape.freehand: + _drawLine(start.row, start.col, end.row, end.col, preview: false); + dragStart = localPosition; + drawProvider.commitGridUpdate(); + break; + case DrawShape.square: + final size = + ((end.row - start.row).abs() + (end.col - start.col).abs()) ~/ 2; + _drawSquare(start.row, start.col, size, preview: true); + break; + case DrawShape.rectangle: + final w = (end.col - start.col).abs() ~/ 2; + final h = (end.row - start.row).abs() ~/ 2; + _drawRectangle(start.row, start.col, h, w, preview: true); + break; + case DrawShape.circle: + final radius = + ((end.row - start.row).abs() + (end.col - start.col).abs()) ~/ 2; + _drawCircle(start.row, start.col, radius, preview: true); + break; + case DrawShape.triangle: + final height = (end.row - start.row).abs(); + _drawTriangle(start.row, start.col, height, preview: true); + break; + } + + // ignore: invalid_use_of_protected_member + drawProvider.notifyListeners(); } } @@ -142,7 +202,9 @@ class _BMBadgeState extends State { @override Widget build(BuildContext context) { + final aspectRatio = widget.selectedSize.width / widget.selectedSize.height; final width = MediaQuery.of(context).size.width; + final size = Size(width, width / aspectRatio); return ChangeNotifierProvider.value( value: drawProvider, @@ -152,11 +214,11 @@ class _BMBadgeState extends State { onPanUpdate: _handlePanUpdate, onPanEnd: _handlePanEnd, child: AspectRatio( - aspectRatio: 3.2, + aspectRatio: aspectRatio, child: Consumer( builder: (_, value, __) => CustomPaint( painter: BadgePaint(grid: value.getDrawViewGrid()), - size: Size(width, width / 3.2), + size: size, ), ), ), diff --git a/test/byte_array_utils_test.dart b/test/byte_array_utils_test.dart index 219329cfb..4fa7625f5 100644 --- a/test/byte_array_utils_test.dart +++ b/test/byte_array_utils_test.dart @@ -36,6 +36,7 @@ void main() { final expectedBytes = [10, 20, 30, 40]; final result = hexStringToByteArray(hexString); + expect(result, equals(expectedBytes)); }); diff --git a/test/converters_test.dart b/test/converters_test.dart index 938fcf0e2..ba6dd9f23 100644 --- a/test/converters_test.dart +++ b/test/converters_test.dart @@ -1,3 +1,4 @@ +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/bademagic_module/utils/converters.dart'; import 'package:badgemagic/providers/getitlocator.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -9,13 +10,21 @@ void main() { setupLocator(); Converters converters = Converters(); const String message = "Hii!"; - List result = await converters.messageTohex(message, false); + const int badgeHeight = 11; + const int badgeWidth = 44; + List result = await converters.messageTohex( + message, + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + ); List expected = [ - "00C6C6C6C6FEC6C6C6C600", - "0018180038181818183C00", - "0018180038181818183C00", - "00183C3C3C181800181800" + "00666666667e6666666600", // 'H' + "0010100030101010103800", // 'i' + "0010100030101010103800", // 'i' + "0010383838101000101000", // '!' ]; + expect(result, expected); }); diff --git a/test/data_to_bytearray_converter_test.dart b/test/data_to_bytearray_converter_test.dart index 100d2ef4a..6ae2cacb2 100644 --- a/test/data_to_bytearray_converter_test.dart +++ b/test/data_to_bytearray_converter_test.dart @@ -2,6 +2,7 @@ import 'dart:core'; import 'package:badgemagic/bademagic_module/models/data.dart'; import 'package:badgemagic/bademagic_module/models/messages.dart'; import 'package:badgemagic/bademagic_module/models/mode.dart'; +import 'package:badgemagic/bademagic_module/models/screen_size.dart'; import 'package:badgemagic/bademagic_module/models/speed.dart'; import 'package:badgemagic/bademagic_module/utils/byte_array_utils.dart'; import 'package:badgemagic/bademagic_module/utils/converters.dart'; @@ -10,6 +11,9 @@ import 'package:badgemagic/providers/getitlocator.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + const int badgeHeight = 11; // adjust as needed for your test badge + const int badgeWidth = 44; + test('result should start with 77616E670000', () { DataToByteArrayConverter converter = DataToByteArrayConverter(); var data = Data(messages: [ @@ -38,14 +42,78 @@ void main() { Converters converters = Converters(); DataToByteArrayConverter converter = DataToByteArrayConverter(); var data = Data(messages: [ - Message(text: await converters.messageTohex('Hii', false), flash: true), - Message(text: await converters.messageTohex('Hii', false), flash: true), - Message(text: await converters.messageTohex('Hii', false), flash: false), - Message(text: await converters.messageTohex('Hii', false), flash: false), - Message(text: await converters.messageTohex('Hii', false), flash: true), - Message(text: await converters.messageTohex('Hii', false), flash: false), - Message(text: await converters.messageTohex('Hii', false), flash: true), - Message(text: await converters.messageTohex('Hii', false), flash: false) + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + flash: true), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + flash: true), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + flash: false), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + flash: false), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + flash: true), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + flash: false), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + flash: true), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + flash: false) ]); var result = converter.convert(data); @@ -58,7 +126,15 @@ void main() { Converters converters = Converters(); DataToByteArrayConverter converter = DataToByteArrayConverter(); var data = Data(messages: [ - Message(text: await converters.messageTohex('Hii', false), marquee: false) + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + marquee: false) ]); var result = converter.convert(data); @@ -72,17 +148,79 @@ void main() { Converters converters = Converters(); DataToByteArrayConverter converter = DataToByteArrayConverter(); var data = Data(messages: [ - Message(text: await converters.messageTohex('Hii', false), marquee: true), - Message(text: await converters.messageTohex('Hii', false), marquee: true), Message( - text: await converters.messageTohex('Hii', false), marquee: false), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + marquee: true, + ), Message( - text: await converters.messageTohex('Hii', false), marquee: false), - Message(text: await converters.messageTohex('Hii', false), marquee: true), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + marquee: true), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + marquee: false), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + marquee: false), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + marquee: true), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + marquee: false), Message( - text: await converters.messageTohex('Hii', false), marquee: false), - Message(text: await converters.messageTohex('Hii', false), marquee: true), - Message(text: await converters.messageTohex('Hii', false), marquee: false) + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + marquee: true), + Message( + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), + marquee: false) ]); var result = converter.convert(data); @@ -97,35 +235,83 @@ void main() { DataToByteArrayConverter converter = DataToByteArrayConverter(); Data data = Data(messages: [ Message( - text: await converters.messageTohex('Hii', false), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), speed: Speed.one, mode: Mode.right), Message( - text: await converters.messageTohex('Hii', false), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), speed: Speed.two, mode: Mode.left), Message( - text: await converters.messageTohex('Hii', false), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), speed: Speed.three, mode: Mode.up), Message( - text: await converters.messageTohex('Hii', false), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), speed: Speed.four, mode: Mode.fixed), Message( - text: await converters.messageTohex("Hii", false), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), speed: Speed.five, mode: Mode.animation), Message( - text: await converters.messageTohex('Hii', false), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), speed: Speed.six, mode: Mode.laser), Message( - text: await converters.messageTohex('Hii', false), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), speed: Speed.seven, mode: Mode.snowflake), Message( - text: await converters.messageTohex('Hii', false), + text: await converters.messageTohex( + 'Hii', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + ), speed: Speed.eight, mode: Mode.picture), ]); @@ -142,15 +328,62 @@ void main() { Converters converters = Converters(); DataToByteArrayConverter converter = DataToByteArrayConverter(); Data data = Data(messages: [ - Message(text: await converters.messageTohex('A', false)), - Message(text: await converters.messageTohex('...', false)), Message( text: await converters.messageTohex( - 'abcdefghijklmnopqrstuvwxyz', false)), - Message(text: await converters.messageTohex('_' * 500, false)), - Message(text: await converters.messageTohex('°', false)), - Message(text: await converters.messageTohex('ÇÇÇÇÇabc', false)), - Message(text: await converters.messageTohex('', false)), + 'A', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + '...', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + 'abcdefghijklmnopqrstuvwxyz', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + '_' * 500, + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + '°', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + 'ÇÇÇÇÇabc', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + '', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), ]); List> result = converter.convert(data); @@ -179,8 +412,16 @@ void main() { Converters converters = Converters(); DataToByteArrayConverter converter = DataToByteArrayConverter(); - var data = Data( - messages: [Message(text: await converters.messageTohex('A', false))]); + var data = Data(messages: [ + Message( + text: await converters.messageTohex( + 'A', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )) + ]); var result = converter.convert(data); expect(result[2].sublist(0, 6), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); @@ -204,8 +445,22 @@ void main() { Converters converters = Converters(); DataToByteArrayConverter converter = DataToByteArrayConverter(); Data data = Data(messages: [ - Message(text: await converters.messageTohex('AB', false)), - Message(text: await converters.messageTohex('°C', false)), + Message( + text: await converters.messageTohex( + 'AB', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + '°C', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), ]); List> result = converter.convert(data); @@ -221,17 +476,67 @@ void main() { Converters converters = Converters(); DataToByteArrayConverter converter = DataToByteArrayConverter(); // Given - final data1 = Data( - messages: [Message(text: await converters.messageTohex('A', false))]); + final data1 = Data(messages: [ + Message( + text: await converters.messageTohex( + 'A', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )) + ]); final data2 = Data(messages: [ - Message(text: await converters.messageTohex('B', false)), - Message(text: await converters.messageTohex('BBB', false)) + Message( + text: await converters.messageTohex( + 'B', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + 'BBB', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )) ]); final data3 = Data(messages: [ - Message(text: await converters.messageTohex('C', false)), - Message(text: await converters.messageTohex('CCC', false)), - Message(text: await converters.messageTohex('CCCCC', false)), - Message(text: await converters.messageTohex('CCCCCCCC', false)) + Message( + text: await converters.messageTohex( + 'C', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + 'CCC', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + 'CCCCC', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )), + Message( + text: await converters.messageTohex( + 'CCCCCCCC', + false, + badgeHeight, + ScreenSize(width: badgeWidth, height: badgeHeight, name: ''), + scale: false, + )) ]); // When