diff --git a/README.md b/README.md index 17f0113..10854ea 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ GitHub + + Static Badge + ## Description @@ -13,6 +16,8 @@ Let's try it now in your browser [Open Dev](https://jamalianpour.github.io/open-dev) +![OpenDev Dashboard](assets/screenshot/dashboard.png) + ## Table of Contents 1. [Features 🚀](#features) 2. [Screenshots 📷](#screenshots) @@ -24,28 +29,43 @@ Let's try it now in your browser [Open Dev](https://jamalianpour.github.io/open- ## Features 🚀 - **JSON Parser and Converter to YAML:** Parse and show JSON in object viewer to read and search, Easily convert JSON data to YAML format for better readability and use in various applications. + - **XML Parser and Converter to JSON:** Transform XML data into JSON format effortlessly, making it easier to work with in JavaScript and other environments. + - **Cron Parser:** Interpret and validate cron expressions to ensure correct scheduling of automated tasks. + - **Unix Time Converter:** Convert Unix timestamps to human-readable dates and vice versa, simplifying the handling of time data. + - **README Helper and Real-time Viewer:** Create and preview README files in real-time to ensure your documentation looks perfect. + - **Developer News Based on RSS:** Stay updated with the latest developer news through RSS feeds from popular sources. + - **Base64 String/Image Encode/Decode:** Encode and decode Base64 strings and images for data processing and transmission. + - **JWT Debugger:** Decode and debug JSON Web Tokens (JWT) to verify token contents and ensure security it locally without internet connection. + - **Hash Generator:** Generate cryptographic hashes for strings to ensure data integrity and security. + - **Color Converter:** Convert colors between different formats (HEX, RGB, HSL) for design and development purposes. + - **RegExp Tester:** Test and debug regular expressions to ensure they match the intended patterns. + - **Lorem Ipsum Generator:** Generate placeholder text for your projects to fill in design layouts. + - **Password Generator:** Create secure, random passwords to enhance security. + - **QR Code Generator:** Generate QR codes from text or URLs for easy sharing and access. + - **Image Extensions Formatter:** Convert images between different file formats for compatibility and optimization. + - **URL Encode/Decode:** Encode and decode URLs to ensure proper formatting and transmission. +- **UUID Generator/Decoder:** Generate and decode UUIDs (Universally Unique Identifiers) for use in applications that require unique identifiers. + ## Screenshots 📷 Here are some screenshots of Open Dev in action: -![OpenDev Dashboard](assets/screenshot/dashboard.png) - | Hash Generator | JSON Parser and Converter to YAML | | ------------------------------------------------------- | ---------------------------------------------- | | ![Hash Generator](assets/screenshot/Hash.png) | ![JSON Parser](assets/screenshot/json.png) | diff --git a/lib/main.dart b/lib/main.dart index 8b164f3..bcb5016 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart'; import 'package:open_dev/views/base_view.dart'; +import 'package:open_dev/views/size_error_view.dart'; import 'package:window_manager/window_manager.dart'; import 'utils/color_schemes.g.dart'; @@ -58,7 +59,9 @@ class MyApp extends StatelessWidget { darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme), debugShowCheckedModeBanner: false, home: kIsWeb - ? Container(color: Colors.grey[900], child: const BaseView()) + ? MediaQuery.sizeOf(context).width < 800 + ? const SizeErrorView() + : Container(color: Colors.grey[900], child: const BaseView()) : const SafeArea( child: TitlebarSafeArea( child: BaseView(), diff --git a/lib/utils/uuid_utils.dart b/lib/utils/uuid_utils.dart new file mode 100644 index 0000000..9df230e --- /dev/null +++ b/lib/utils/uuid_utils.dart @@ -0,0 +1,85 @@ +import 'package:uuid/uuid.dart'; + +class UuidUtils { + static String generateV4() { + return const Uuid().v4(); + } + + static String generateV5(String namespace, String name) { + return const Uuid().v5(namespace, name); + } + + static String generateV6() { + return const Uuid().v6(); + } + + static String generateV7() { + return const Uuid().v7(); + } + + static String generateV8() { + return const Uuid().v8(); + } + + static String generateV1() { + return const Uuid().v1(); + } + + /// Decodes a UUID and extracts its version, variant, timestamp, clock sequence, and node. + /// + /// - param `String` uuid - The UUID to decode. + /// - return `Tuple` A tuple containing the version, variant, timestamp, clock sequence, and node. + static (int, String, int, int, String) decode(String uuid) { + // Parse the UUID to a list of bytes + List bytes = Uuid.parse(uuid); + + // Extracting information from the UUID + var version = bytes[6] >> 4; // Version is in the 7th byte + var variant = (bytes[8] & 0xC0) >> 6; // Variant is in the 9th byte + + // Extract timestamp (60 bits: 4 bits from byte 6, all bits from byte 7, 5, 4, 3, 2, and 1) + var timestamp = ((bytes[6] & 0x0F) << 56) | + (bytes[7] << 48) | + (bytes[4] << 40) | + (bytes[5] << 32) | + (bytes[0] << 24) | + (bytes[1] << 16) | + (bytes[2] << 8) | + bytes[3]; + + // UUID timestamp is in 100-nanosecond intervals since 1582-10-15 + // Convert to Unix epoch (1970-01-01) + const UUID_EPOCH = 0x01B21DD213814000; + timestamp -= UUID_EPOCH; + var millisecondsSinceEpoch = timestamp ~/ 10000; // Convert to milliseconds + + // Extract clock sequence + var clockSeq = ((bytes[8] & 0x3F) << 8) | bytes[9]; + + // Extract node (MAC address) + var node = [ + bytes[10].toRadixString(16).padLeft(2, '0'), + bytes[11].toRadixString(16).padLeft(2, '0'), + bytes[12].toRadixString(16).padLeft(2, '0'), + bytes[13].toRadixString(16).padLeft(2, '0'), + bytes[14].toRadixString(16).padLeft(2, '0'), + bytes[15].toRadixString(16).padLeft(2, '0') + ].join(':'); + + String variantName = ''; + + if (variant == 0) { + variantName = '0 (NCS backward compatibility)'; + } else if (variant == 1) { + variantName = '1 (RFC 4122/DCE 1.1)'; + } else if (variant == 2) { + variantName = '2 (Microsoft\'s GUIDs)'; + } else if (variant == 3) { + variantName = '3 (Reserved for future use)'; + } else { + variantName = 'Unknown'; + } + + return (version, variantName, millisecondsSinceEpoch, clockSeq, node); + } +} diff --git a/lib/views/base_view.dart b/lib/views/base_view.dart index e5fbaea..5497fac 100644 --- a/lib/views/base_view.dart +++ b/lib/views/base_view.dart @@ -19,6 +19,7 @@ import 'readme_view.dart'; import 'regex_view.dart'; import 'unix_time_view.dart'; import 'url_view.dart'; +import 'uuid_view.dart'; class BaseView extends StatefulWidget { const BaseView({super.key}); @@ -73,7 +74,8 @@ class _BaseViewState extends State { const PasswordView(), const QrView(), const ImageView(), - const UrlView() + const UrlView(), + const UuidView(), ], ), ), diff --git a/lib/views/dashboard_view.dart b/lib/views/dashboard_view.dart index ac0e101..8b71297 100644 --- a/lib/views/dashboard_view.dart +++ b/lib/views/dashboard_view.dart @@ -466,6 +466,39 @@ class DashboardView extends StatelessWidget { ], ), ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + Expanded( + child: Semantics( + button: true, + label: + 'UUID Generator/Decode | Generate and decode UUIDs (Universally Unique Identifiers) for use in applications that require unique identifiers.', + child: DashboardCard( + title: 'UUID Generator/Decode', + description: + 'Generate and decode UUIDs (Universally Unique Identifiers) for use in applications that require unique identifiers.', + icon: const Icon( + CupertinoIcons.underline, + size: 50, + color: Colors.white60, + ), + onTap: () { + sideMenu.changePage(17); + }, + ), + ), + ), + const SizedBox( + width: 16, + ), + const Expanded( + child: SizedBox.shrink(), + ), + ], + ), + ), ], ), ), diff --git a/lib/views/size_error_view.dart b/lib/views/size_error_view.dart new file mode 100644 index 0000000..647662c --- /dev/null +++ b/lib/views/size_error_view.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class SizeErrorView extends StatelessWidget { + const SizeErrorView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset( + 'assets/logo/icon.png', + width: 125, + height: 125, + ), + ), + const Padding( + padding: EdgeInsets.all(16), + child: Text( + 'Sorry but currently Open Dev is not supported on this device 📱. Please use a device with a larger screen 🖥️.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, height: 1.8), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/uuid_view.dart b/lib/views/uuid_view.dart new file mode 100644 index 0000000..248cb4f --- /dev/null +++ b/lib/views/uuid_view.dart @@ -0,0 +1,280 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:open_dev/utils/uuid_utils.dart'; +import 'package:open_dev/widgets/data_widget.dart'; + +import '../widgets/secondary_button.dart'; + +class UuidView extends StatefulWidget { + const UuidView({super.key}); + + @override + State createState() => _UuidViewState(); +} + +class _UuidViewState extends State { + final TextEditingController _input = TextEditingController(); + final TextEditingController _generated = TextEditingController(); + final TextEditingController _generateCount = TextEditingController(); + + String version = '', variant = '', timestamp = '', clockSeq = '', node = ''; + + String uuidType = 'V1'; + + @override + void initState() { + _input.text = UuidUtils.generateV1(); + var result = UuidUtils.decode(_input.text); + version = result.$1.toString(); + variant = result.$2; + timestamp = result.$3.toString(); + clockSeq = result.$4.toString(); + node = result.$5; + super.initState(); + } + + void _decode() { + var result = UuidUtils.decode(_input.text); + version = result.$1.toString(); + variant = result.$2; + timestamp = result.$3.toString(); + clockSeq = result.$4.toString(); + node = result.$5; + setState(() {}); + } + + Padding _buildHeader(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + Text('UUID Generator/Decode', style: Theme.of(context).textTheme.headlineSmall), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const Divider( + indent: 8, + endIndent: 8, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), + child: Row( + children: [ + Expanded( + flex: 9, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('input:'), + const SizedBox( + height: 8, + ), + Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), gapPadding: 0), + isDense: true, + prefixIcon: const Icon(CupertinoIcons.underline), + contentPadding: const EdgeInsets.all(0), + filled: true, + ), + controller: _input, + onSubmitted: (value) { + _decode(); + }, + ), + ), + const SizedBox( + width: 6, + ), + SecondaryButton( + text: 'Decode', + height: 38, + onTap: () { + _decode(); + }, + ) + ], + ), + const SizedBox( + height: 8, + ), + DataWidget( + value: version, + title: 'Version', + width: (MediaQuery.sizeOf(context).width - 250) * 0.41, + maxWidth: 700, + minWidth: 340, + ), + const SizedBox( + height: 8, + ), + DataWidget( + value: variant, + title: 'Variant', + width: (MediaQuery.sizeOf(context).width - 250) * 0.41, + maxWidth: 700, + minWidth: 340, + ), + const SizedBox( + height: 8, + ), + DataWidget( + value: timestamp, + title: 'Created', + width: (MediaQuery.sizeOf(context).width - 250) * 0.41, + maxWidth: 700, + minWidth: 340, + ), + const SizedBox( + height: 8, + ), + DataWidget( + value: clockSeq, + title: 'ClockSeq', + width: (MediaQuery.sizeOf(context).width - 250) * 0.41, + maxWidth: 700, + minWidth: 340, + ), + const SizedBox( + height: 8, + ), + DataWidget( + value: node, + title: 'Node', + width: (MediaQuery.sizeOf(context).width - 250) * 0.41, + maxWidth: 700, + minWidth: 340, + ), + ], + ), + ), + ), + const VerticalDivider(), + Expanded( + flex: 10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text('UUID:'), + const Spacer(), + SizedBox( + width: 50, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).splashColor, + borderRadius: BorderRadius.circular(3), + ), + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: DropdownButtonFormField( + borderRadius: BorderRadius.circular(6), + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + ), + items: ['V1', 'V4', 'V5', 'V6', 'V7', 'V8'].map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + value: uuidType, + onChanged: (value) { + setState(() { + uuidType = value ?? 'V1'; + }); + }, + ), + ), + ), + ), + const SizedBox( + width: 4, + ), + const Text('×'), + const SizedBox( + width: 4, + ), + SizedBox( + width: 50, + child: TextField( + controller: _generateCount, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), gapPadding: 0, borderSide: BorderSide.none), + isCollapsed: true, + contentPadding: const EdgeInsets.all(5), + fillColor: Theme.of(context).splashColor, + filled: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + ), + ), + const SizedBox( + width: 8, + ), + SecondaryButton( + text: 'Generate', + onTap: () { + _generated.clear(); + int count = int.parse(_generateCount.text); + for (int i = 0; i < count; i++) { + if (uuidType == 'V1') _generated.text = '${_generated.text}${UuidUtils.generateV1()}\n'; + if (uuidType == 'V4') _generated.text = '${_generated.text}${UuidUtils.generateV4()}\n'; + if (uuidType == 'V5') { + _generated.text = + '${_generated.text}${UuidUtils.generateV5("00000000-0000-0000-0000-000000000000", "00000000-0000-0000-0000-000000000000")}\n'; + } + if (uuidType == 'V6') _generated.text = '${_generated.text}${UuidUtils.generateV6()}\n'; + if (uuidType == 'V7') _generated.text = '${_generated.text}${UuidUtils.generateV7()}\n'; + if (uuidType == 'V8') _generated.text = '${_generated.text}${UuidUtils.generateV8()}\n'; + setState(() {}); + } + }, + ), + ], + ), + const SizedBox( + height: 6, + ), + Expanded( + child: TextField( + controller: _generated, + decoration: InputDecoration( + filled: true, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), gapPadding: 0), + contentPadding: const EdgeInsets.all(12), + ), + onChanged: (value) {}, + maxLines: 999, + ), + ), + ], + ), + ), + ], + ), + )) + ], + ); + } +} diff --git a/lib/widgets/secondary_button.dart b/lib/widgets/secondary_button.dart index 90d7ca1..ae20d53 100644 --- a/lib/widgets/secondary_button.dart +++ b/lib/widgets/secondary_button.dart @@ -3,12 +3,14 @@ import 'package:flutter/material.dart'; class SecondaryButton extends StatelessWidget { final String text; final Icon? icon; + final double? height; final Function onTap; const SecondaryButton({ Key? key, required this.text, this.icon, required this.onTap, + this.height, }) : super(key: key); @override @@ -17,6 +19,7 @@ class SecondaryButton extends StatelessWidget { onTap: () => onTap(), borderRadius: BorderRadius.circular(8), child: Container( + height: height, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Theme.of(context).colorScheme.primaryContainer, diff --git a/lib/widgets/side_menu_widget.dart b/lib/widgets/side_menu_widget.dart index f7f0329..013a7e0 100644 --- a/lib/widgets/side_menu_widget.dart +++ b/lib/widgets/side_menu_widget.dart @@ -57,7 +57,7 @@ class SideMenuWidget extends StatelessWidget { icon: const Icon(CupertinoIcons.doc_text), ), SideMenuItem( - title: 'Developer news', + title: 'Developer News', onTap: (index, _) { sideMenu.changePage(index); }, @@ -85,7 +85,7 @@ class SideMenuWidget extends StatelessWidget { icon: const Icon(Icons.fingerprint), ), SideMenuItem( - title: 'Color picker', + title: 'Color Picker', onTap: (index, _) { sideMenu.changePage(index); }, @@ -120,19 +120,26 @@ class SideMenuWidget extends StatelessWidget { icon: const Icon(CupertinoIcons.qrcode), ), SideMenuItem( - title: 'Image format', + title: 'Image Format', onTap: (index, _) { sideMenu.changePage(index); }, icon: const Icon(CupertinoIcons.photo_fill), ), SideMenuItem( - title: 'URL parser', + title: 'URL Parser', onTap: (index, _) { sideMenu.changePage(index); }, icon: const Icon(CupertinoIcons.globe), ), + SideMenuItem( + title: 'UUID Generator/Decode', + onTap: (index, _) { + sideMenu.changePage(index); + }, + icon: const Icon(CupertinoIcons.underline), + ), ]; Widget side() { @@ -149,15 +156,17 @@ class SideMenuWidget extends StatelessWidget { header: true, child: Image.asset( 'assets/logo/icon.png', - width: 65, - height: 65, + width: MediaQuery.sizeOf(context).width > 900 ? 65 : 50, + height: MediaQuery.sizeOf(context).width > 900 ? 65 : 50, ), ), - const SizedBox(width: 4), - Text( - 'Open Dev', - style: Theme.of(context).textTheme.headlineLarge, - ), + if (MediaQuery.sizeOf(context).width > 900) ...[ + const SizedBox(width: 4), + Text( + 'Open Dev', + style: Theme.of(context).textTheme.headlineLarge, + ), + ] ], ), ) @@ -173,9 +182,8 @@ class SideMenuWidget extends StatelessWidget { style: SideMenuStyle( itemHeight: 35, openSideMenuWidth: 250, - // backgroundColor: Colors.transparent, - // showHamburger: true, - displayMode: SideMenuDisplayMode.open, + compactSideMenuWidth: 60, + displayMode: MediaQuery.sizeOf(context).width < 900 ? SideMenuDisplayMode.compact : SideMenuDisplayMode.open, selectedTitleTextStyle: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.primary), unselectedTitleTextStyle: const TextStyle(fontSize: 14, color: Colors.white70), selectedColor: Theme.of(context).colorScheme.primaryContainer, diff --git a/pubspec.lock b/pubspec.lock index da59d28..ce0b662 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -629,6 +629,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -749,6 +757,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + url: "https://pub.dev" + source: hosted + version: "4.4.2" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6c77655..705d357 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: open_dev description: "Open source cross-platform developer assistant" -version: 0.5.0+1 +version: 0.5.1+2 environment: sdk: '>=3.2.1 <4.0.0' @@ -35,6 +35,7 @@ dependencies: image: ^4.2.0 url_launcher: ^6.3.0 device_info_plus: ^10.1.0 + uuid: ^4.4.2 dev_dependencies: flutter_test: diff --git a/test/uuid_test.dart b/test/uuid_test.dart new file mode 100644 index 0000000..901c722 --- /dev/null +++ b/test/uuid_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:open_dev/utils/uuid_utils.dart'; + +void main() { + group('UuidUtils.decode', () { + test('should decode a valid UUID', () { + var uuid = UuidUtils.generateV1(); + var result = UuidUtils.decode(uuid); + + expect(result.$1, 1); + expect(result.$2, '2 (Microsoft\'s GUIDs)'); + expect(result.$3 >= 0, true); + expect(result.$4 >= 0, true); + expect(result.$5.split(':').length, 6); + }); + + test('should handle invalid UUIDs', () { + expect(() => UuidUtils.decode('invalid'), throwsA(isA())); + }); + }); +} \ No newline at end of file