diff --git a/assets/displays.json b/assets/displays.json new file mode 100644 index 00000000..7be8447a --- /dev/null +++ b/assets/displays.json @@ -0,0 +1,26 @@ +{ + "displays": [ + { + "id": "epaper-3.7-bwr", + "model": "GDEY037Z03", + "name": "E-Paper 3.7\"", + "size": 3.7, + "resolution": [416, 240], + "colors": ["black", "white", "red"], + "driver": "UC8253", + "imagePath": "assets/images/displays/epaper_3.7_bwr.png", + "epdClass": "Gdey037z03" + }, + { + "id": "epaper-3.7-bw", + "model": "GDEY037T03", + "name": "E-Paper 3.7\"", + "size": 3.7, + "resolution": [416, 240], + "colors": ["black", "white"], + "driver": "UC8253", + "imagePath": "assets/images/displays/epaper_3.7_bw.PNG", + "epdClass": "Gdey037z03BW" + } + ] +} \ No newline at end of file diff --git a/assets/images/displays/epaper_3.7_bw.PNG b/assets/images/displays/epaper_3.7_bw.PNG new file mode 100644 index 00000000..69736eea Binary files /dev/null and b/assets/images/displays/epaper_3.7_bw.PNG differ diff --git a/assets/images/displays/epaper_3.7_bwr.png b/assets/images/displays/epaper_3.7_bwr.png new file mode 100644 index 00000000..ccbcd1fc Binary files /dev/null and b/assets/images/displays/epaper_3.7_bwr.png differ diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 00000000..a86dc8e0 --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +//Colors used in the app +// Primary Colors +const Color colorPrimary = Color(0xFFD32F2F); // You can adjust this color value +const Color colorPrimaryDark = Color(0xFFC72C2C); +const Color colorAccent = Color(0xFFD32F2F); + +// Knob Colors +const Color backCircleColor = Color(0xFFEDEDED); +const Color indicatorColor = Color(0xFFD32F2F); +const Color progressSecondaryColor = Color(0xFFEEEEEE); + +// Additional Colors +const Color mdGrey400 = Color(0xFFBDBDBD); +const Color dividerColor = Color(0xFFE0E0E0); +const Color drawerHeaderTitle = Color(0xFFFFFFFF); diff --git a/lib/main.dart b/lib/main.dart index c712efac..e358f88e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:magic_epaper_app/provider/image_loader.dart'; +import 'package:magic_epaper_app/provider/display_provider.dart'; import 'package:provider/provider.dart'; -import 'package:magic_epaper_app/view/home_screen.dart'; +import 'package:magic_epaper_app/view/display_selection_screen.dart'; void main() { runApp(MultiProvider(providers: [ ChangeNotifierProvider(create: (context) => ImageLoader()), + ChangeNotifierProvider(create: (context) => DisplayProvider()), ], child: const MyApp())); } @@ -15,13 +17,9 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( title: 'Magic Epaper', - theme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange), - ), - home: const SelectDisplay(), + home: DisplaySelectionScreen(), ); } } diff --git a/lib/model/display_model.dart b/lib/model/display_model.dart new file mode 100644 index 00000000..661af45d --- /dev/null +++ b/lib/model/display_model.dart @@ -0,0 +1,65 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/util/epd/edp.dart'; + +class DisplayModel { + final String id; + final String name; + final String ModelName; // For future use + final double size; // Inches + final int width, height; // Pixels + final List colors; + final String driver; + final String imagePath; + final Epd epd; + + // Constructor + DisplayModel({ + required this.id, + required this.name, + required this.size, + required this.width, + required this.height, + required this.colors, + required this.ModelName, + required this.driver, + required this.imagePath, + required this.epd, + }); + + // Computed properties + bool get isColor => colors.length > 2; // More than black and white + bool get isHd => width >= 1280 && height >= 720; + double get ppi => sqrt(pow(width, 2) + pow(height, 2)) / size; + + // Format aspect ratio + String get aspectRatio { + int gcd = _findGCD(width, height); + return '${width ~/ gcd}:${height ~/ gcd}'; + } + + // Helper method to find GCD for aspect ratio calculation + int _findGCD(int a, int b) { + while (b != 0) { + int t = b; + b = a % b; + a = t; + } + return a; + } + + // Get color names as a formatted string + String get colorNames { + final List colorNames = []; + + if (colors.contains(Colors.black)) colorNames.add('Black'); + if (colors.contains(Colors.white)) colorNames.add('White'); + if (colors.contains(Colors.red)) colorNames.add('Red'); + if (colors.contains(Colors.yellow)) colorNames.add('Yellow'); + + return colorNames.join(', '); + } + + // Format resolution as a string + String get resolution => '$width × $height'; +} diff --git a/lib/provider/display_provider.dart b/lib/provider/display_provider.dart new file mode 100644 index 00000000..db9c04d3 --- /dev/null +++ b/lib/provider/display_provider.dart @@ -0,0 +1,145 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:magic_epaper_app/model/display_model.dart'; +import 'package:magic_epaper_app/util/epd/gdey037z03.dart'; +import 'package:magic_epaper_app/util/epd/gdey037z03bw.dart'; + +class DisplayProvider extends ChangeNotifier { + // Filter options + String _activeFilter = 'All Displays'; + int _selectedDisplayIndex = -1; // -1 means no selection + + // Getters + String get activeFilter => _activeFilter; + int get selectedDisplayIndex => _selectedDisplayIndex; + bool get hasSelection => _selectedDisplayIndex != -1; + DisplayModel? get selectedDisplay => + hasSelection ? filteredDisplays[_selectedDisplayIndex] : null; + + // All filter options + final List filterOptions = [ + 'All Displays', + 'Color', + 'Black & White', + 'HD', + ]; + + // List of all available displays + List allDisplays = []; + + // Constructor - Initialize by loading from JSON + DisplayProvider() { + loadDisplaysFromJson(); + } + + // Load displays from JSON file + Future loadDisplaysFromJson() async { + try { + // Load the JSON file from the assets + final String jsonString = + await rootBundle.loadString('assets/displays.json'); + final Map jsonData = json.decode(jsonString); + + // Clear the existing displays + allDisplays = []; + + // Parse the JSON data + for (var displayData in jsonData['displays']) { + // Convert colors from strings to Color objects + List colors = []; + for (var colorName in displayData['colors']) { + switch (colorName) { + case 'black': + colors.add(Colors.black); + break; + case 'white': + colors.add(Colors.white); + break; + case 'red': + colors.add(Colors.red); + break; + case 'yellow': + colors.add(Colors.yellow); + break; + } + } + + // Determine which EPD to use based on the class name + var epd = _getEpdForClassName(displayData['epdClass']); + + // Create a DisplayModel from the JSON data + final display = DisplayModel( + id: displayData['id'], + name: displayData['name'], + size: displayData['size'], + ModelName: displayData['model'], + width: displayData['resolution'][0], + height: displayData['resolution'][1], + colors: colors, + driver: displayData['driver'], + imagePath: displayData['imagePath'], + epd: epd, + ); + + allDisplays.add(display); + } + + // Notify listeners that the data has changed + notifyListeners(); + } catch (e) { + print('Error loading displays from JSON: $e'); + } + } + + // Helper method to get the appropriate EPD based on class name + dynamic _getEpdForClassName(String className) { + switch (className) { + case 'Gdey037z03': + return Gdey037z03(); + case 'Gdey037z03BW': + return Gdey037z03BW(); + default: + return Gdey037z03(); // Default fallback + } + } + + // Get filtered displays based on the active filter + List get filteredDisplays { + switch (_activeFilter) { + case 'HD': + return allDisplays.where((display) => display.isHd).toList(); + case 'Color': + return allDisplays.where((display) => display.isColor).toList(); + case 'Black & White': + return allDisplays.where((display) => !display.isColor).toList(); + case 'All Displays': + default: + return allDisplays; + } + } + + // Set the active filter + void setFilter(String filter) { + if (_activeFilter != filter) { + _activeFilter = filter; + // Reset selection when filter changes + _selectedDisplayIndex = -1; + notifyListeners(); + } + } + + // Select a display + void selectDisplay(int index) { + if (index >= 0 && index < filteredDisplays.length) { + _selectedDisplayIndex = index; + notifyListeners(); + } + } + + // Clear selection + void clearSelection() { + _selectedDisplayIndex = -1; + notifyListeners(); + } +} diff --git a/lib/view/display_selection_screen.dart b/lib/view/display_selection_screen.dart new file mode 100644 index 00000000..46d1175c --- /dev/null +++ b/lib/view/display_selection_screen.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:magic_epaper_app/provider/display_provider.dart'; +import 'package:magic_epaper_app/view/widget/display_card.dart'; +import 'package:magic_epaper_app/view/widget/filter_chip_option.dart'; +import 'package:magic_epaper_app/view/image_editor.dart'; +import 'package:magic_epaper_app/constants.dart'; + +class DisplaySelectionScreen extends StatelessWidget { + const DisplaySelectionScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: colorAccent, + elevation: 0, // Remove shadow + title: const Padding( + padding: EdgeInsets.fromLTRB(5, 16, 16, 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Magic ePaper', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + SizedBox(height: 8), + Text( + 'Select your ePaper display type', + style: TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + ], + ), + ), + toolbarHeight: 85, // Adjust height to accommodate both texts + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(10.0, 14, 16.0, 16.0), + child: Column( + children: [ + // Filter chips + _buildFilterChips(), + const SizedBox(height: 16), + + // Display cards grid + Expanded( + child: _buildDisplayGrid(), + ), + + // Continue button + _buildContinueButton(context), + ], + ), + ), + ), + ); + } + + // Build filter chips section + Widget _buildFilterChips() { + return Consumer( + builder: (context, displayProvider, child) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: displayProvider.filterOptions.map((filter) { + final isSelected = filter == displayProvider.activeFilter; + return FilterChipOption( + label: filter, + isSelected: isSelected, + onSelected: () => displayProvider.setFilter(filter), + ); + }).toList(), + ), + ); + }, + ); + } + + // Build display cards grid + Widget _buildDisplayGrid() { + return Consumer( + builder: (context, displayProvider, child) { + final displays = displayProvider.filteredDisplays; + + if (displays.isEmpty) { + return const Center( + child: Text( + 'No displays match the selected filter', + style: TextStyle(color: Colors.grey), + ), + ); + } + + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.6, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + ), + itemCount: displays.length, + itemBuilder: (context, index) { + final display = displays[index]; + final isSelected = index == displayProvider.selectedDisplayIndex; + + return DisplayCard( + display: display, + isSelected: isSelected, + onTap: () => displayProvider.selectDisplay(index), + ); + }, + ); + }, + ); + } + + // Build continue button + Widget _buildContinueButton(BuildContext context) { + return Consumer( + builder: (context, displayProvider, child) { + return SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: colorPrimary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + onPressed: displayProvider.hasSelection + ? () { + // Navigate to image editor with selected display + final selectedDisplay = displayProvider.selectedDisplay!; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ImageEditor( + epd: selectedDisplay.epd, + ), + ), + ); + } + : null, + child: const Text( + 'Continue', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/view/widget/color_dot.dart b/lib/view/widget/color_dot.dart new file mode 100644 index 00000000..089c40e7 --- /dev/null +++ b/lib/view/widget/color_dot.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +/// A widget that displays a colored dot +class ColorDot extends StatelessWidget { + final Color color; + final bool selected; + final double size; + + const ColorDot({ + super.key, + required this.color, + this.selected = false, + this.size = 12.0, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + margin: const EdgeInsets.symmetric(horizontal: 2.0), + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey.shade300, + width: 1.0, + ), + boxShadow: selected + ? [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2.0, + spreadRadius: 1.0, + ) + ] + : null, + ), + ); + } +} diff --git a/lib/view/widget/display_card.dart b/lib/view/widget/display_card.dart new file mode 100644 index 00000000..f80b8451 --- /dev/null +++ b/lib/view/widget/display_card.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/constants.dart'; +import 'package:magic_epaper_app/model/display_model.dart'; +import 'package:magic_epaper_app/view/widget/color_dot.dart'; + +/// A card that displays information about an ePaper display +class DisplayCard extends StatelessWidget { + final DisplayModel display; + final bool isSelected; + final VoidCallback onTap; + + const DisplayCard({ + super.key, + required this.display, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Card( + color: Colors.white, + elevation: isSelected ? 4 : 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: isSelected ? colorPrimary : Colors.grey.shade300, + width: isSelected ? 2 : 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Display image + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(11), + topRight: Radius.circular(11), + ), + child: Container( + width: double.infinity, + color: const Color.fromARGB(255, 255, 255, 255), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Image.asset( + display.imagePath, + fit: BoxFit.contain, + height: 160, + errorBuilder: (context, error, stackTrace) { + print( + 'Error loading image: ${display.imagePath} - $error'); + // Fallback if image is not available + return Center( + child: Icon( + Icons.display_settings, + size: 60, + color: Colors.grey.shade400, + ), + ); + }, + ), + ), + ), + ), + ), + + // Display information + Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Display name and size + Text( + display.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + + const SizedBox(height: 8), + + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Color dots in a row + Row( + children: display.colors + .map((color) => ColorDot(color: color)) + .toList(), + ), + + // Color names below dots + const SizedBox(height: 4), + Text( + display.colorNames, + style: const TextStyle(fontSize: 10), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + + const SizedBox(height: 8), + + // Specs + _buildSpecRow('Model:', display.ModelName), + _buildSpecRow('Resolution:', display.resolution), + _buildSpecRow('Aspect Ratio:', display.aspectRatio), + _buildSpecRow('Driver:', display.driver), + ], + ), + ), + ], + ), + ), + ); + } + + // Helper to build a specification row + Widget _buildSpecRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/widget/filter_chip_option.dart b/lib/view/widget/filter_chip_option.dart new file mode 100644 index 00000000..11810c45 --- /dev/null +++ b/lib/view/widget/filter_chip_option.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/constants.dart'; + +/// A custom filter chip with selectable state +class FilterChipOption extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback onSelected; + + const FilterChipOption({ + super.key, + required this.label, + required this.isSelected, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onSelected, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: isSelected ? colorPrimary : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? Colors.transparent : Colors.grey.shade300, + width: 1, + ), + ), + child: Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : Colors.black87, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index bc2c05c1..e2bcd869 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,3 +43,5 @@ flutter: assets: # Add assets from the images directory to the application. - assets/images/ + - assets/images/displays/ + - assets/displays.json