├── .gitignore ├── .vscode-test.mjs ├── .vscode ├── extensions.json └── launch.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── eslint.config.mjs ├── extension.js ├── flutter_structure.png ├── generate_feature.dart ├── generate_project.dart ├── icon.jpeg ├── jsconfig.json ├── package-lock.json ├── package.json ├── test └── extension.test.js ├── tool ├── generate_feature.dart └── generate_project.dart └── vsc-extension-quickstart.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "ms-vscode.extension-test-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/jsconfig.json 8 | **/*.map 9 | **/eslint.config.mjs 10 | **/.vscode-test.* 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "lutter-full-structure" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Full Structure Generator 🏗️ 2 | 3 | A powerful Visual Studio Code extension that helps you scaffold **Clean Architecture** for your Flutter apps in seconds. 4 | 5 | This tool creates full project structure or feature modules with proper domain/data/presentation layers, as well as essential shared layers like `core`, `routing`, `theme`, and `utils`. 6 | 7 | --- 8 | 9 | ## ✨ Features 10 | 11 | - ✅ **Generate Full Project**: Clean architecture with: 12 | - `core/` (constants, network, services, utils, theme, routing, cubits) 13 | - `features/` (e.g., splash, onboarding, auth) 14 | - `main.dart`, `app.dart`, `app_bloc_observer.dart` 15 | - Pre-configured `pubspec.yaml` 16 | - `assets/` folders with dummy localization 17 | - ✅ **Generate Feature Only**: Create a new feature module with domain, data, and presentation layers. 18 | - ✅ Automatically runs Dart scripts via terminal 19 | - ✅ Intelligent folder creation 20 | - ✅ Works on any Flutter project 21 | 22 | --- 23 | 24 | ## 🚀 How to Use 25 | 26 | ### 🔹 Generate Full Project Structure 27 | 28 | 1. Open a Flutter project in VS Code 29 | 2. Press `Ctrl + Shift + P` or `Cmd + Shift + P` 30 | 3. Type `Flutter Full Structure: Generate` and hit Enter 31 | 4. Select `Generate Full Project` 32 | 5. Your app structure will be generated instantly 33 | 34 | ### 🔹 Generate a New Feature 35 | 36 | 1. Same as above (`Ctrl + Shift + P`, search `Flutter Full Structure: Generate`) 37 | 2. Select `Generate Feature` 38 | 3. Enter the **feature name** (e.g., `login`, `home`) 39 | 4. It creates: 40 | - `lib/features/feature_name/` 41 | - Organized folders: `domain`, `data`, `presentation` 42 | - Ready-to-code files like: 43 | - `feature_entity.dart` 44 | - `feature_repository.dart` 45 | - `feature_usecase.dart` 46 | - `feature_model.dart` 47 | - Cubit & state classes 48 | - View (page) with `BlocBuilder` 49 | 50 | --- 51 | 52 | ## 📁 Files Structure (Generate Full Project) 53 | 54 | ``` 55 | lib/ 56 | ├── core/ 57 | │ ├── constants 58 | │ ├── network 59 | │ ├── errors 60 | │ ├── utils 61 | │ ├── services 62 | │ ├── routing 63 | │ ├── theme 64 | │ ├── cubit 65 | │ └── extensions 66 | ├── features/ 67 | │ ├── splash 68 | │ ├── onboarding 69 | ├── app.dart 70 | ├── main.dart 71 | └── app_bloc_observer.dart 72 | ``` 73 | 74 | --- 75 | 76 | ## 🔧 Requirements 77 | 78 | - ✅ Dart SDK installed & configured 79 | - ✅ Flutter SDK installed 80 | - ✅ `tool/` directory is auto-created 81 | 82 | --- 83 | 84 | ## 🛠️ Scripts Included 85 | 86 | - `generate_project.dart` - Generates entire project scaffold 87 | - `generate_feature.dart` - Generates a single feature module 88 | 89 | These are automatically copied from the extension's `assets/` folder into `tool/` if they don't exist. 90 | 91 | --- 92 | 93 | ## 🖼 Preview 94 | 95 | ![Preview](icon.jpeg) 96 | 97 | --- 98 | 99 | ## 📦 Installation 100 | 101 | 1. Download the `.vsix` file 102 | 2. Install manually: 103 | ```bash 104 | code --install-extension flutter_clean_arch_gen.vsix 105 | ``` 106 | 3. Or search for `Flutter Full Structure Generator` in VS Code Marketplace (if published) 107 | 108 | --- 109 | 110 | ## 👨‍💻 Author 111 | 112 | Built with ❤️ by **Abdalluh Essam** 🇪🇬 113 | 114 | 📩 Email: abdallhesam100@gmail.com 115 | 116 | --- 117 | 118 | ## 📄 License 119 | 120 | This project is licensed under the MIT License. 121 | 122 | ``` 123 | MIT License 124 | 125 | Copyright (c) 2025 Abdalluh Essam 126 | 127 | Permission is hereby granted, free of charge, to any person obtaining a copy 128 | of this software and associated documentation files (the "Software"), to deal 129 | in the Software without restriction, including without limitation the rights 130 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 131 | copies of the Software, and to permit persons to whom the Software is 132 | furnished to do so, subject to the following conditions: 133 | 134 | The above copyright notice and this permission notice shall be included in all 135 | copies or substantial portions of the Software. 136 | 137 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 138 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 139 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 140 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 141 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 142 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 143 | SOFTWARE. 144 | ``` 145 | 146 | --- 147 | 148 | ## 🙌 Contributions 149 | 150 | Pull requests are welcome! 151 | Feel free to fork the project and open issues for suggestions or improvements. 🙏 152 | 153 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | 3 | export default [{ 4 | files: ["**/*.js"], 5 | languageOptions: { 6 | globals: { 7 | ...globals.commonjs, 8 | ...globals.node, 9 | ...globals.mocha, 10 | }, 11 | 12 | ecmaVersion: 2022, 13 | sourceType: "module", 14 | }, 15 | 16 | rules: { 17 | "no-const-assign": "warn", 18 | "no-this-before-super": "warn", 19 | "no-undef": "warn", 20 | "no-unreachable": "warn", 21 | "no-unused-vars": "warn", 22 | "constructor-super": "warn", 23 | "valid-typeof": "warn", 24 | }, 25 | }]; -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | function activate(context) { 6 | 7 | const disposable = vscode.commands.registerCommand('flutter-full-structure.generate', function () { 8 | const terminal = vscode.window.createTerminal('Flutter Generator'); 9 | const workspaceFolders = vscode.workspace.workspaceFolders; 10 | 11 | if (workspaceFolders) { 12 | const projectPath = workspaceFolders[0].uri.fsPath; 13 | const toolDir = path.join(projectPath, 'tool'); 14 | const dartScriptPath = path.join(toolDir, 'generate_project.dart'); 15 | 16 | if (!fs.existsSync(toolDir)) { 17 | fs.mkdirSync(toolDir, { recursive: true }); 18 | } 19 | 20 | const dartScript = `// tool/generate_project.dart 21 | import 'dart:io'; 22 | import 'package:path/path.dart'; 23 | 24 | void main(List arguments) { 25 | final projectDir = Directory.current; 26 | final projectName = basename(projectDir.path); 27 | final libDir = Directory(join(projectDir.path, 'lib')); 28 | final assetsDir = Directory(join(projectDir.path, 'assets')); 29 | 30 | _generateCoreStructure(libDir); 31 | _generateFeatureStructure(libDir); 32 | _generateMainFiles(libDir, projectName); 33 | _generateAssetsFolder(assetsDir); 34 | _generatePubspecYaml(projectDir, projectName); 35 | 36 | print('\\n✅ Project "\$projectName" structure generated successfully!'); 37 | } 38 | 39 | void _generateCoreStructure(Directory libDir) { 40 | final coreDirs = [ 41 | 'animations', 'constants', 'errors', 'network', 'services', 'utils', 'widgets', 42 | 'theme', 'routing', 'di', 'cubit/locale', 'cubit/theme' 43 | ]; 44 | for (final dir in coreDirs) { 45 | _createDir(Directory(join(libDir.path, 'core', dir))); 46 | } 47 | _createFile(join(libDir.path, 'core', 'di', 'service_locator.dart'), '/* Dependency Injection Setup */'); 48 | _createFile(join(libDir.path, 'core', 'routing', 'app_router.dart'), '/* Routing Setup */'); 49 | _createFile(join(libDir.path, 'core', 'utils', 'app_shared_preferences.dart'), '/* Shared Preferences Utility */'); 50 | _createFile(join(libDir.path, 'app_bloc_opserver.dart'), '/* Bloc Observer Setup */'); 51 | _createFile(join(libDir.path, 'core', 'constants', 'app_constants.dart'), ''' 52 | import 'package:flutter/material.dart'; 53 | class AppConstants { 54 | static const String appName = 'Quick Wash'; 55 | static const supportedLocales = [Locale('en'), Locale('ar')]; 56 | }'''); 57 | _createDir(Directory(join(libDir.path, 'generated'))); 58 | _createFile(join(libDir.path, 'generated', 'assets.dart'), '/* generated assets references */'); 59 | } 60 | 61 | void _generateFeatureStructure(Directory libDir) { 62 | final featuresDir = Directory(join(libDir.path, 'features')); 63 | _createDir(featuresDir); 64 | final modules = ['auth', 'splash', 'onboarding']; 65 | final subDirs = [ 66 | 'data/datasources', 'data/models', 'data/repositories', 67 | 'domain/entities', 'domain/repositories', 'domain/usecases', 68 | 'presentation/bloc', 'presentation/screens', 'presentation/widgets', 69 | ]; 70 | for (final module in modules) { 71 | final moduleDir = Directory(join(featuresDir.path, module)); 72 | for (final dir in subDirs) { 73 | _createDir(Directory(join(moduleDir.path, dir))); 74 | } 75 | _createFile(join(moduleDir.path, 'presentation/screens/${module}_screen.dart'), ''' 76 | import 'package:flutter/material.dart'; 77 | class ${_capitalize(module)}Screen extends StatelessWidget { 78 | const ${_capitalize(module)}Screen({super.key}); 79 | @override 80 | Widget build(BuildContext context) { 81 | return Scaffold( 82 | appBar: AppBar(title: const Text('${_capitalize(module)}')), 83 | body: const Center(child: Text('${_capitalize(module)} content here')), 84 | ); 85 | } 86 | }'''); 87 | } 88 | } 89 | 90 | void _generateMainFiles(Directory libDir, String projectName) { 91 | _createFile(join(libDir.path, 'main.dart'), ''' 92 | import 'package:easy_localization/easy_localization.dart'; 93 | import 'package:flutter/material.dart'; 94 | import 'package:flutter_bloc/flutter_bloc.dart'; 95 | import 'package:$projectName/app.dart'; 96 | import 'app_bloc_opserver.dart'; 97 | import 'core/constants/app_constants.dart'; 98 | import 'core/cubit/Locale/locale_cubit.dart'; 99 | import 'core/cubit/theme/theme_cubit.dart'; 100 | import 'core/routing/app_router.dart'; 101 | import 'core/utils/app_shared_preferences.dart'; 102 | 103 | void main() async { 104 | WidgetsFlutterBinding.ensureInitialized(); 105 | await EasyLocalization.ensureInitialized(); 106 | Bloc.observer = AppBlocObserver(); 107 | await AppPreferences().init(); 108 | runApp( 109 | EasyLocalization( 110 | supportedLocales: AppConstants.supportedLocales, 111 | path: 'assets/lang', 112 | fallbackLocale: const Locale('en'), 113 | startLocale: const Locale('en'), 114 | child: MultiBlocProvider( 115 | providers: [ 116 | BlocProvider(create: (_) => LocaleCubit()), 117 | BlocProvider(create: (_) => ThemeCubit()), 118 | ], 119 | child: QuickWashApp(appRouter: AppRouter()), 120 | ), 121 | ), 122 | ); 123 | }'''); 124 | 125 | _createFile(join(libDir.path, 'app.dart'), ''' 126 | import 'package:easy_localization/easy_localization.dart'; 127 | import 'package:flutter/material.dart'; 128 | import 'package:flutter_bloc/flutter_bloc.dart'; 129 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 130 | import 'core/constants/app_constants.dart'; 131 | import 'core/cubit/theme/theme_cubit.dart'; 132 | import 'core/routing/app_router.dart'; 133 | import 'core/routing/routes.dart'; 134 | import 'core/theme/app_theme.dart'; 135 | 136 | class QuickWashApp extends StatelessWidget { 137 | final AppRouter appRouter; 138 | const QuickWashApp({super.key, required this.appRouter}); 139 | @override 140 | Widget build(BuildContext context) { 141 | return ScreenUtilInit( 142 | designSize: const Size(402, 874), 143 | minTextAdapt: true, 144 | splitScreenMode: true, 145 | builder: (context, child) => BlocBuilder( 146 | builder: (context, themeState) => MaterialApp( 147 | title: AppConstants.appName, 148 | debugShowCheckedModeBanner: false, 149 | theme: AppTheme.lightTheme, 150 | darkTheme: AppTheme.darkTheme, 151 | themeMode: themeState.themeMode, 152 | locale: context.locale, 153 | supportedLocales: context.supportedLocales, 154 | localizationsDelegates: context.localizationDelegates, 155 | initialRoute: Routes.splashScreen, 156 | onGenerateRoute: appRouter.generateRoute, 157 | home: child, 158 | ), 159 | ), 160 | ); 161 | } 162 | }'''); 163 | } 164 | 165 | void _generateAssetsFolder(Directory assetsDir) { 166 | final subDirs = ['images', 'icons', 'lottie', 'lang', 'fonts']; 167 | for (final dir in subDirs) { 168 | _createDir(Directory(join(assetsDir.path, dir))); 169 | } 170 | } 171 | 172 | 173 | void _generatePubspecYaml(Directory root, String name) { 174 | final pubspec = File(join(root.path, 'pubspec.yaml')); 175 | if (!pubspec.existsSync()) return; 176 | 177 | final content = pubspec.readAsStringSync(); 178 | if (!content.contains('flutter_bloc')) { 179 | pubspec.writeAsStringSync(''' 180 | name: $name 181 | description: "A new Flutter project." 182 | publish_to: 'none' 183 | version: 1.0.0+1 184 | 185 | environment: 186 | sdk: ">=3.7.2 <4.0.0" 187 | 188 | dependencies: 189 | flutter: 190 | sdk: flutter 191 | flutter_bloc: ^9.1.0 192 | dio: ^5.8.0+1 193 | shared_preferences: ^2.2.2 194 | easy_localization: ^3.0.7+1 195 | intl: 196 | equatable: ^2.0.7 197 | get_it: ^8.0.3 198 | cached_network_image: ^3.4.1 199 | flutter_screenutil: ^5.9.3 200 | flutter_animate: ^4.5.2 201 | freezed_annotation: ^3.0.0 202 | json_annotation: ^4.9.0 203 | flutter_native_splash: ^2.4.5 204 | animate_do: ^4.2.0 205 | lottie: ^3.3.1 206 | google_fonts: ^6.2.1 207 | flutter_launcher_icons: ^0.14.3 208 | animator: ^3.3.0 209 | dartz: ^0.10.1 210 | flutter_svg: ^2.0.7 211 | cupertino_icons: ^1.0.8 212 | 213 | dev_dependencies: 214 | flutter_test: 215 | sdk: flutter 216 | build_runner: ^2.4.6 217 | freezed: ^3.0.4 218 | json_serializable: ^6.6.2 219 | bloc_test: ^10.0.0 220 | mockito: ^5.3.2 221 | flutter_lints: ^2.0.1 222 | 223 | flutter: 224 | uses-material-design: true 225 | generate: true 226 | assets: 227 | - assets/images/ 228 | - assets/icons/ 229 | - assets/lottie/ 230 | - assets/lang/ 231 | fonts: 232 | - family: Nunito 233 | fonts: 234 | - asset: assets/fonts/Nunito-Bold.ttf 235 | - asset: assets/fonts/Nunito-ExtraBold.ttf 236 | - asset: assets/fonts/Nunito-Regular.ttf 237 | weight: 700 238 | 239 | flutter_native_splash: 240 | color: "#FFFFFF" 241 | image: assets/images/glusfamily_logo.png 242 | android_12: 243 | image: assets/images/glusfamily_logo.png 244 | color: "#FFFFFF" 245 | '''); 246 | print('📄 pubspec.yaml updated'); 247 | } 248 | } 249 | 250 | void _createDir(Directory dir) { 251 | if (!dir.existsSync()) { 252 | dir.createSync(recursive: true); 253 | print('📁 Created directory: \${dir.path}'); 254 | } 255 | } 256 | 257 | void _createFile(String path, String content) { 258 | final file = File(path); 259 | if (!file.existsSync()) { 260 | file.writeAsStringSync(content); 261 | print('📄 Created file: $path'); 262 | } 263 | } 264 | 265 | String _capitalize(String input) => input[0].toUpperCase() + input.substring(1); 266 | `; 267 | 268 | fs.writeFileSync(dartScriptPath, dartScript); 269 | terminal.show(); 270 | terminal.sendText('dart run tool/generate_project.dart'); 271 | } else { 272 | vscode.window.showErrorMessage('No Flutter workspace folder found!'); 273 | } 274 | }); 275 | 276 | context.subscriptions.push(disposable); 277 | } 278 | 279 | function deactivate() {} 280 | 281 | module.exports = { 282 | activate, 283 | deactivate 284 | }; 285 | 286 | function _capitalize(input) { 287 | if (typeof input !== 'string' || input.length === 0) { 288 | return input; // في حال كان المدخل غير نصي أو فارغ، يرجع المدخل كما هو 289 | } 290 | return input.charAt(0).toUpperCase() + input.slice(1); // استخدام charAt للوصول إلى أول حرف 291 | } -------------------------------------------------------------------------------- /flutter_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbdalluhEssam/flutter_full_structure/083ecc866cd290fe51bf4772f706803fba0f7a6c/flutter_structure.png -------------------------------------------------------------------------------- /generate_feature.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | void main(List arguments) { 4 | // التأكد من أن المستخدم قد أدخل اسم الـ feature 5 | if (arguments.isEmpty) { 6 | print('❌ رجاءً اكتب اسم الـ feature:'); 7 | print('مثال: dart tool/generate_feature.dart login'); 8 | return; 9 | } 10 | 11 | final featureName = arguments.first.toLowerCase(); 12 | final className = capitalize(featureName); 13 | final basePath = 'lib/features/$featureName'; 14 | 15 | // قائمة المجلدات التي سيتم إنشاؤها 16 | final folders = [ 17 | '$basePath/domain/entities', 18 | '$basePath/domain/repositories', 19 | '$basePath/domain/usecases', 20 | '$basePath/data/models', 21 | '$basePath/data/datasources', 22 | '$basePath/data/repositories', 23 | '$basePath/presentation/cubit', 24 | '$basePath/presentation/pages', 25 | ]; 26 | 27 | // إنشاء المجلدات المطلوبة 28 | for (final folder in folders) { 29 | Directory(folder).createSync(recursive: true); 30 | } 31 | 32 | // إنشاء الطبقات المختلفة: Domain, Data, Presentation 33 | 34 | // Domain Layer 35 | _createDomainFiles(basePath, featureName, className); 36 | 37 | // Data Layer 38 | _createDataFiles(basePath, featureName, className); 39 | 40 | // Presentation Layer 41 | _createPresentationFiles(basePath, featureName, className); 42 | 43 | print('✅ تم إنشاء الـ Feature "$featureName" مع الهيكل الكامل.'); 44 | } 45 | 46 | void _createDomainFiles(String basePath, String featureName, String className) { 47 | // إنشاء الملفات الخاصة بالـ Domain Layer 48 | File('$basePath/domain/entities/${featureName}_entity.dart') 49 | .writeAsStringSync(''' 50 | class ${className}Entity { 51 | // TODO: Define entity fields 52 | } 53 | '''); 54 | 55 | File('$basePath/domain/repositories/${featureName}_repository.dart') 56 | .writeAsStringSync(''' 57 | abstract class ${className}Repository { 58 | // TODO: Define repository methods 59 | } 60 | '''); 61 | 62 | File('$basePath/domain/usecases/${featureName}_usecase.dart') 63 | .writeAsStringSync(''' 64 | import '../repositories/${featureName}_repository.dart'; 65 | 66 | class ${className}UseCase { 67 | final ${className}Repository repository; 68 | 69 | ${className}UseCase(this.repository); 70 | 71 | // TODO: Implement call logic 72 | } 73 | '''); 74 | } 75 | 76 | void _createDataFiles(String basePath, String featureName, String className) { 77 | // إنشاء الملفات الخاصة بالـ Data Layer 78 | File('$basePath/data/models/${featureName}_model.dart').writeAsStringSync(''' 79 | import '../../domain/entities/${featureName}_entity.dart'; 80 | 81 | class ${className}Model extends ${className}Entity { 82 | ${className}Model() : super(); 83 | 84 | factory ${className}Model.fromJson(Map json) { 85 | // TODO: Map JSON to model 86 | return ${className}Model(); 87 | } 88 | } 89 | '''); 90 | 91 | File('$basePath/data/datasources/${featureName}_remote_datasource.dart') 92 | .writeAsStringSync(''' 93 | abstract class ${className}RemoteDataSource { 94 | // TODO: Define methods like login(email, password) 95 | } 96 | '''); 97 | 98 | File('$basePath/data/repositories/${featureName}_repository_impl.dart') 99 | .writeAsStringSync(''' 100 | import '../../domain/repositories/${featureName}_repository.dart'; 101 | import '../datasources/${featureName}_remote_datasource.dart'; 102 | 103 | class ${className}RepositoryImpl implements ${className}Repository { 104 | final ${className}RemoteDataSource remoteDataSource; 105 | 106 | ${className}RepositoryImpl(this.remoteDataSource); 107 | 108 | // TODO: Implement repository logic 109 | } 110 | '''); 111 | } 112 | 113 | void _createPresentationFiles( 114 | String basePath, String featureName, String className) { 115 | // إنشاء الملفات الخاصة بالـ Presentation Layer: Cubit و Page 116 | File('$basePath/presentation/cubit/${featureName}_state.dart') 117 | .writeAsStringSync(''' 118 | abstract class ${className}State {} 119 | 120 | class ${className}Initial extends ${className}State {} 121 | 122 | class ${className}Loading extends ${className}State {} 123 | 124 | class ${className}Success extends ${className}State { 125 | // final result; 126 | // Success(this.result); 127 | } 128 | 129 | class ${className}Failure extends ${className}State { 130 | final String error; 131 | ${className}Failure(this.error); 132 | } 133 | '''); 134 | 135 | File('$basePath/presentation/cubit/${featureName}_cubit.dart') 136 | .writeAsStringSync(''' 137 | import 'package:flutter_bloc/flutter_bloc.dart'; 138 | import '${featureName}_state.dart'; 139 | 140 | class ${className}Cubit extends Cubit<${className}State> { 141 | ${className}Cubit() : super(${className}Initial()); 142 | 143 | Future doSomething() async { 144 | emit(${className}Loading()); 145 | try { 146 | // Call usecase 147 | // emit(${className}Success(result)); 148 | } catch (e) { 149 | emit(${className}Failure(e.toString())); 150 | } 151 | } 152 | } 153 | '''); 154 | 155 | File('$basePath/presentation/pages/${featureName}_page.dart') 156 | .writeAsStringSync(''' 157 | import 'package:flutter/material.dart'; 158 | import 'package:flutter_bloc/flutter_bloc.dart'; 159 | import '../cubit/${featureName}_cubit.dart'; 160 | import '../cubit/${featureName}_state.dart'; 161 | 162 | class ${className}Page extends StatelessWidget { 163 | const ${className}Page({super.key}); 164 | 165 | @override 166 | Widget build(BuildContext context) { 167 | return BlocProvider( 168 | create: (_) => ${className}Cubit(), 169 | child: Scaffold( 170 | appBar: AppBar(title: const Text('$className Page')), 171 | body: BlocBuilder<${className}Cubit, ${className}State>( 172 | builder: (context, state) { 173 | if (state is ${className}Loading) { 174 | return const Center(child: CircularProgressIndicator()); 175 | } else if (state is ${className}Failure) { 176 | return Center(child: Text('Error: \${state.error}')); 177 | } 178 | return const Center(child: Text('$className Page')); 179 | }, 180 | ), 181 | ), 182 | ); 183 | } 184 | } 185 | '''); 186 | } 187 | 188 | // وظيفة لكتابة أول حرف بحروف كبيرة 189 | String capitalize(String s) => s[0].toUpperCase() + s.substring(1); 190 | -------------------------------------------------------------------------------- /generate_project.dart: -------------------------------------------------------------------------------- 1 | // سكربت Dart احترافي لتوليد مشروع Flutter كامل بترتيب Clean Architecture 2 | // يحتوي على ملفات core, features, main app مع كود فعلي داخل كل ملف 3 | import 'dart:io'; 4 | import 'package:path/path.dart'; 5 | 6 | void main(List arguments) async { 7 | final projectDir = Directory.current; 8 | final projectName = basename(projectDir.path); 9 | final libDir = Directory(join(projectDir.path, 'lib')); 10 | 11 | await _generateCore(libDir, projectName); 12 | await _generateFeatures(libDir, projectName); 13 | await _generateMainApp(libDir, projectName); 14 | await _generateAssetsFolders(projectDir); 15 | await _generateLocalizationFiles(projectDir); 16 | await _generateReadme(projectDir, projectName); 17 | await _generatePubspecYaml(projectDir, projectName); 18 | 19 | print('\n✅ Flutter project is fully ready with Clean Architecture 🇪🇬'); 20 | } 21 | 22 | Future _generateAssetsFolders(Directory projectDir) async { 23 | final assetDirs = [ 24 | 'assets/images', 25 | 'assets/icons', 26 | 'assets/lottie', 27 | 'assets/lang', 28 | ]; 29 | for (final dir in assetDirs) { 30 | await Directory(join(projectDir.path, dir)).create(recursive: true); 31 | } 32 | } 33 | 34 | Future _generateLocalizationFiles(Directory projectDir) async { 35 | final langDir = Directory(join(projectDir.path, 'assets/lang')); 36 | 37 | await File(join(langDir.path, 'en.json')).writeAsString('''{ 38 | "appName": "My App", 39 | "welcome": "Welcome", 40 | "login": "Login", 41 | "signup": "Sign Up" 42 | }'''); 43 | 44 | await File(join(langDir.path, 'ar.json')).writeAsString('''{ 45 | "appName": "تطبيقي", 46 | "welcome": "مرحبًا", 47 | "login": "تسجيل الدخول", 48 | "signup": "إنشاء حساب" 49 | }'''); 50 | } 51 | 52 | Future _generateReadme(Directory projectDir, String projectName) async { 53 | final readmeFile = File(join(projectDir.path, 'README.md')); 54 | await readmeFile.writeAsString(''' 55 | Hello Created By Abdalluh Essam 🇪🇬🇪🇬🇪 56 | abdallhesam100@gmail.com 57 | 58 | 59 | 60 | 61 | # $projectName 62 | 63 | 64 | 🚀 Clean Architecture Flutter Project Generated Automatically 65 | 66 | 67 | ## Structure 68 | 69 | 70 | ``` 71 | lib/ 72 | ├── core/ 73 | │ ├── constants 74 | │ ├── network 75 | │ ├── errors 76 | │ ├── utils 77 | │ ├── services 78 | │ ├── routing 79 | │ ├── theme 80 | │ ├── cubit 81 | │ └── extensions 82 | ├── features/ 83 | │ ├── splash 84 | │ ├── onboarding 85 | │ ├── auth 86 | │ └── home 87 | ├── app.dart 88 | ├── main.dart 89 | └── app_bloc_observer.dart 90 | ``` 91 | 92 | 93 | ## Getting Started 94 | ```bash 95 | flutter pub get 96 | flutter run 97 | ``` 98 | 99 | 100 | --- 101 | 102 | 103 | ✅ Built with ❤️ using the Clean Architecture Generator 104 | 105 | 106 | Hello Created By Abdalluh Essam 🇪🇬🇪🇬🇪 107 | '''); 108 | } 109 | 110 | Future _generateCore(Directory libDir, String projectName) async { 111 | final coreDir = Directory(join(libDir.path, 'core')); 112 | final structure = { 113 | 'constants': { 114 | 'app_constants.dart': _appConstantsCode(), 115 | 'endpoint_constants.dart': _endpointConstantsCode(), 116 | 'strings_constants.dart': _stringsConstantsCode(), 117 | }, 118 | 'network': { 119 | 'api_consumer.dart': _apiConsumerCode(), 120 | 'dio_consumer.dart': _dioConsumerCode(), 121 | 'interceptors.dart': _interceptorsCode(), 122 | 'status_code.dart': _statusCodeCode(), 123 | }, 124 | 'errors': { 125 | 'exceptions.dart': _exceptionsCode(), 126 | 'failures.dart': _failuresCode(), 127 | }, 128 | 'utils': { 129 | 'app_utils.dart': _appUtilsCode(), 130 | 'app_shared_preferences.dart': _appPrefsCode(), 131 | }, 132 | 'services': { 133 | 'locale_service.dart': _localeServiceCode(), 134 | 'theme_service.dart': _themeServiceCode(), 135 | }, 136 | 'routing': { 137 | 'app_router.dart': _appRouterCode(projectName), 138 | 'routes.dart': _routesCode(), 139 | }, 140 | 'theme': { 141 | 'app_theme.dart': _appThemeCode(), 142 | 'app_colors.dart': _appColorsCode(), 143 | }, 144 | 'cubit/locale': { 145 | 'locale_cubit.dart': _localeCubitCode(), 146 | 'locale_state.dart': _localeStateCode(), 147 | }, 148 | 'cubit/theme': { 149 | 'theme_cubit.dart': _themeCubitCode(), 150 | 'theme_state.dart': _themeStateCode(), 151 | }, 152 | 'extensions': { 153 | 'navigation_extensions.dart': _navigationExtCode(), 154 | 'sizedbox_extensions.dart': _sizedBoxExtCode(), 155 | 'theme_extensions.dart': _themeExtCode(), 156 | }, 157 | }; 158 | 159 | for (final entry in structure.entries) { 160 | final dir = Directory(join(coreDir.path, entry.key)); 161 | await dir.create(recursive: true); 162 | for (final fileEntry in entry.value.entries) { 163 | final filePath = join(dir.path, fileEntry.key); 164 | await File(filePath).writeAsString(fileEntry.value); 165 | } 166 | } 167 | } 168 | 169 | Future _generateFeatures(Directory libDir, String projectName) async { 170 | final featuresDir = Directory(join(libDir.path, 'features')); 171 | await featuresDir.create(recursive: true); 172 | 173 | final splashDir = Directory( 174 | join(featuresDir.path, 'splash/presentation/screens'), 175 | ); 176 | final onboardingDir = Directory( 177 | join(featuresDir.path, 'onboarding/presentation/screens'), 178 | ); 179 | 180 | await splashDir.create(recursive: true); 181 | await onboardingDir.create(recursive: true); 182 | 183 | await File( 184 | join(splashDir.path, 'splash_screen.dart'), 185 | ).writeAsString(_splashScreenCode(projectName)); 186 | 187 | await File( 188 | join(onboardingDir.path, 'onboarding_screen.dart'), 189 | ).writeAsString(_onboardingScreenCode(projectName)); 190 | } 191 | 192 | Future _generateMainApp(Directory libDir, String projectName) async { 193 | await File(join(libDir.path, 'main.dart')).writeAsString(_mainCode()); 194 | await File(join(libDir.path, 'app.dart')).writeAsString(_appCode()); 195 | await File( 196 | join(libDir.path, 'app_bloc_observer.dart'), 197 | ).writeAsString(_blocObserverCode()); 198 | } 199 | 200 | Future _generatePubspecYaml( 201 | Directory projectDir, 202 | String projectName, 203 | ) async { 204 | final flutterCmd = Platform.isWindows ? 'flutter.bat' : 'flutter'; 205 | 206 | late final String flutterVersionOutput; 207 | try { 208 | final flutterVersionResult = await Process.run(flutterCmd, ['--version']); 209 | flutterVersionOutput = flutterVersionResult.stdout.toString(); 210 | } catch (e) { 211 | stderr.writeln( 212 | '❌ فشل في تشغيل flutter --version. تأكد أن Flutter موجود في PATH.'); 213 | rethrow; 214 | } 215 | 216 | // استخراج إصدار Dart باستخدام RegExp 217 | final sdkMatch = RegExp(r'Dart\sSDK\sversion:\s(\d+\.\d+\.\d+)') 218 | .firstMatch(flutterVersionOutput); 219 | final dartVersion = sdkMatch != null ? sdkMatch.group(1)! : '3.0.0'; 220 | final dartVersionClean = dartVersion.replaceAll('^', ''); 221 | 222 | // استخراج إصدار Flutter 223 | final flutterMatch = 224 | RegExp(r'Flutter\s(\d+\.\d+\.\d+)').firstMatch(flutterVersionOutput); 225 | final flutterVersion = 226 | flutterMatch != null ? flutterMatch.group(1)! : 'unknown'; 227 | 228 | final pubspecContent = ''' 229 | name: $projectName 230 | description: A new Flutter project with Clean Architecture by Abdalluh Essam 231 | # Flutter version on machine: $flutterVersion 232 | publish_to: 'none' 233 | 234 | environment: 235 | sdk: '>=$dartVersionClean <4.0.0' 236 | 237 | dependencies: 238 | flutter: 239 | sdk: flutter 240 | flutter_bloc: 241 | dio: 242 | shared_preferences: 243 | easy_localization: 244 | intl: 245 | equatable: 246 | get_it: 247 | cached_network_image: 248 | flutter_screenutil: 249 | flutter_animate: 250 | freezed_annotation: 251 | json_annotation: 252 | flutter_native_splash: 253 | animate_do: 254 | lottie: 255 | google_fonts: 256 | flutter_launcher_icons: 257 | animator: 258 | dartz: 259 | flutter_svg: 260 | cupertino_icons: 261 | 262 | dev_dependencies: 263 | flutter_test: 264 | sdk: flutter 265 | build_runner: 266 | freezed: 267 | json_serializable: 268 | bloc_test: 269 | mockito: 270 | flutter_lints: 271 | 272 | flutter: 273 | uses-material-design: true 274 | generate: true 275 | assets: 276 | - assets/images/ 277 | - assets/icons/ 278 | - assets/lottie/ 279 | - assets/lang/ 280 | '''; 281 | 282 | final file = File(join(projectDir.path, 'pubspec.yaml')); 283 | await file.writeAsString(pubspecContent); 284 | 285 | print('📄 تم إنشاء ملف pubspec.yaml بنجاح!'); 286 | } 287 | 288 | // ============================ الأكواد ============================ 289 | 290 | String _appConstantsCode() => ''' 291 | import 'dart:ui'; 292 | 293 | 294 | class AppConstants { 295 | static const String appName = 'My App'; 296 | static const List supportedLocales = [Locale('en'), Locale('ar')]; 297 | static const String localeKey = 'app_locale'; 298 | static const String themeKey = 'app_theme'; 299 | } 300 | '''; 301 | 302 | String _endpointConstantsCode() => ''' 303 | class EndpointConstants { 304 | static const String baseUrl = 'https://api.example.com/v1'; 305 | // Add your endpoints here 306 | } 307 | '''; 308 | 309 | String _stringsConstantsCode() => ''' 310 | class StringsConstants { 311 | static const Map en = { 312 | 'appName': 'My App', 313 | }; 314 | 315 | 316 | static const Map ar = { 317 | 'appName': 'تطبيقي', 318 | }; 319 | } 320 | '''; 321 | 322 | String _apiConsumerCode() => ''' 323 | abstract class ApiConsumer { 324 | Future get(String path, {Map? queryParameters}); 325 | Future post(String path, {Map? body}); 326 | Future put(String path, {Map? body}); 327 | Future delete(String path); 328 | } 329 | '''; 330 | 331 | String _dioConsumerCode() => ''' 332 | import 'package:dio/dio.dart'; 333 | import '../constants/endpoint_constants.dart'; 334 | import 'api_consumer.dart'; 335 | import 'interceptors.dart'; 336 | import 'status_code.dart'; 337 | 338 | 339 | class DioConsumer implements ApiConsumer { 340 | final Dio client; 341 | 342 | 343 | DioConsumer({required this.client}) { 344 | client.options 345 | ..baseUrl = EndpointConstants.baseUrl 346 | ..responseType = ResponseType.json 347 | ..connectTimeout = const Duration(seconds: 15) 348 | ..receiveTimeout = const Duration(seconds: 15); 349 | 350 | 351 | client.interceptors.add(AppInterceptors()); 352 | } 353 | 354 | 355 | @override 356 | Future get(String path, {Map? queryParameters}) async { 357 | try { 358 | final response = await client.get(path, queryParameters: queryParameters); 359 | return response.data; 360 | } on DioException catch (error) { 361 | _handleDioError(error); 362 | } 363 | } 364 | 365 | 366 | // Implement other methods (post, put, delete) 367 | 368 | 369 | void _handleDioError(DioException error) { 370 | // Handle different error types 371 | } 372 | 373 | 374 | @override 375 | Future delete(String path) { 376 | // TODO: implement delete 377 | throw UnimplementedError(); 378 | } 379 | 380 | 381 | @override 382 | Future post(String path, {Map? body}) { 383 | // TODO: implement post 384 | throw UnimplementedError(); 385 | } 386 | 387 | 388 | @override 389 | Future put(String path, {Map? body}) { 390 | // TODO: implement put 391 | throw UnimplementedError(); 392 | } 393 | } 394 | 395 | 396 | '''; 397 | 398 | String _interceptorsCode() => ''' 399 | import 'package:dio/dio.dart'; 400 | 401 | 402 | class AppInterceptors extends Interceptor { 403 | @override 404 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) { 405 | // Add headers or tokens if needed 406 | super.onRequest(options, handler); 407 | } 408 | 409 | 410 | @override 411 | void onResponse(Response response, ResponseInterceptorHandler handler) { 412 | super.onResponse(response, handler); 413 | } 414 | 415 | 416 | @override 417 | void onError(DioException err, ErrorInterceptorHandler handler) { 418 | super.onError(err, handler); 419 | } 420 | } 421 | '''; 422 | 423 | String _statusCodeCode() => ''' 424 | class StatusCode { 425 | static const int ok = 200; 426 | static const int badRequest = 400; 427 | static const int unauthorized = 401; 428 | static const int forbidden = 403; 429 | static const int notFound = 404; 430 | static const int conflict = 409; 431 | static const int internalServerError = 500; 432 | } 433 | '''; 434 | 435 | String _exceptionsCode() => ''' 436 | abstract class AppException implements Exception { 437 | final String message; 438 | final int statusCode; 439 | 440 | 441 | const AppException(this.message, this.statusCode); 442 | } 443 | 444 | 445 | class ServerException extends AppException { 446 | const ServerException([super.message = 'Server Error', super.statusCode = 500]); 447 | } 448 | 449 | 450 | class CacheException extends AppException { 451 | const CacheException([super.message = 'Cache Error', super.statusCode = 500]); 452 | } 453 | // Add more exceptions as needed 454 | 455 | 456 | '''; 457 | 458 | String _failuresCode() => ''' 459 | abstract class Failure { 460 | final String message; 461 | final int statusCode; 462 | 463 | 464 | Failure(this.message, this.statusCode); 465 | } 466 | 467 | 468 | class ServerFailure extends Failure { 469 | ServerFailure([super.message = 'Server Error', super.statusCode = 500]); 470 | } 471 | 472 | 473 | class CacheFailure extends Failure { 474 | CacheFailure([super.message = 'Cache Error', super.statusCode = 500]); 475 | } 476 | // Add more failures as needed 477 | 478 | 479 | '''; 480 | 481 | String _appUtilsCode() => ''' 482 | class AppUtils { 483 | static bool isEmailValid(String email) { 484 | return RegExp(r'^[^@]+@[^@]+.[^@]+').hasMatch(email); 485 | } 486 | 487 | 488 | static bool hasLowerCase(String password) { 489 | return RegExp(r'^(?=.*[a-z])').hasMatch(password); 490 | } 491 | 492 | 493 | static bool hasUpperCase(String password) { 494 | return RegExp(r'^(?=.*[A-Z])').hasMatch(password); 495 | } 496 | 497 | 498 | static bool hasNumber(String password) { 499 | return RegExp(r'^(?=.*?[0-9])').hasMatch(password); 500 | } 501 | 502 | 503 | static bool hasMinLength(String password) { 504 | return RegExp(r'^(?=.{8,})').hasMatch(password); 505 | } 506 | } 507 | 508 | 509 | '''; 510 | 511 | String _appPrefsCode() => ''' 512 | import 'package:shared_preferences/shared_preferences.dart'; 513 | import 'dart:convert'; 514 | 515 | 516 | class AppPreferences { 517 | static final AppPreferences _instance = AppPreferences._internal(); 518 | late SharedPreferences _prefs; 519 | 520 | 521 | factory AppPreferences() { 522 | return _instance; 523 | } 524 | 525 | 526 | AppPreferences._internal(); 527 | 528 | 529 | /// ✅ **تهيئة SharedPreferences** 530 | Future init() async { 531 | _prefs = await SharedPreferences.getInstance(); 532 | } 533 | 534 | 535 | /// ✅ **حفظ البيانات بأي نوع (`String, int, double, bool, List`)** 536 | Future setData(String key, dynamic value) async { 537 | if (value is String) { 538 | await _prefs.setString(key, value); 539 | } else if (value is int) { 540 | await _prefs.setInt(key, value); 541 | } else if (value is double) { 542 | await _prefs.setDouble(key, value); 543 | } else if (value is bool) { 544 | await _prefs.setBool(key, value); 545 | } else if (value is List) { 546 | await _prefs.setStringList(key, value); 547 | } else { 548 | throw Exception("Unsupported data type"); 549 | } 550 | } 551 | 552 | 553 | /// ✅ **استرجاع البيانات (`String, int, double, bool, List`)** 554 | dynamic getData(String key) { 555 | return _prefs.get(key); 556 | } 557 | 558 | 559 | /// ✅ **حذف بيانات مفتاح معين** 560 | Future removeData(String key) async { 561 | await _prefs.remove(key); 562 | } 563 | 564 | 565 | /// ✅ **حفظ كائن Model (`T`)** 566 | Future saveModel(String key, T model, Map Function(T) toJson) async { 567 | final String jsonString = jsonEncode(toJson(model)); 568 | await _prefs.setString(key, jsonString); 569 | } 570 | 571 | 572 | /// ✅ **استرجاع كائن Model (`T`)** 573 | T? getModel(String key, T Function(Map) fromJson) { 574 | final String? jsonString = _prefs.getString(key); 575 | if (jsonString != null) { 576 | final Map jsonMap = jsonDecode(jsonString); 577 | return fromJson(jsonMap); 578 | } 579 | return null; 580 | } 581 | 582 | 583 | /// ✅ **حفظ قائمة من النماذج (`List`)** 584 | Future saveModels(String key, List models, Map Function(T) toJson) async { 585 | final List jsonList = models.map((model) => jsonEncode(toJson(model))).toList(); 586 | await _prefs.setStringList(key, jsonList); 587 | } 588 | 589 | 590 | /// ✅ **استرجاع قائمة من النماذج (`List`)** 591 | List getModels(String key, T Function(Map) fromJson) { 592 | final List? jsonList = _prefs.getStringList(key); 593 | if (jsonList != null) { 594 | return jsonList.map((json) => fromJson(jsonDecode(json))).toList(); 595 | } 596 | return []; 597 | } 598 | 599 | 600 | Future clearExceptCredentials() async { 601 | // حفظ بيانات تسجيل الدخول قبل الحذف 602 | String? savedEmail = _prefs.getString('saved_email'); 603 | String? savedPassword = _prefs.getString('saved_password'); 604 | bool? rememberMe = _prefs.getBool('remember_me'); 605 | 606 | 607 | // مسح كل البيانات 608 | await _prefs.clear(); 609 | 610 | 611 | // استرجاع بيانات تسجيل الدخول 612 | if (savedEmail != null) await _prefs.setString('saved_email', savedEmail); 613 | if (savedPassword != null) await _prefs.setString('saved_password', savedPassword); 614 | if (rememberMe != null) await _prefs.setBool('remember_me', rememberMe); 615 | } 616 | bool isLoggedInUser() { 617 | return _prefs.containsKey("userModel"); 618 | } 619 | 620 | 621 | } 622 | '''; 623 | 624 | String _localeServiceCode() => ''' 625 | import 'package:flutter/material.dart'; 626 | import '../constants/app_constants.dart'; 627 | import 'package:easy_localization/easy_localization.dart'; 628 | 629 | 630 | import '../utils/app_shared_preferences.dart'; 631 | 632 | 633 | class LocaleService { 634 | 635 | 636 | LocaleService(); 637 | 638 | 639 | static const _defaultLocale = Locale('en'); 640 | 641 | 642 | /// Load saved locale or fallback 643 | Locale getCurrentLocale() { 644 | 645 | 646 | final localeCode = AppPreferences().getData(AppConstants.localeKey); 647 | if (localeCode != null) { 648 | return Locale(localeCode); 649 | } 650 | return _defaultLocale; 651 | } 652 | 653 | 654 | /// Save locale and update easy_localization 655 | Future setLocale(BuildContext context, String languageCode) async { 656 | await AppPreferences().setData(AppConstants.localeKey, languageCode); 657 | await context.setLocale(Locale(languageCode)); 658 | } 659 | } 660 | 661 | 662 | '''; 663 | 664 | String _themeServiceCode() => ''' 665 | import 'package:flutter/material.dart'; 666 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 667 | import '../theme/app_colors.dart'; 668 | 669 | 670 | 671 | 672 | class AppTheme { 673 | 674 | 675 | 676 | 677 | // 🌓 الوضع الفاتح 678 | static ThemeData get lightTheme { 679 | return ThemeData( 680 | brightness: Brightness.light, 681 | primaryColor: AppColor.primaryColor, 682 | scaffoldBackgroundColor: AppColor.backgroundColor, 683 | fontFamily: 'Nunito', 684 | colorScheme: ColorScheme( 685 | primary: AppColor.primaryColor, // اللون الثاني 686 | secondary: Colors.green, // اللون الثانوي الثاني 687 | surface: Colors.white, // خلفية التطبيقات 688 | error: Colors.red, // اللون الخاص بالأخطاء 689 | onPrimary: Colors.white, // اللون عند استخدام الـ primary 690 | onSecondary: Colors.black, // اللون عند استخدام الـ secondary 691 | onSurface: AppColor.grayColor, // اللون عند استخدام الـ background 692 | onError: Colors.white, // اللون عند استخدام الـ error 693 | brightness: Brightness.light, // مستوى السطوع (فاتح أو غامق) 694 | ), 695 | appBarTheme: const AppBarTheme( 696 | elevation: 0, 697 | centerTitle: true, 698 | backgroundColor: Colors.white, 699 | iconTheme: IconThemeData(color: Colors.black), 700 | titleTextStyle: TextStyle( 701 | color: Colors.black, 702 | fontSize: 18, 703 | fontWeight: FontWeight.bold, 704 | ), 705 | ), 706 | textTheme: TextTheme( 707 | displayLarge: textStyle(24.sp, FontWeight.bold, AppColor.black), 708 | displayMedium: textStyle(20.sp, FontWeight.bold, AppColor.black), 709 | displaySmall: textStyle(18.sp, FontWeight.w600, AppColor.black), 710 | headlineMedium: textStyle(16.sp, FontWeight.bold, AppColor.black), 711 | bodyLarge: textStyle(14.sp, FontWeight.normal, AppColor.black), 712 | bodyMedium: textStyle(12.sp, FontWeight.normal, AppColor.black), 713 | ), 714 | 715 | 716 | cardColor: AppColor.primaryColor, 717 | buttonTheme: ButtonThemeData( 718 | buttonColor: AppColor.backgroundColor, 719 | shape: RoundedRectangleBorder( 720 | borderRadius: BorderRadius.circular(8), 721 | ), 722 | ), 723 | elevatedButtonTheme: ElevatedButtonThemeData( 724 | style: ElevatedButton.styleFrom( 725 | foregroundColor: Colors.white, 726 | backgroundColor: AppColor.primaryColor, 727 | textStyle: textStyle(16, FontWeight.bold, Colors.white), 728 | shape: RoundedRectangleBorder( 729 | borderRadius: BorderRadius.circular(8), 730 | ), 731 | ), 732 | ), 733 | iconTheme: const IconThemeData(color: Colors.black), 734 | ); 735 | } 736 | 737 | 738 | // 🌙 الوضع الداكن 739 | static ThemeData get darkTheme { 740 | return ThemeData( 741 | brightness: Brightness.dark, 742 | primaryColor: AppColor.primaryColor, 743 | scaffoldBackgroundColor: AppColor.backgroundColor, 744 | fontFamily: 'Nunito', 745 | appBarTheme: const AppBarTheme( 746 | elevation: 0, 747 | centerTitle: true, 748 | backgroundColor: Colors.black, 749 | iconTheme: IconThemeData(color: Colors.white), 750 | titleTextStyle: TextStyle( 751 | color: Colors.white, 752 | fontSize: 18, 753 | fontWeight: FontWeight.bold, 754 | ), 755 | ), 756 | textTheme: TextTheme( 757 | displayLarge: textStyle(24.sp, FontWeight.bold, Colors.white), 758 | displayMedium: textStyle(20.sp, FontWeight.bold, Colors.white), 759 | displaySmall: textStyle(18.sp, FontWeight.w600, Colors.white), 760 | headlineMedium: textStyle(16.sp, FontWeight.bold, Colors.white), 761 | bodyLarge: textStyle(14.sp, FontWeight.normal, Colors.white), 762 | bodyMedium: textStyle(12.sp, FontWeight.normal, Colors.grey), 763 | ), 764 | cardColor: Colors.grey[900], 765 | buttonTheme: ButtonThemeData( 766 | buttonColor: AppColor.primaryColor, 767 | shape: RoundedRectangleBorder( 768 | borderRadius: BorderRadius.circular(8), 769 | ), 770 | ), 771 | elevatedButtonTheme: ElevatedButtonThemeData( 772 | style: ElevatedButton.styleFrom( 773 | foregroundColor: Colors.white, 774 | backgroundColor: AppColor.primaryColor, 775 | textStyle: textStyle(16, FontWeight.bold, Colors.white), 776 | shape: RoundedRectangleBorder( 777 | borderRadius: BorderRadius.circular(8), 778 | ), 779 | ), 780 | ), 781 | iconTheme: const IconThemeData(color: Colors.white), 782 | ); 783 | } 784 | 785 | 786 | // 🎨 دالة لإنشاء TextStyle بسهولة 787 | static TextStyle textStyle(double size, FontWeight weight, Color color) { 788 | return TextStyle( 789 | fontSize: size, 790 | fontWeight: weight, 791 | color: color, 792 | ); 793 | } 794 | } 795 | 796 | 797 | '''; 798 | 799 | String _navigationExtCode() => ''' 800 | //! NAVIGATION EXTENSION 801 | import 'package:flutter/material.dart'; 802 | 803 | 804 | extension NavigationExtensions on BuildContext { 805 | // Push a new page onto the stack 806 | void push(Widget page) => 807 | Navigator.of(this).push(MaterialPageRoute(builder: (_) => page)); 808 | 809 | 810 | // Push a named route onto the stack 811 | Future pushNamed(String routeName, {Object? arguments}) => 812 | Navigator.of(this).pushNamed(routeName, arguments: arguments); 813 | 814 | 815 | // Replace the current route with a new one 816 | Future pushReplacement(Widget page) => Navigator.of( 817 | this, 818 | ).pushReplacement(MaterialPageRoute(builder: (_) => page)); 819 | 820 | 821 | // Replace the current route with a named route 822 | Future pushReplacementNamed( 823 | String routeName, { 824 | Object? arguments, 825 | }) => Navigator.of( 826 | this, 827 | ).pushReplacementNamed(routeName, arguments: arguments); 828 | 829 | 830 | // Pop the current route off the stack 831 | void back() => Navigator.of(this).pop(); 832 | 833 | 834 | // Pop until the predicate returns true 835 | void popUntil(RoutePredicate predicate) => 836 | Navigator.of(this).popUntil(predicate); 837 | 838 | 839 | // Pop the current route and push a new route 840 | Future popAndPushNamed(String routeName, {Object? arguments}) => 841 | Navigator.of( 842 | this, 843 | ).popAndPushNamed(routeName, arguments: arguments); 844 | 845 | 846 | // Push a new route and remove all previous routes 847 | Future pushAndRemoveUntil(Widget page, RoutePredicate predicate) => 848 | Navigator.of( 849 | this, 850 | ).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => page), predicate); 851 | 852 | 853 | // Push a named route and remove all previous routes 854 | Future pushNamedAndRemoveUntil( 855 | String routeName, { 856 | Object? arguments, 857 | }) => Navigator.of(this).pushNamedAndRemoveUntil( 858 | routeName, 859 | (route) => false, 860 | arguments: arguments, 861 | ); 862 | 863 | 864 | // Try to pop the route; returns true if successful, otherwise false 865 | Future maybePop() => Navigator.of(this).maybePop(); 866 | 867 | 868 | // Replace the current route with a new route using a custom route 869 | Future replaceWithCustomRoute(Route route) => 870 | Navigator.of(this).pushReplacement(route); 871 | 872 | 873 | // Push a custom route onto the stack 874 | Future pushCustomRoute(Route route) => 875 | Navigator.of(this).push(route); 876 | } 877 | 878 | 879 | '''; 880 | 881 | String _sizedBoxExtCode() => ''' 882 | import 'package:flutter/material.dart'; 883 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 884 | 885 | 886 | SizedBox verticalSpace(double height) => SizedBox(height: height.h); 887 | SizedBox horizontalSpace(double width) => SizedBox(width: width.w); 888 | '''; 889 | 890 | String _themeExtCode() => ''' 891 | //! THEME EXTENSION 892 | import 'package:flutter/material.dart'; 893 | 894 | 895 | extension ThemeExtensions on BuildContext { 896 | //! Recommended to use: ThemeData get theme => Theme.of(this); 897 | ThemeData get theme => Theme.of(this); 898 | 899 | 900 | // Existing extensions 901 | IconThemeData get iconTheme => Theme.of(this).iconTheme; 902 | 903 | 904 | TextTheme get textTheme => Theme.of(this).textTheme; 905 | 906 | 907 | AppBarTheme get appBarTheme => Theme.of(this).appBarTheme; 908 | 909 | 910 | InputDecorationTheme get inputDecorationTheme => 911 | Theme.of(this).inputDecorationTheme; 912 | 913 | 914 | CheckboxThemeData get checkboxTheme => Theme.of(this).checkboxTheme; 915 | 916 | 917 | ElevatedButtonThemeData get elevatedButtonTheme => 918 | Theme.of(this).elevatedButtonTheme; 919 | 920 | 921 | OutlinedButtonThemeData get outlinedButtonTheme => 922 | Theme.of(this).outlinedButtonTheme; 923 | 924 | 925 | TextButtonThemeData get textButtonTheme => Theme.of(this).textButtonTheme; 926 | 927 | 928 | CardThemeData get cardTheme => Theme.of(this).cardTheme; 929 | 930 | 931 | DialogThemeData get dialogTheme => Theme.of(this).dialogTheme; 932 | 933 | 934 | FloatingActionButtonThemeData get floatingActionButtonTheme => 935 | Theme.of(this).floatingActionButtonTheme; 936 | 937 | 938 | BottomNavigationBarThemeData get bottomNavigationBarTheme => 939 | Theme.of(this).bottomNavigationBarTheme; 940 | 941 | 942 | NavigationRailThemeData get navigationRailTheme => 943 | Theme.of(this).navigationRailTheme; 944 | 945 | 946 | SliderThemeData get sliderTheme => Theme.of(this).sliderTheme; 947 | 948 | 949 | TabBarThemeData get tabBarTheme => Theme.of(this).tabBarTheme; 950 | 951 | 952 | TooltipThemeData get tooltipTheme => Theme.of(this).tooltipTheme; 953 | 954 | 955 | PopupMenuThemeData get popupMenuTheme => Theme.of(this).popupMenuTheme; 956 | 957 | 958 | MaterialBannerThemeData get bannerTheme => Theme.of(this).bannerTheme; 959 | 960 | 961 | DividerThemeData get dividerTheme => Theme.of(this).dividerTheme; 962 | 963 | 964 | BottomSheetThemeData get bottomSheetTheme => Theme.of(this).bottomSheetTheme; 965 | 966 | 967 | TimePickerThemeData get timePickerTheme => Theme.of(this).timePickerTheme; 968 | 969 | 970 | ThemeData get darkTheme => ThemeData.dark(); 971 | 972 | 973 | ThemeData get lightTheme => ThemeData.light(); 974 | 975 | 976 | // Additional extensions 977 | ButtonThemeData get buttonTheme => Theme.of(this).buttonTheme; 978 | 979 | 980 | ChipThemeData get chipTheme => Theme.of(this).chipTheme; 981 | 982 | 983 | DataTableThemeData get dataTableTheme => Theme.of(this).dataTableTheme; 984 | 985 | 986 | DrawerThemeData get drawerTheme => Theme.of(this).drawerTheme; 987 | 988 | 989 | ExpansionTileThemeData get expansionTileTheme => 990 | Theme.of(this).expansionTileTheme; 991 | 992 | 993 | ListTileThemeData get listTileTheme => Theme.of(this).listTileTheme; 994 | 995 | 996 | MenuThemeData get menuTheme => Theme.of(this).menuTheme; 997 | 998 | 999 | NavigationBarThemeData get navigationBarTheme => 1000 | Theme.of(this).navigationBarTheme; 1001 | 1002 | 1003 | PageTransitionsTheme get pageTransitionsTheme => 1004 | Theme.of(this).pageTransitionsTheme; 1005 | 1006 | 1007 | ProgressIndicatorThemeData get progressIndicatorTheme => 1008 | Theme.of(this).progressIndicatorTheme; 1009 | 1010 | 1011 | RadioThemeData get radioTheme => Theme.of(this).radioTheme; 1012 | 1013 | 1014 | ScrollbarThemeData get scrollbarTheme => Theme.of(this).scrollbarTheme; 1015 | 1016 | 1017 | SwitchThemeData get switchTheme => Theme.of(this).switchTheme; 1018 | 1019 | 1020 | TextSelectionThemeData get textSelectionTheme => 1021 | Theme.of(this).textSelectionTheme; 1022 | 1023 | 1024 | BottomAppBarTheme get bottomAppBarTheme => Theme.of(this).bottomAppBarTheme; 1025 | 1026 | 1027 | MaterialTapTargetSize get materialTapTargetSize => 1028 | Theme.of(this).materialTapTargetSize; 1029 | 1030 | 1031 | Typography get typography => Theme.of(this).typography; 1032 | 1033 | 1034 | VisualDensity get visualDensity => Theme.of(this).visualDensity; 1035 | 1036 | 1037 | IconButtonThemeData get iconButtonTheme => Theme.of(this).iconButtonTheme; 1038 | 1039 | 1040 | ColorScheme get colorScheme => Theme.of(this).colorScheme; 1041 | } 1042 | 1043 | 1044 | '''; 1045 | 1046 | String _splashScreenCode(projectName) => ''' 1047 | import 'package:flutter/material.dart'; 1048 | import '../../../../core/routing/routes.dart'; 1049 | import 'package:flutter_bloc/flutter_bloc.dart'; 1050 | import 'package:easy_localization/easy_localization.dart'; 1051 | import 'dart:async'; 1052 | import '../../../../core/extensions/navigation_extensions.dart'; 1053 | 1054 | 1055 | class SplashScreen extends StatefulWidget { 1056 | const SplashScreen({super.key}); 1057 | 1058 | 1059 | @override 1060 | State createState() => _SplashScreenState(); 1061 | } 1062 | 1063 | 1064 | class _SplashScreenState extends State { 1065 | @override 1066 | void initState() { 1067 | super.initState(); 1068 | Timer(const Duration(seconds: 2), () { 1069 | context.pushNamed(Routes.onBoardingScreen); 1070 | }); 1071 | } 1072 | 1073 | 1074 | @override 1075 | Widget build(BuildContext context) { 1076 | return const Scaffold( 1077 | body: Center( 1078 | child: Column( 1079 | mainAxisAlignment: MainAxisAlignment.center, 1080 | children: [ 1081 | FlutterLogo(size: 100), 1082 | SizedBox(height: 20), 1083 | Text("Splash Screen", style: TextStyle(fontSize: 20)), 1084 | ], 1085 | ), 1086 | ), 1087 | ); 1088 | } 1089 | } 1090 | 1091 | 1092 | '''; 1093 | String _localeCubitCode() => ''' 1094 | import 'package:flutter/material.dart'; 1095 | import 'package:flutter_bloc/flutter_bloc.dart'; 1096 | import '../../utils/app_shared_preferences.dart'; 1097 | import '../../constants/app_constants.dart'; 1098 | 1099 | 1100 | part 'locale_state.dart'; 1101 | 1102 | 1103 | class LocaleCubit extends Cubit { 1104 | LocaleCubit() : super(LocaleState(_getInitialLocale())); 1105 | 1106 | 1107 | static Locale _getInitialLocale() { 1108 | final savedLocale = AppPreferences().getData(AppConstants.localeKey); 1109 | return savedLocale == 'ar' ? const Locale('ar') : const Locale('en'); 1110 | } 1111 | 1112 | 1113 | Future changeLocale(Locale newLocale) async { 1114 | await AppPreferences().setData(AppConstants.localeKey, newLocale.languageCode); 1115 | emit(LocaleState(newLocale)); 1116 | } 1117 | } 1118 | 1119 | 1120 | '''; 1121 | 1122 | String _themeCubitCode() => ''' 1123 | import 'package:flutter/material.dart'; 1124 | import 'package:flutter_bloc/flutter_bloc.dart'; 1125 | import '../../utils/app_shared_preferences.dart'; 1126 | import '../../constants/app_constants.dart'; 1127 | 1128 | 1129 | part 'theme_state.dart'; 1130 | 1131 | 1132 | class ThemeCubit extends Cubit { 1133 | ThemeCubit() : super(ThemeState(_getInitialTheme())); 1134 | 1135 | 1136 | static ThemeMode _getInitialTheme() { 1137 | final savedTheme = AppPreferences().getData(AppConstants.themeKey); 1138 | if (savedTheme == 'dark') return ThemeMode.dark; 1139 | if (savedTheme == 'light') return ThemeMode.light; 1140 | return ThemeMode.system; 1141 | } 1142 | 1143 | 1144 | Future changeTheme(ThemeMode newTheme) async { 1145 | await AppPreferences().setData(AppConstants.themeKey, newTheme.name); 1146 | emit(ThemeState(newTheme)); 1147 | } 1148 | } 1149 | 1150 | 1151 | '''; 1152 | 1153 | String _appRouterCode(String projectName) => ''' 1154 | import 'package:flutter/material.dart'; 1155 | import '../routing/routes.dart'; 1156 | import '../../features/splash/presentation/screens/splash_screen.dart'; 1157 | import '../../features/onboarding/presentation/screens/onboarding_screen.dart'; 1158 | 1159 | 1160 | class AppRouter { 1161 | Route? generateRoute(RouteSettings settings) { 1162 | switch (settings.name) { 1163 | case Routes.splashScreen: 1164 | return _createRoute(const SplashScreen()); 1165 | case Routes.onBoardingScreen: 1166 | return _createRoute(const OnboardingScreen()); 1167 | 1168 | 1169 | default: 1170 | return null; 1171 | } 1172 | } 1173 | 1174 | 1175 | PageRouteBuilder _createRoute(Widget page) { 1176 | return PageRouteBuilder( 1177 | transitionDuration: const Duration(milliseconds: 400), 1178 | pageBuilder: (context, animation, secondaryAnimation) => page, 1179 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 1180 | return FadeTransition( 1181 | opacity: animation, 1182 | child: child, 1183 | ); 1184 | }, 1185 | ); 1186 | } 1187 | } 1188 | '''; 1189 | 1190 | String _routesCode() => ''' 1191 | class Routes { 1192 | static const String splashScreen = '/splashScreen'; 1193 | static const String onBoardingScreen = '/onBoardingScreen'; 1194 | static const String loginScreen = '/loginScreen'; 1195 | static const String signupScreen = '/signupScreen'; 1196 | static const String homeScreen = '/homeScreen'; 1197 | } 1198 | '''; 1199 | 1200 | String _appThemeCode() => ''' 1201 | import 'package:flutter/material.dart'; 1202 | import 'app_colors.dart'; 1203 | 1204 | 1205 | class AppTheme { 1206 | static ThemeData get lightTheme => ThemeData( 1207 | primaryColor: AppColor.primaryColor, 1208 | scaffoldBackgroundColor: AppColor.backgroundColor, 1209 | fontFamily: 'Nunito', 1210 | brightness: Brightness.light, 1211 | appBarTheme: const AppBarTheme( 1212 | backgroundColor: AppColor.white, 1213 | elevation: 0, 1214 | centerTitle: true, 1215 | iconTheme: IconThemeData(color: AppColor.black), 1216 | titleTextStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: AppColor.black), 1217 | ), 1218 | ); 1219 | 1220 | 1221 | static ThemeData get darkTheme => ThemeData( 1222 | primaryColor: AppColor.primaryColor, 1223 | scaffoldBackgroundColor: Colors.black, 1224 | fontFamily: 'Nunito', 1225 | brightness: Brightness.dark, 1226 | appBarTheme: const AppBarTheme( 1227 | backgroundColor: Colors.black, 1228 | elevation: 0, 1229 | centerTitle: true, 1230 | iconTheme: IconThemeData(color: AppColor.white), 1231 | titleTextStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: AppColor.white), 1232 | ), 1233 | ); 1234 | } 1235 | '''; 1236 | 1237 | String _appColorsCode() => ''' 1238 | import 'package:flutter/material.dart'; 1239 | 1240 | 1241 | class AppColor { 1242 | static const Color primaryColor = Color(0xff1F21A8); 1243 | static const Color backgroundColor = Color(0xffF8FBFC); 1244 | static const Color grayColor = Colors.grey; 1245 | static const Color white = Colors.white; 1246 | static const Color black = Colors.black; 1247 | } 1248 | '''; 1249 | 1250 | String _mainCode() => ''' 1251 | import 'package:flutter/material.dart'; 1252 | import 'package:flutter_bloc/flutter_bloc.dart'; 1253 | import 'package:easy_localization/easy_localization.dart'; 1254 | import 'core/constants/app_constants.dart'; 1255 | import 'core/cubit/locale/locale_cubit.dart'; 1256 | import 'core/cubit/theme/theme_cubit.dart'; 1257 | import 'core/utils/app_shared_preferences.dart'; 1258 | import 'core/routing/app_router.dart'; 1259 | import 'app.dart'; 1260 | import 'app_bloc_observer.dart'; 1261 | 1262 | 1263 | void main() async { 1264 | WidgetsFlutterBinding.ensureInitialized(); 1265 | await EasyLocalization.ensureInitialized(); 1266 | Bloc.observer = AppBlocObserver(); 1267 | await AppPreferences().init(); 1268 | 1269 | 1270 | runApp(EasyLocalization( 1271 | supportedLocales: AppConstants.supportedLocales, 1272 | path: 'assets/lang', 1273 | fallbackLocale: const Locale('en'), 1274 | child: MultiBlocProvider( 1275 | providers: [ 1276 | BlocProvider(create: (_) => LocaleCubit()), 1277 | BlocProvider(create: (_) => ThemeCubit()), 1278 | ], 1279 | child: MyApp(appRouter: AppRouter()), 1280 | ), 1281 | )); 1282 | } 1283 | '''; 1284 | 1285 | String _appCode() => ''' 1286 | import 'package:flutter/material.dart'; 1287 | import 'package:flutter_bloc/flutter_bloc.dart'; 1288 | import 'package:easy_localization/easy_localization.dart'; 1289 | import 'core/theme/app_theme.dart'; 1290 | import 'core/routing/app_router.dart'; 1291 | import 'core/constants/app_constants.dart'; 1292 | import 'core/cubit/theme/theme_cubit.dart'; 1293 | 1294 | 1295 | class MyApp extends StatelessWidget { 1296 | final AppRouter appRouter; 1297 | const MyApp({super.key, required this.appRouter}); 1298 | 1299 | 1300 | @override 1301 | Widget build(BuildContext context) { 1302 | return BlocBuilder( 1303 | builder: (context, themeState) => MaterialApp( 1304 | debugShowCheckedModeBanner: false, 1305 | title: AppConstants.appName, 1306 | theme: AppTheme.lightTheme, 1307 | darkTheme: AppTheme.darkTheme, 1308 | themeMode: themeState.themeMode, 1309 | locale: context.locale, 1310 | supportedLocales: context.supportedLocales, 1311 | localizationsDelegates: context.localizationDelegates, 1312 | onGenerateRoute: appRouter.generateRoute, 1313 | initialRoute: '/', 1314 | ), 1315 | ); 1316 | } 1317 | } 1318 | '''; 1319 | 1320 | String _blocObserverCode() => ''' 1321 | import 'package:flutter_bloc/flutter_bloc.dart'; 1322 | 1323 | 1324 | class AppBlocObserver extends BlocObserver { 1325 | @override 1326 | void onCreate(BlocBase bloc) { 1327 | super.onCreate(bloc); 1328 | print('🔍 Bloc Created: \${bloc.runtimeType}'); 1329 | } 1330 | 1331 | 1332 | @override 1333 | void onChange(BlocBase bloc, Change change) { 1334 | super.onChange(bloc, change); 1335 | print('🔁 Bloc Change in \${bloc.runtimeType}: \$change'); 1336 | } 1337 | 1338 | 1339 | @override 1340 | void onError(BlocBase bloc, Object error, StackTrace stackTrace) { 1341 | print('❌ Bloc Error in \${bloc.runtimeType}: \$error'); 1342 | super.onError(bloc, error, stackTrace); 1343 | } 1344 | 1345 | 1346 | @override 1347 | void onClose(BlocBase bloc) { 1348 | print('🛑 Bloc Closed: \${bloc.runtimeType}'); 1349 | super.onClose(bloc); 1350 | } 1351 | } 1352 | '''; 1353 | String _onboardingScreenCode(String projectName) => ''' 1354 | import 'package:flutter/material.dart'; 1355 | 1356 | 1357 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 1358 | import 'package:google_fonts/google_fonts.dart'; 1359 | 1360 | 1361 | class OnboardingScreen extends StatefulWidget { 1362 | const OnboardingScreen({super.key}); 1363 | 1364 | 1365 | @override 1366 | State createState() => _OnboardingScreenState(); 1367 | } 1368 | 1369 | 1370 | class _OnboardingScreenState extends State { 1371 | final controller = PageController(); 1372 | bool isLastPage = false; 1373 | 1374 | 1375 | @override 1376 | void dispose() { 1377 | controller.dispose(); 1378 | super.dispose(); 1379 | } 1380 | 1381 | 1382 | @override 1383 | Widget build(BuildContext context) { 1384 | return Scaffold( 1385 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 1386 | body: Padding( 1387 | padding: EdgeInsets.symmetric(horizontal: 24.w), 1388 | child: Column( 1389 | children: [ 1390 | SizedBox(height: 60.h), 1391 | Expanded( 1392 | child: PageView( 1393 | controller: controller, 1394 | onPageChanged: (index) => setState(() => isLastPage = index == 2), 1395 | children: const [ 1396 | OnboardPage(title: 'Welcome', description: 'This is onboarding 1'), 1397 | OnboardPage(title: 'Explore', description: 'This is onboarding 2'), 1398 | OnboardPage(title: 'Start', description: 'This is onboarding 3'), 1399 | ], 1400 | ), 1401 | ), 1402 | 1403 | SizedBox(height: 20.h), 1404 | SizedBox( 1405 | width: double.infinity, 1406 | child: ElevatedButton( 1407 | onPressed: () { 1408 | if (isLastPage) { 1409 | } else { 1410 | controller.nextPage( 1411 | duration: const Duration(milliseconds: 500), 1412 | curve: Curves.easeInOut, 1413 | ); 1414 | } 1415 | }, 1416 | child: Text(isLastPage ? 'Get Started' : 'Next'), 1417 | ), 1418 | ), 1419 | SizedBox(height: 40.h), 1420 | ], 1421 | ), 1422 | ), 1423 | ); 1424 | } 1425 | } 1426 | 1427 | 1428 | class OnboardPage extends StatelessWidget { 1429 | final String title; 1430 | final String description; 1431 | const OnboardPage({super.key, required this.title, required this.description}); 1432 | 1433 | 1434 | @override 1435 | Widget build(BuildContext context) { 1436 | return Padding( 1437 | padding: EdgeInsets.symmetric(horizontal: 24.w), 1438 | child: Column( 1439 | mainAxisAlignment: MainAxisAlignment.center, 1440 | children: [ 1441 | Icon(Icons.flutter_dash, size: 120.r), 1442 | SizedBox(height: 20.h), 1443 | Text( 1444 | title, 1445 | style: GoogleFonts.nunito( 1446 | fontSize: 26.sp, 1447 | fontWeight: FontWeight.bold, 1448 | ), 1449 | ), 1450 | SizedBox(height: 12.h), 1451 | Text( 1452 | description, 1453 | textAlign: TextAlign.center, 1454 | style: GoogleFonts.nunito(fontSize: 16.sp), 1455 | ), 1456 | ], 1457 | ), 1458 | ); 1459 | } 1460 | } 1461 | 1462 | 1463 | '''; 1464 | 1465 | String _loginScreenCode(String projectName) => ''' 1466 | import 'package:flutter/material.dart'; 1467 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 1468 | import 'package:$projectName/core/routing/routes.dart'; 1469 | 1470 | 1471 | class LoginScreen extends StatelessWidget { 1472 | const LoginScreen({super.key}); 1473 | 1474 | 1475 | @override 1476 | Widget build(BuildContext context) { 1477 | return Scaffold( 1478 | appBar: AppBar(title: const Text('Login')), 1479 | body: Padding( 1480 | padding: EdgeInsets.all(20.w), 1481 | child: Column( 1482 | crossAxisAlignment: CrossAxisAlignment.stretch, 1483 | children: [ 1484 | TextField(decoration: const InputDecoration(labelText: 'Email')), 1485 | SizedBox(height: 16.h), 1486 | TextField(obscureText: true, decoration: const InputDecoration(labelText: 'Password')), 1487 | SizedBox(height: 24.h), 1488 | ElevatedButton( 1489 | onPressed: () => Navigator.pushReplacementNamed(context, Routes.homeScreen), 1490 | child: const Text('Login'), 1491 | ), 1492 | SizedBox(height: 8.h), 1493 | TextButton( 1494 | onPressed: () => Navigator.pushNamed(context, Routes.signupScreen), 1495 | child: const Text("Don\'t have an account? Sign up"), 1496 | ), 1497 | ], 1498 | ), 1499 | ), 1500 | ); 1501 | } 1502 | } 1503 | '''; 1504 | 1505 | String _signupScreenCode() => ''' 1506 | import 'package:flutter/material.dart'; 1507 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 1508 | 1509 | 1510 | class SignUpScreen extends StatelessWidget { 1511 | const SignUpScreen({super.key}); 1512 | 1513 | 1514 | @override 1515 | Widget build(BuildContext context) { 1516 | return Scaffold( 1517 | appBar: AppBar(title: const Text('Sign Up')), 1518 | body: Padding( 1519 | padding: EdgeInsets.all(20.w), 1520 | child: Column( 1521 | crossAxisAlignment: CrossAxisAlignment.stretch, 1522 | children: [ 1523 | const TextField(decoration: InputDecoration(labelText: 'Name')), 1524 | SizedBox(height: 16.h), 1525 | const TextField(decoration: InputDecoration(labelText: 'Email')), 1526 | SizedBox(height: 16.h), 1527 | const TextField(obscureText: true, decoration: InputDecoration(labelText: 'Password')), 1528 | SizedBox(height: 24.h), 1529 | ElevatedButton( 1530 | onPressed: () {}, 1531 | child: const Text('Create Account'), 1532 | ), 1533 | ], 1534 | ), 1535 | ), 1536 | ); 1537 | } 1538 | } 1539 | '''; 1540 | String _themeStateCode() => ''' 1541 | part of 'theme_cubit.dart'; 1542 | 1543 | 1544 | class ThemeState { 1545 | final ThemeMode themeMode; 1546 | 1547 | 1548 | const ThemeState(this.themeMode); 1549 | 1550 | 1551 | @override 1552 | bool operator ==(Object other) { 1553 | if (identical(this, other)) return true; 1554 | return other is ThemeState && other.themeMode == themeMode; 1555 | } 1556 | 1557 | 1558 | @override 1559 | int get hashCode => themeMode.hashCode; 1560 | } 1561 | '''; 1562 | 1563 | String _localeStateCode() => ''' 1564 | part of 'locale_cubit.dart'; 1565 | 1566 | 1567 | class LocaleState { 1568 | final Locale locale; 1569 | 1570 | 1571 | const LocaleState(this.locale); 1572 | 1573 | 1574 | @override 1575 | bool operator ==(Object other) { 1576 | if (identical(this, other)) return true; 1577 | return other is LocaleState && other.locale == locale; 1578 | } 1579 | 1580 | 1581 | @override 1582 | int get hashCode => locale.hashCode; 1583 | } 1584 | '''; 1585 | -------------------------------------------------------------------------------- /icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbdalluhEssam/flutter_full_structure/083ecc866cd290fe51bf4772f706803fba0f7a6c/icon.jpeg -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2022", 5 | "checkJs": true, /* Typecheck .js files. */ 6 | "lib": [ 7 | "ES2022" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter-full-structure", 3 | "displayName": "Flutter Full Structure Generator", 4 | "description": "A powerful Flutter extension that instantly generates a complete clean architecture project structure with a single command.", 5 | "version": "0.0.4", 6 | "publisher": "AbdalluhEssamElsayed", 7 | "engines": { 8 | "vscode": "^1.70.0" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/AbdalluhEssam/flutter_full_structure.git" 13 | }, 14 | "keywords": ["flutter", "clean architecture", "structure", "project generator"], 15 | "icon": "icon.jpeg", 16 | "categories": [ 17 | "Other" 18 | ], 19 | "activationEvents": [ 20 | "onCommand:flutter-full-structure.generate" 21 | ], 22 | "main": "./extension.js", 23 | "contributes": { 24 | "commands": [ 25 | { 26 | "command": "flutter-full-structure.generate", 27 | "title": "Generate Flutter Project Structure" 28 | } 29 | ] 30 | }, 31 | "scripts": { 32 | "lint": "eslint .", 33 | "pretest": "npm run lint", 34 | "test": "node ./test/runTest.js" 35 | }, 36 | "devDependencies": { 37 | "@types/vscode": "^1.70.0", 38 | "@types/node": "^18.0.0", 39 | "eslint": "^8.0.0", 40 | "@vscode/test-electron": "^2.4.1" 41 | } 42 | } -------------------------------------------------------------------------------- /test/extension.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | const vscode = require('vscode'); 6 | // const myExtension = require('../extension'); 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tool/generate_feature.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | void main(List arguments) { 4 | // التأكد من أن المستخدم قد أدخل اسم الـ feature 5 | if (arguments.isEmpty) { 6 | print('❌ رجاءً اكتب اسم الـ feature:'); 7 | print('مثال: dart tool/generate_feature.dart login'); 8 | return; 9 | } 10 | 11 | final featureName = arguments.first.toLowerCase(); 12 | final className = capitalize(featureName); 13 | final basePath = 'lib/features/$featureName'; 14 | 15 | // قائمة المجلدات التي سيتم إنشاؤها 16 | final folders = [ 17 | '$basePath/domain/entities', 18 | '$basePath/domain/repositories', 19 | '$basePath/domain/usecases', 20 | '$basePath/data/models', 21 | '$basePath/data/datasources', 22 | '$basePath/data/repositories', 23 | '$basePath/presentation/cubit', 24 | '$basePath/presentation/pages', 25 | ]; 26 | 27 | // إنشاء المجلدات المطلوبة 28 | for (final folder in folders) { 29 | Directory(folder).createSync(recursive: true); 30 | } 31 | 32 | // إنشاء الطبقات المختلفة: Domain, Data, Presentation 33 | 34 | // Domain Layer 35 | _createDomainFiles(basePath, featureName, className); 36 | 37 | // Data Layer 38 | _createDataFiles(basePath, featureName, className); 39 | 40 | // Presentation Layer 41 | _createPresentationFiles(basePath, featureName, className); 42 | 43 | print('✅ تم إنشاء الـ Feature "$featureName" مع الهيكل الكامل.'); 44 | } 45 | 46 | void _createDomainFiles(String basePath, String featureName, String className) { 47 | // إنشاء الملفات الخاصة بالـ Domain Layer 48 | File('$basePath/domain/entities/${featureName}_entity.dart') 49 | .writeAsStringSync(''' 50 | class ${className}Entity { 51 | // TODO: Define entity fields 52 | } 53 | '''); 54 | 55 | File('$basePath/domain/repositories/${featureName}_repository.dart') 56 | .writeAsStringSync(''' 57 | abstract class ${className}Repository { 58 | // TODO: Define repository methods 59 | } 60 | '''); 61 | 62 | File('$basePath/domain/usecases/${featureName}_usecase.dart') 63 | .writeAsStringSync(''' 64 | import '../repositories/${featureName}_repository.dart'; 65 | 66 | class ${className}UseCase { 67 | final ${className}Repository repository; 68 | 69 | ${className}UseCase(this.repository); 70 | 71 | // TODO: Implement call logic 72 | } 73 | '''); 74 | } 75 | 76 | void _createDataFiles(String basePath, String featureName, String className) { 77 | // إنشاء الملفات الخاصة بالـ Data Layer 78 | File('$basePath/data/models/${featureName}_model.dart').writeAsStringSync(''' 79 | import '../../domain/entities/${featureName}_entity.dart'; 80 | 81 | class ${className}Model extends ${className}Entity { 82 | ${className}Model() : super(); 83 | 84 | factory ${className}Model.fromJson(Map json) { 85 | // TODO: Map JSON to model 86 | return ${className}Model(); 87 | } 88 | } 89 | '''); 90 | 91 | File('$basePath/data/datasources/${featureName}_remote_datasource.dart') 92 | .writeAsStringSync(''' 93 | abstract class ${className}RemoteDataSource { 94 | // TODO: Define methods like login(email, password) 95 | } 96 | '''); 97 | 98 | File('$basePath/data/repositories/${featureName}_repository_impl.dart') 99 | .writeAsStringSync(''' 100 | import '../../domain/repositories/${featureName}_repository.dart'; 101 | import '../datasources/${featureName}_remote_datasource.dart'; 102 | 103 | class ${className}RepositoryImpl implements ${className}Repository { 104 | final ${className}RemoteDataSource remoteDataSource; 105 | 106 | ${className}RepositoryImpl(this.remoteDataSource); 107 | 108 | // TODO: Implement repository logic 109 | } 110 | '''); 111 | } 112 | 113 | void _createPresentationFiles( 114 | String basePath, String featureName, String className) { 115 | // إنشاء الملفات الخاصة بالـ Presentation Layer: Cubit و Page 116 | File('$basePath/presentation/cubit/${featureName}_state.dart') 117 | .writeAsStringSync(''' 118 | abstract class ${className}State {} 119 | 120 | class ${className}Initial extends ${className}State {} 121 | 122 | class ${className}Loading extends ${className}State {} 123 | 124 | class ${className}Success extends ${className}State { 125 | // final result; 126 | // Success(this.result); 127 | } 128 | 129 | class ${className}Failure extends ${className}State { 130 | final String error; 131 | ${className}Failure(this.error); 132 | } 133 | '''); 134 | 135 | File('$basePath/presentation/cubit/${featureName}_cubit.dart') 136 | .writeAsStringSync(''' 137 | import 'package:flutter_bloc/flutter_bloc.dart'; 138 | import '${featureName}_state.dart'; 139 | 140 | class ${className}Cubit extends Cubit<${className}State> { 141 | ${className}Cubit() : super(${className}Initial()); 142 | 143 | Future doSomething() async { 144 | emit(${className}Loading()); 145 | try { 146 | // Call usecase 147 | // emit(${className}Success(result)); 148 | } catch (e) { 149 | emit(${className}Failure(e.toString())); 150 | } 151 | } 152 | } 153 | '''); 154 | 155 | File('$basePath/presentation/pages/${featureName}_page.dart') 156 | .writeAsStringSync(''' 157 | import 'package:flutter/material.dart'; 158 | import 'package:flutter_bloc/flutter_bloc.dart'; 159 | import '../cubit/${featureName}_cubit.dart'; 160 | import '../cubit/${featureName}_state.dart'; 161 | 162 | class ${className}Page extends StatelessWidget { 163 | const ${className}Page({super.key}); 164 | 165 | @override 166 | Widget build(BuildContext context) { 167 | return BlocProvider( 168 | create: (_) => ${className}Cubit(), 169 | child: Scaffold( 170 | appBar: AppBar(title: const Text('$className Page')), 171 | body: BlocBuilder<${className}Cubit, ${className}State>( 172 | builder: (context, state) { 173 | if (state is ${className}Loading) { 174 | return const Center(child: CircularProgressIndicator()); 175 | } else if (state is ${className}Failure) { 176 | return Center(child: Text('Error: \${state.error}')); 177 | } 178 | return const Center(child: Text('$className Page')); 179 | }, 180 | ), 181 | ), 182 | ); 183 | } 184 | } 185 | '''); 186 | } 187 | 188 | // وظيفة لكتابة أول حرف بحروف كبيرة 189 | String capitalize(String s) => s[0].toUpperCase() + s.substring(1); 190 | -------------------------------------------------------------------------------- /tool/generate_project.dart: -------------------------------------------------------------------------------- 1 | // سكربت Dart احترافي لتوليد مشروع Flutter كامل بترتيب Clean Architecture 2 | // يحتوي على ملفات core, features, main app مع كود فعلي داخل كل ملف 3 | import 'dart:io'; 4 | import 'package:path/path.dart'; 5 | 6 | void main(List arguments) async { 7 | final projectDir = Directory.current; 8 | final projectName = basename(projectDir.path); 9 | final libDir = Directory(join(projectDir.path, 'lib')); 10 | 11 | await _generateCore(libDir, projectName); 12 | await _generateFeatures(libDir, projectName); 13 | await _generateMainApp(libDir, projectName); 14 | await _generateAssetsFolders(projectDir); 15 | await _generateLocalizationFiles(projectDir); 16 | await _generateReadme(projectDir, projectName); 17 | await _generatePubspecYaml(projectDir, projectName); 18 | 19 | print('\n✅ Flutter project is fully ready with Clean Architecture 🇪🇬'); 20 | } 21 | 22 | Future _generateAssetsFolders(Directory projectDir) async { 23 | final assetDirs = [ 24 | 'assets/images', 25 | 'assets/icons', 26 | 'assets/lottie', 27 | 'assets/lang', 28 | ]; 29 | for (final dir in assetDirs) { 30 | await Directory(join(projectDir.path, dir)).create(recursive: true); 31 | } 32 | } 33 | 34 | Future _generateLocalizationFiles(Directory projectDir) async { 35 | final langDir = Directory(join(projectDir.path, 'assets/lang')); 36 | 37 | await File(join(langDir.path, 'en.json')).writeAsString('''{ 38 | "appName": "My App", 39 | "welcome": "Welcome", 40 | "login": "Login", 41 | "signup": "Sign Up" 42 | }'''); 43 | 44 | await File(join(langDir.path, 'ar.json')).writeAsString('''{ 45 | "appName": "تطبيقي", 46 | "welcome": "مرحبًا", 47 | "login": "تسجيل الدخول", 48 | "signup": "إنشاء حساب" 49 | }'''); 50 | } 51 | 52 | Future _generateReadme(Directory projectDir, String projectName) async { 53 | final readmeFile = File(join(projectDir.path, 'README.md')); 54 | await readmeFile.writeAsString(''' 55 | Hello Created By Abdalluh Essam 🇪🇬🇪🇬🇪 56 | abdallhesam100@gmail.com 57 | 58 | 59 | 60 | 61 | # $projectName 62 | 63 | 64 | 🚀 Clean Architecture Flutter Project Generated Automatically 65 | 66 | 67 | ## Structure 68 | 69 | 70 | ``` 71 | lib/ 72 | ├── core/ 73 | │ ├── constants 74 | │ ├── network 75 | │ ├── errors 76 | │ ├── utils 77 | │ ├── services 78 | │ ├── routing 79 | │ ├── theme 80 | │ ├── cubit 81 | │ └── extensions 82 | ├── features/ 83 | │ ├── splash 84 | │ ├── onboarding 85 | │ ├── auth 86 | │ └── home 87 | ├── app.dart 88 | ├── main.dart 89 | └── app_bloc_observer.dart 90 | ``` 91 | 92 | 93 | ## Getting Started 94 | ```bash 95 | flutter pub get 96 | flutter run 97 | ``` 98 | 99 | 100 | --- 101 | 102 | 103 | ✅ Built with ❤️ using the Clean Architecture Generator 104 | 105 | 106 | Hello Created By Abdalluh Essam 🇪🇬🇪🇬🇪 107 | '''); 108 | } 109 | 110 | Future _generateCore(Directory libDir, String projectName) async { 111 | final coreDir = Directory(join(libDir.path, 'core')); 112 | final structure = { 113 | 'constants': { 114 | 'app_constants.dart': _appConstantsCode(), 115 | 'endpoint_constants.dart': _endpointConstantsCode(), 116 | 'strings_constants.dart': _stringsConstantsCode(), 117 | }, 118 | 'network': { 119 | 'api_consumer.dart': _apiConsumerCode(), 120 | 'dio_consumer.dart': _dioConsumerCode(), 121 | 'interceptors.dart': _interceptorsCode(), 122 | 'status_code.dart': _statusCodeCode(), 123 | }, 124 | 'errors': { 125 | 'exceptions.dart': _exceptionsCode(), 126 | 'failures.dart': _failuresCode(), 127 | }, 128 | 'utils': { 129 | 'app_utils.dart': _appUtilsCode(), 130 | 'app_shared_preferences.dart': _appPrefsCode(), 131 | }, 132 | 'services': { 133 | 'locale_service.dart': _localeServiceCode(), 134 | 'theme_service.dart': _themeServiceCode(), 135 | }, 136 | 'routing': { 137 | 'app_router.dart': _appRouterCode(projectName), 138 | 'routes.dart': _routesCode(), 139 | }, 140 | 'theme': { 141 | 'app_theme.dart': _appThemeCode(), 142 | 'app_colors.dart': _appColorsCode(), 143 | }, 144 | 'cubit/locale': { 145 | 'locale_cubit.dart': _localeCubitCode(), 146 | 'locale_state.dart': _localeStateCode(), 147 | }, 148 | 'cubit/theme': { 149 | 'theme_cubit.dart': _themeCubitCode(), 150 | 'theme_state.dart': _themeStateCode(), 151 | }, 152 | 'extensions': { 153 | 'navigation_extensions.dart': _navigationExtCode(), 154 | 'sizedbox_extensions.dart': _sizedBoxExtCode(), 155 | 'theme_extensions.dart': _themeExtCode(), 156 | }, 157 | }; 158 | 159 | for (final entry in structure.entries) { 160 | final dir = Directory(join(coreDir.path, entry.key)); 161 | await dir.create(recursive: true); 162 | for (final fileEntry in entry.value.entries) { 163 | final filePath = join(dir.path, fileEntry.key); 164 | await File(filePath).writeAsString(fileEntry.value); 165 | } 166 | } 167 | } 168 | 169 | Future _generateFeatures(Directory libDir, String projectName) async { 170 | final featuresDir = Directory(join(libDir.path, 'features')); 171 | await featuresDir.create(recursive: true); 172 | 173 | final splashDir = Directory( 174 | join(featuresDir.path, 'splash/presentation/screens'), 175 | ); 176 | final onboardingDir = Directory( 177 | join(featuresDir.path, 'onboarding/presentation/screens'), 178 | ); 179 | 180 | await splashDir.create(recursive: true); 181 | await onboardingDir.create(recursive: true); 182 | 183 | await File( 184 | join(splashDir.path, 'splash_screen.dart'), 185 | ).writeAsString(_splashScreenCode(projectName)); 186 | 187 | await File( 188 | join(onboardingDir.path, 'onboarding_screen.dart'), 189 | ).writeAsString(_onboardingScreenCode(projectName)); 190 | } 191 | 192 | Future _generateMainApp(Directory libDir, String projectName) async { 193 | await File(join(libDir.path, 'main.dart')).writeAsString(_mainCode()); 194 | await File(join(libDir.path, 'app.dart')).writeAsString(_appCode()); 195 | await File( 196 | join(libDir.path, 'app_bloc_observer.dart'), 197 | ).writeAsString(_blocObserverCode()); 198 | } 199 | 200 | Future _generatePubspecYaml( 201 | Directory projectDir, 202 | String projectName, 203 | ) async { 204 | final flutterCmd = Platform.isWindows ? 'flutter.bat' : 'flutter'; 205 | 206 | late final String flutterVersionOutput; 207 | try { 208 | final flutterVersionResult = await Process.run(flutterCmd, ['--version']); 209 | flutterVersionOutput = flutterVersionResult.stdout.toString(); 210 | } catch (e) { 211 | stderr.writeln( 212 | '❌ فشل في تشغيل flutter --version. تأكد أن Flutter موجود في PATH.'); 213 | rethrow; 214 | } 215 | 216 | // استخراج إصدار Dart باستخدام RegExp 217 | final sdkMatch = RegExp(r'Dart\sSDK\sversion:\s(\d+\.\d+\.\d+)') 218 | .firstMatch(flutterVersionOutput); 219 | final dartVersion = sdkMatch != null ? sdkMatch.group(1)! : '3.0.0'; 220 | final dartVersionClean = dartVersion.replaceAll('^', ''); 221 | 222 | // استخراج إصدار Flutter 223 | final flutterMatch = 224 | RegExp(r'Flutter\s(\d+\.\d+\.\d+)').firstMatch(flutterVersionOutput); 225 | final flutterVersion = 226 | flutterMatch != null ? flutterMatch.group(1)! : 'unknown'; 227 | 228 | final pubspecContent = ''' 229 | name: $projectName 230 | description: A new Flutter project with Clean Architecture by Abdalluh Essam 231 | # Flutter version on machine: $flutterVersion 232 | publish_to: 'none' 233 | 234 | environment: 235 | sdk: '>=$dartVersionClean <4.0.0' 236 | 237 | dependencies: 238 | flutter: 239 | sdk: flutter 240 | flutter_bloc: 241 | dio: 242 | shared_preferences: 243 | easy_localization: 244 | intl: 245 | equatable: 246 | get_it: 247 | cached_network_image: 248 | flutter_screenutil: 249 | flutter_animate: 250 | freezed_annotation: 251 | json_annotation: 252 | flutter_native_splash: 253 | animate_do: 254 | lottie: 255 | google_fonts: 256 | flutter_launcher_icons: 257 | animator: 258 | dartz: 259 | flutter_svg: 260 | cupertino_icons: 261 | 262 | dev_dependencies: 263 | flutter_test: 264 | sdk: flutter 265 | build_runner: 266 | freezed: 267 | json_serializable: 268 | bloc_test: 269 | mockito: 270 | flutter_lints: 271 | 272 | flutter: 273 | uses-material-design: true 274 | generate: true 275 | assets: 276 | - assets/images/ 277 | - assets/icons/ 278 | - assets/lottie/ 279 | - assets/lang/ 280 | '''; 281 | 282 | final file = File(join(projectDir.path, 'pubspec.yaml')); 283 | await file.writeAsString(pubspecContent); 284 | 285 | print('📄 تم إنشاء ملف pubspec.yaml بنجاح!'); 286 | } 287 | 288 | // ============================ الأكواد ============================ 289 | 290 | String _appConstantsCode() => ''' 291 | import 'dart:ui'; 292 | 293 | 294 | class AppConstants { 295 | static const String appName = 'My App'; 296 | static const List supportedLocales = [Locale('en'), Locale('ar')]; 297 | static const String localeKey = 'app_locale'; 298 | static const String themeKey = 'app_theme'; 299 | } 300 | '''; 301 | 302 | String _endpointConstantsCode() => ''' 303 | class EndpointConstants { 304 | static const String baseUrl = 'https://api.example.com/v1'; 305 | // Add your endpoints here 306 | } 307 | '''; 308 | 309 | String _stringsConstantsCode() => ''' 310 | class StringsConstants { 311 | static const Map en = { 312 | 'appName': 'My App', 313 | }; 314 | 315 | 316 | static const Map ar = { 317 | 'appName': 'تطبيقي', 318 | }; 319 | } 320 | '''; 321 | 322 | String _apiConsumerCode() => ''' 323 | abstract class ApiConsumer { 324 | Future get(String path, {Map? queryParameters}); 325 | Future post(String path, {Map? body}); 326 | Future put(String path, {Map? body}); 327 | Future delete(String path); 328 | } 329 | '''; 330 | 331 | String _dioConsumerCode() => ''' 332 | import 'package:dio/dio.dart'; 333 | import '../constants/endpoint_constants.dart'; 334 | import 'api_consumer.dart'; 335 | import 'interceptors.dart'; 336 | import 'status_code.dart'; 337 | 338 | 339 | class DioConsumer implements ApiConsumer { 340 | final Dio client; 341 | 342 | 343 | DioConsumer({required this.client}) { 344 | client.options 345 | ..baseUrl = EndpointConstants.baseUrl 346 | ..responseType = ResponseType.json 347 | ..connectTimeout = const Duration(seconds: 15) 348 | ..receiveTimeout = const Duration(seconds: 15); 349 | 350 | 351 | client.interceptors.add(AppInterceptors()); 352 | } 353 | 354 | 355 | @override 356 | Future get(String path, {Map? queryParameters}) async { 357 | try { 358 | final response = await client.get(path, queryParameters: queryParameters); 359 | return response.data; 360 | } on DioException catch (error) { 361 | _handleDioError(error); 362 | } 363 | } 364 | 365 | 366 | // Implement other methods (post, put, delete) 367 | 368 | 369 | void _handleDioError(DioException error) { 370 | // Handle different error types 371 | } 372 | 373 | 374 | @override 375 | Future delete(String path) { 376 | // TODO: implement delete 377 | throw UnimplementedError(); 378 | } 379 | 380 | 381 | @override 382 | Future post(String path, {Map? body}) { 383 | // TODO: implement post 384 | throw UnimplementedError(); 385 | } 386 | 387 | 388 | @override 389 | Future put(String path, {Map? body}) { 390 | // TODO: implement put 391 | throw UnimplementedError(); 392 | } 393 | } 394 | 395 | 396 | '''; 397 | 398 | String _interceptorsCode() => ''' 399 | import 'package:dio/dio.dart'; 400 | 401 | 402 | class AppInterceptors extends Interceptor { 403 | @override 404 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) { 405 | // Add headers or tokens if needed 406 | super.onRequest(options, handler); 407 | } 408 | 409 | 410 | @override 411 | void onResponse(Response response, ResponseInterceptorHandler handler) { 412 | super.onResponse(response, handler); 413 | } 414 | 415 | 416 | @override 417 | void onError(DioException err, ErrorInterceptorHandler handler) { 418 | super.onError(err, handler); 419 | } 420 | } 421 | '''; 422 | 423 | String _statusCodeCode() => ''' 424 | class StatusCode { 425 | static const int ok = 200; 426 | static const int badRequest = 400; 427 | static const int unauthorized = 401; 428 | static const int forbidden = 403; 429 | static const int notFound = 404; 430 | static const int conflict = 409; 431 | static const int internalServerError = 500; 432 | } 433 | '''; 434 | 435 | String _exceptionsCode() => ''' 436 | abstract class AppException implements Exception { 437 | final String message; 438 | final int statusCode; 439 | 440 | 441 | const AppException(this.message, this.statusCode); 442 | } 443 | 444 | 445 | class ServerException extends AppException { 446 | const ServerException([super.message = 'Server Error', super.statusCode = 500]); 447 | } 448 | 449 | 450 | class CacheException extends AppException { 451 | const CacheException([super.message = 'Cache Error', super.statusCode = 500]); 452 | } 453 | // Add more exceptions as needed 454 | 455 | 456 | '''; 457 | 458 | String _failuresCode() => ''' 459 | abstract class Failure { 460 | final String message; 461 | final int statusCode; 462 | 463 | 464 | Failure(this.message, this.statusCode); 465 | } 466 | 467 | 468 | class ServerFailure extends Failure { 469 | ServerFailure([super.message = 'Server Error', super.statusCode = 500]); 470 | } 471 | 472 | 473 | class CacheFailure extends Failure { 474 | CacheFailure([super.message = 'Cache Error', super.statusCode = 500]); 475 | } 476 | // Add more failures as needed 477 | 478 | 479 | '''; 480 | 481 | String _appUtilsCode() => ''' 482 | class AppUtils { 483 | static bool isEmailValid(String email) { 484 | return RegExp(r'^[^@]+@[^@]+.[^@]+').hasMatch(email); 485 | } 486 | 487 | 488 | static bool hasLowerCase(String password) { 489 | return RegExp(r'^(?=.*[a-z])').hasMatch(password); 490 | } 491 | 492 | 493 | static bool hasUpperCase(String password) { 494 | return RegExp(r'^(?=.*[A-Z])').hasMatch(password); 495 | } 496 | 497 | 498 | static bool hasNumber(String password) { 499 | return RegExp(r'^(?=.*?[0-9])').hasMatch(password); 500 | } 501 | 502 | 503 | static bool hasMinLength(String password) { 504 | return RegExp(r'^(?=.{8,})').hasMatch(password); 505 | } 506 | } 507 | 508 | 509 | '''; 510 | 511 | String _appPrefsCode() => ''' 512 | import 'package:shared_preferences/shared_preferences.dart'; 513 | import 'dart:convert'; 514 | 515 | 516 | class AppPreferences { 517 | static final AppPreferences _instance = AppPreferences._internal(); 518 | late SharedPreferences _prefs; 519 | 520 | 521 | factory AppPreferences() { 522 | return _instance; 523 | } 524 | 525 | 526 | AppPreferences._internal(); 527 | 528 | 529 | /// ✅ **تهيئة SharedPreferences** 530 | Future init() async { 531 | _prefs = await SharedPreferences.getInstance(); 532 | } 533 | 534 | 535 | /// ✅ **حفظ البيانات بأي نوع (`String, int, double, bool, List`)** 536 | Future setData(String key, dynamic value) async { 537 | if (value is String) { 538 | await _prefs.setString(key, value); 539 | } else if (value is int) { 540 | await _prefs.setInt(key, value); 541 | } else if (value is double) { 542 | await _prefs.setDouble(key, value); 543 | } else if (value is bool) { 544 | await _prefs.setBool(key, value); 545 | } else if (value is List) { 546 | await _prefs.setStringList(key, value); 547 | } else { 548 | throw Exception("Unsupported data type"); 549 | } 550 | } 551 | 552 | 553 | /// ✅ **استرجاع البيانات (`String, int, double, bool, List`)** 554 | dynamic getData(String key) { 555 | return _prefs.get(key); 556 | } 557 | 558 | 559 | /// ✅ **حذف بيانات مفتاح معين** 560 | Future removeData(String key) async { 561 | await _prefs.remove(key); 562 | } 563 | 564 | 565 | /// ✅ **حفظ كائن Model (`T`)** 566 | Future saveModel(String key, T model, Map Function(T) toJson) async { 567 | final String jsonString = jsonEncode(toJson(model)); 568 | await _prefs.setString(key, jsonString); 569 | } 570 | 571 | 572 | /// ✅ **استرجاع كائن Model (`T`)** 573 | T? getModel(String key, T Function(Map) fromJson) { 574 | final String? jsonString = _prefs.getString(key); 575 | if (jsonString != null) { 576 | final Map jsonMap = jsonDecode(jsonString); 577 | return fromJson(jsonMap); 578 | } 579 | return null; 580 | } 581 | 582 | 583 | /// ✅ **حفظ قائمة من النماذج (`List`)** 584 | Future saveModels(String key, List models, Map Function(T) toJson) async { 585 | final List jsonList = models.map((model) => jsonEncode(toJson(model))).toList(); 586 | await _prefs.setStringList(key, jsonList); 587 | } 588 | 589 | 590 | /// ✅ **استرجاع قائمة من النماذج (`List`)** 591 | List getModels(String key, T Function(Map) fromJson) { 592 | final List? jsonList = _prefs.getStringList(key); 593 | if (jsonList != null) { 594 | return jsonList.map((json) => fromJson(jsonDecode(json))).toList(); 595 | } 596 | return []; 597 | } 598 | 599 | 600 | Future clearExceptCredentials() async { 601 | // حفظ بيانات تسجيل الدخول قبل الحذف 602 | String? savedEmail = _prefs.getString('saved_email'); 603 | String? savedPassword = _prefs.getString('saved_password'); 604 | bool? rememberMe = _prefs.getBool('remember_me'); 605 | 606 | 607 | // مسح كل البيانات 608 | await _prefs.clear(); 609 | 610 | 611 | // استرجاع بيانات تسجيل الدخول 612 | if (savedEmail != null) await _prefs.setString('saved_email', savedEmail); 613 | if (savedPassword != null) await _prefs.setString('saved_password', savedPassword); 614 | if (rememberMe != null) await _prefs.setBool('remember_me', rememberMe); 615 | } 616 | bool isLoggedInUser() { 617 | return _prefs.containsKey("userModel"); 618 | } 619 | 620 | 621 | } 622 | '''; 623 | 624 | String _localeServiceCode() => ''' 625 | import 'package:flutter/material.dart'; 626 | import '../constants/app_constants.dart'; 627 | import 'package:easy_localization/easy_localization.dart'; 628 | 629 | 630 | import '../utils/app_shared_preferences.dart'; 631 | 632 | 633 | class LocaleService { 634 | 635 | 636 | LocaleService(); 637 | 638 | 639 | static const _defaultLocale = Locale('en'); 640 | 641 | 642 | /// Load saved locale or fallback 643 | Locale getCurrentLocale() { 644 | 645 | 646 | final localeCode = AppPreferences().getData(AppConstants.localeKey); 647 | if (localeCode != null) { 648 | return Locale(localeCode); 649 | } 650 | return _defaultLocale; 651 | } 652 | 653 | 654 | /// Save locale and update easy_localization 655 | Future setLocale(BuildContext context, String languageCode) async { 656 | await AppPreferences().setData(AppConstants.localeKey, languageCode); 657 | await context.setLocale(Locale(languageCode)); 658 | } 659 | } 660 | 661 | 662 | '''; 663 | 664 | String _themeServiceCode() => ''' 665 | import 'package:flutter/material.dart'; 666 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 667 | import '../theme/app_colors.dart'; 668 | 669 | 670 | 671 | 672 | class AppTheme { 673 | 674 | 675 | 676 | 677 | // 🌓 الوضع الفاتح 678 | static ThemeData get lightTheme { 679 | return ThemeData( 680 | brightness: Brightness.light, 681 | primaryColor: AppColor.primaryColor, 682 | scaffoldBackgroundColor: AppColor.backgroundColor, 683 | fontFamily: 'Nunito', 684 | colorScheme: ColorScheme( 685 | primary: AppColor.primaryColor, // اللون الثاني 686 | secondary: Colors.green, // اللون الثانوي الثاني 687 | surface: Colors.white, // خلفية التطبيقات 688 | error: Colors.red, // اللون الخاص بالأخطاء 689 | onPrimary: Colors.white, // اللون عند استخدام الـ primary 690 | onSecondary: Colors.black, // اللون عند استخدام الـ secondary 691 | onSurface: AppColor.grayColor, // اللون عند استخدام الـ background 692 | onError: Colors.white, // اللون عند استخدام الـ error 693 | brightness: Brightness.light, // مستوى السطوع (فاتح أو غامق) 694 | ), 695 | appBarTheme: const AppBarTheme( 696 | elevation: 0, 697 | centerTitle: true, 698 | backgroundColor: Colors.white, 699 | iconTheme: IconThemeData(color: Colors.black), 700 | titleTextStyle: TextStyle( 701 | color: Colors.black, 702 | fontSize: 18, 703 | fontWeight: FontWeight.bold, 704 | ), 705 | ), 706 | textTheme: TextTheme( 707 | displayLarge: textStyle(24.sp, FontWeight.bold, AppColor.black), 708 | displayMedium: textStyle(20.sp, FontWeight.bold, AppColor.black), 709 | displaySmall: textStyle(18.sp, FontWeight.w600, AppColor.black), 710 | headlineMedium: textStyle(16.sp, FontWeight.bold, AppColor.black), 711 | bodyLarge: textStyle(14.sp, FontWeight.normal, AppColor.black), 712 | bodyMedium: textStyle(12.sp, FontWeight.normal, AppColor.black), 713 | ), 714 | 715 | 716 | cardColor: AppColor.primaryColor, 717 | buttonTheme: ButtonThemeData( 718 | buttonColor: AppColor.backgroundColor, 719 | shape: RoundedRectangleBorder( 720 | borderRadius: BorderRadius.circular(8), 721 | ), 722 | ), 723 | elevatedButtonTheme: ElevatedButtonThemeData( 724 | style: ElevatedButton.styleFrom( 725 | foregroundColor: Colors.white, 726 | backgroundColor: AppColor.primaryColor, 727 | textStyle: textStyle(16, FontWeight.bold, Colors.white), 728 | shape: RoundedRectangleBorder( 729 | borderRadius: BorderRadius.circular(8), 730 | ), 731 | ), 732 | ), 733 | iconTheme: const IconThemeData(color: Colors.black), 734 | ); 735 | } 736 | 737 | 738 | // 🌙 الوضع الداكن 739 | static ThemeData get darkTheme { 740 | return ThemeData( 741 | brightness: Brightness.dark, 742 | primaryColor: AppColor.primaryColor, 743 | scaffoldBackgroundColor: AppColor.backgroundColor, 744 | fontFamily: 'Nunito', 745 | appBarTheme: const AppBarTheme( 746 | elevation: 0, 747 | centerTitle: true, 748 | backgroundColor: Colors.black, 749 | iconTheme: IconThemeData(color: Colors.white), 750 | titleTextStyle: TextStyle( 751 | color: Colors.white, 752 | fontSize: 18, 753 | fontWeight: FontWeight.bold, 754 | ), 755 | ), 756 | textTheme: TextTheme( 757 | displayLarge: textStyle(24.sp, FontWeight.bold, Colors.white), 758 | displayMedium: textStyle(20.sp, FontWeight.bold, Colors.white), 759 | displaySmall: textStyle(18.sp, FontWeight.w600, Colors.white), 760 | headlineMedium: textStyle(16.sp, FontWeight.bold, Colors.white), 761 | bodyLarge: textStyle(14.sp, FontWeight.normal, Colors.white), 762 | bodyMedium: textStyle(12.sp, FontWeight.normal, Colors.grey), 763 | ), 764 | cardColor: Colors.grey[900], 765 | buttonTheme: ButtonThemeData( 766 | buttonColor: AppColor.primaryColor, 767 | shape: RoundedRectangleBorder( 768 | borderRadius: BorderRadius.circular(8), 769 | ), 770 | ), 771 | elevatedButtonTheme: ElevatedButtonThemeData( 772 | style: ElevatedButton.styleFrom( 773 | foregroundColor: Colors.white, 774 | backgroundColor: AppColor.primaryColor, 775 | textStyle: textStyle(16, FontWeight.bold, Colors.white), 776 | shape: RoundedRectangleBorder( 777 | borderRadius: BorderRadius.circular(8), 778 | ), 779 | ), 780 | ), 781 | iconTheme: const IconThemeData(color: Colors.white), 782 | ); 783 | } 784 | 785 | 786 | // 🎨 دالة لإنشاء TextStyle بسهولة 787 | static TextStyle textStyle(double size, FontWeight weight, Color color) { 788 | return TextStyle( 789 | fontSize: size, 790 | fontWeight: weight, 791 | color: color, 792 | ); 793 | } 794 | } 795 | 796 | 797 | '''; 798 | 799 | String _navigationExtCode() => ''' 800 | //! NAVIGATION EXTENSION 801 | import 'package:flutter/material.dart'; 802 | 803 | 804 | extension NavigationExtensions on BuildContext { 805 | // Push a new page onto the stack 806 | void push(Widget page) => 807 | Navigator.of(this).push(MaterialPageRoute(builder: (_) => page)); 808 | 809 | 810 | // Push a named route onto the stack 811 | Future pushNamed(String routeName, {Object? arguments}) => 812 | Navigator.of(this).pushNamed(routeName, arguments: arguments); 813 | 814 | 815 | // Replace the current route with a new one 816 | Future pushReplacement(Widget page) => Navigator.of( 817 | this, 818 | ).pushReplacement(MaterialPageRoute(builder: (_) => page)); 819 | 820 | 821 | // Replace the current route with a named route 822 | Future pushReplacementNamed( 823 | String routeName, { 824 | Object? arguments, 825 | }) => Navigator.of( 826 | this, 827 | ).pushReplacementNamed(routeName, arguments: arguments); 828 | 829 | 830 | // Pop the current route off the stack 831 | void back() => Navigator.of(this).pop(); 832 | 833 | 834 | // Pop until the predicate returns true 835 | void popUntil(RoutePredicate predicate) => 836 | Navigator.of(this).popUntil(predicate); 837 | 838 | 839 | // Pop the current route and push a new route 840 | Future popAndPushNamed(String routeName, {Object? arguments}) => 841 | Navigator.of( 842 | this, 843 | ).popAndPushNamed(routeName, arguments: arguments); 844 | 845 | 846 | // Push a new route and remove all previous routes 847 | Future pushAndRemoveUntil(Widget page, RoutePredicate predicate) => 848 | Navigator.of( 849 | this, 850 | ).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => page), predicate); 851 | 852 | 853 | // Push a named route and remove all previous routes 854 | Future pushNamedAndRemoveUntil( 855 | String routeName, { 856 | Object? arguments, 857 | }) => Navigator.of(this).pushNamedAndRemoveUntil( 858 | routeName, 859 | (route) => false, 860 | arguments: arguments, 861 | ); 862 | 863 | 864 | // Try to pop the route; returns true if successful, otherwise false 865 | Future maybePop() => Navigator.of(this).maybePop(); 866 | 867 | 868 | // Replace the current route with a new route using a custom route 869 | Future replaceWithCustomRoute(Route route) => 870 | Navigator.of(this).pushReplacement(route); 871 | 872 | 873 | // Push a custom route onto the stack 874 | Future pushCustomRoute(Route route) => 875 | Navigator.of(this).push(route); 876 | } 877 | 878 | 879 | '''; 880 | 881 | String _sizedBoxExtCode() => ''' 882 | import 'package:flutter/material.dart'; 883 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 884 | 885 | 886 | SizedBox verticalSpace(double height) => SizedBox(height: height.h); 887 | SizedBox horizontalSpace(double width) => SizedBox(width: width.w); 888 | '''; 889 | 890 | String _themeExtCode() => ''' 891 | //! THEME EXTENSION 892 | import 'package:flutter/material.dart'; 893 | 894 | 895 | extension ThemeExtensions on BuildContext { 896 | //! Recommended to use: ThemeData get theme => Theme.of(this); 897 | ThemeData get theme => Theme.of(this); 898 | 899 | 900 | // Existing extensions 901 | IconThemeData get iconTheme => Theme.of(this).iconTheme; 902 | 903 | 904 | TextTheme get textTheme => Theme.of(this).textTheme; 905 | 906 | 907 | AppBarTheme get appBarTheme => Theme.of(this).appBarTheme; 908 | 909 | 910 | InputDecorationTheme get inputDecorationTheme => 911 | Theme.of(this).inputDecorationTheme; 912 | 913 | 914 | CheckboxThemeData get checkboxTheme => Theme.of(this).checkboxTheme; 915 | 916 | 917 | ElevatedButtonThemeData get elevatedButtonTheme => 918 | Theme.of(this).elevatedButtonTheme; 919 | 920 | 921 | OutlinedButtonThemeData get outlinedButtonTheme => 922 | Theme.of(this).outlinedButtonTheme; 923 | 924 | 925 | TextButtonThemeData get textButtonTheme => Theme.of(this).textButtonTheme; 926 | 927 | 928 | CardThemeData get cardTheme => Theme.of(this).cardTheme; 929 | 930 | 931 | DialogThemeData get dialogTheme => Theme.of(this).dialogTheme; 932 | 933 | 934 | FloatingActionButtonThemeData get floatingActionButtonTheme => 935 | Theme.of(this).floatingActionButtonTheme; 936 | 937 | 938 | BottomNavigationBarThemeData get bottomNavigationBarTheme => 939 | Theme.of(this).bottomNavigationBarTheme; 940 | 941 | 942 | NavigationRailThemeData get navigationRailTheme => 943 | Theme.of(this).navigationRailTheme; 944 | 945 | 946 | SliderThemeData get sliderTheme => Theme.of(this).sliderTheme; 947 | 948 | 949 | TabBarThemeData get tabBarTheme => Theme.of(this).tabBarTheme; 950 | 951 | 952 | TooltipThemeData get tooltipTheme => Theme.of(this).tooltipTheme; 953 | 954 | 955 | PopupMenuThemeData get popupMenuTheme => Theme.of(this).popupMenuTheme; 956 | 957 | 958 | MaterialBannerThemeData get bannerTheme => Theme.of(this).bannerTheme; 959 | 960 | 961 | DividerThemeData get dividerTheme => Theme.of(this).dividerTheme; 962 | 963 | 964 | BottomSheetThemeData get bottomSheetTheme => Theme.of(this).bottomSheetTheme; 965 | 966 | 967 | TimePickerThemeData get timePickerTheme => Theme.of(this).timePickerTheme; 968 | 969 | 970 | ThemeData get darkTheme => ThemeData.dark(); 971 | 972 | 973 | ThemeData get lightTheme => ThemeData.light(); 974 | 975 | 976 | // Additional extensions 977 | ButtonThemeData get buttonTheme => Theme.of(this).buttonTheme; 978 | 979 | 980 | ChipThemeData get chipTheme => Theme.of(this).chipTheme; 981 | 982 | 983 | DataTableThemeData get dataTableTheme => Theme.of(this).dataTableTheme; 984 | 985 | 986 | DrawerThemeData get drawerTheme => Theme.of(this).drawerTheme; 987 | 988 | 989 | ExpansionTileThemeData get expansionTileTheme => 990 | Theme.of(this).expansionTileTheme; 991 | 992 | 993 | ListTileThemeData get listTileTheme => Theme.of(this).listTileTheme; 994 | 995 | 996 | MenuThemeData get menuTheme => Theme.of(this).menuTheme; 997 | 998 | 999 | NavigationBarThemeData get navigationBarTheme => 1000 | Theme.of(this).navigationBarTheme; 1001 | 1002 | 1003 | PageTransitionsTheme get pageTransitionsTheme => 1004 | Theme.of(this).pageTransitionsTheme; 1005 | 1006 | 1007 | ProgressIndicatorThemeData get progressIndicatorTheme => 1008 | Theme.of(this).progressIndicatorTheme; 1009 | 1010 | 1011 | RadioThemeData get radioTheme => Theme.of(this).radioTheme; 1012 | 1013 | 1014 | ScrollbarThemeData get scrollbarTheme => Theme.of(this).scrollbarTheme; 1015 | 1016 | 1017 | SwitchThemeData get switchTheme => Theme.of(this).switchTheme; 1018 | 1019 | 1020 | TextSelectionThemeData get textSelectionTheme => 1021 | Theme.of(this).textSelectionTheme; 1022 | 1023 | 1024 | BottomAppBarTheme get bottomAppBarTheme => Theme.of(this).bottomAppBarTheme; 1025 | 1026 | 1027 | MaterialTapTargetSize get materialTapTargetSize => 1028 | Theme.of(this).materialTapTargetSize; 1029 | 1030 | 1031 | Typography get typography => Theme.of(this).typography; 1032 | 1033 | 1034 | VisualDensity get visualDensity => Theme.of(this).visualDensity; 1035 | 1036 | 1037 | IconButtonThemeData get iconButtonTheme => Theme.of(this).iconButtonTheme; 1038 | 1039 | 1040 | ColorScheme get colorScheme => Theme.of(this).colorScheme; 1041 | } 1042 | 1043 | 1044 | '''; 1045 | 1046 | String _splashScreenCode(projectName) => ''' 1047 | import 'package:flutter/material.dart'; 1048 | import '../../../../core/routing/routes.dart'; 1049 | import 'package:flutter_bloc/flutter_bloc.dart'; 1050 | import 'package:easy_localization/easy_localization.dart'; 1051 | import 'dart:async'; 1052 | import '../../../../core/extensions/navigation_extensions.dart'; 1053 | 1054 | 1055 | class SplashScreen extends StatefulWidget { 1056 | const SplashScreen({super.key}); 1057 | 1058 | 1059 | @override 1060 | State createState() => _SplashScreenState(); 1061 | } 1062 | 1063 | 1064 | class _SplashScreenState extends State { 1065 | @override 1066 | void initState() { 1067 | super.initState(); 1068 | Timer(const Duration(seconds: 2), () { 1069 | context.pushNamed(Routes.onBoardingScreen); 1070 | }); 1071 | } 1072 | 1073 | 1074 | @override 1075 | Widget build(BuildContext context) { 1076 | return const Scaffold( 1077 | body: Center( 1078 | child: Column( 1079 | mainAxisAlignment: MainAxisAlignment.center, 1080 | children: [ 1081 | FlutterLogo(size: 100), 1082 | SizedBox(height: 20), 1083 | Text("Splash Screen", style: TextStyle(fontSize: 20)), 1084 | ], 1085 | ), 1086 | ), 1087 | ); 1088 | } 1089 | } 1090 | 1091 | 1092 | '''; 1093 | String _localeCubitCode() => ''' 1094 | import 'package:flutter/material.dart'; 1095 | import 'package:flutter_bloc/flutter_bloc.dart'; 1096 | import '../../utils/app_shared_preferences.dart'; 1097 | import '../../constants/app_constants.dart'; 1098 | 1099 | 1100 | part 'locale_state.dart'; 1101 | 1102 | 1103 | class LocaleCubit extends Cubit { 1104 | LocaleCubit() : super(LocaleState(_getInitialLocale())); 1105 | 1106 | 1107 | static Locale _getInitialLocale() { 1108 | final savedLocale = AppPreferences().getData(AppConstants.localeKey); 1109 | return savedLocale == 'ar' ? const Locale('ar') : const Locale('en'); 1110 | } 1111 | 1112 | 1113 | Future changeLocale(Locale newLocale) async { 1114 | await AppPreferences().setData(AppConstants.localeKey, newLocale.languageCode); 1115 | emit(LocaleState(newLocale)); 1116 | } 1117 | } 1118 | 1119 | 1120 | '''; 1121 | 1122 | String _themeCubitCode() => ''' 1123 | import 'package:flutter/material.dart'; 1124 | import 'package:flutter_bloc/flutter_bloc.dart'; 1125 | import '../../utils/app_shared_preferences.dart'; 1126 | import '../../constants/app_constants.dart'; 1127 | 1128 | 1129 | part 'theme_state.dart'; 1130 | 1131 | 1132 | class ThemeCubit extends Cubit { 1133 | ThemeCubit() : super(ThemeState(_getInitialTheme())); 1134 | 1135 | 1136 | static ThemeMode _getInitialTheme() { 1137 | final savedTheme = AppPreferences().getData(AppConstants.themeKey); 1138 | if (savedTheme == 'dark') return ThemeMode.dark; 1139 | if (savedTheme == 'light') return ThemeMode.light; 1140 | return ThemeMode.system; 1141 | } 1142 | 1143 | 1144 | Future changeTheme(ThemeMode newTheme) async { 1145 | await AppPreferences().setData(AppConstants.themeKey, newTheme.name); 1146 | emit(ThemeState(newTheme)); 1147 | } 1148 | } 1149 | 1150 | 1151 | '''; 1152 | 1153 | String _appRouterCode(String projectName) => ''' 1154 | import 'package:flutter/material.dart'; 1155 | import '../routing/routes.dart'; 1156 | import '../../features/splash/presentation/screens/splash_screen.dart'; 1157 | import '../../features/onboarding/presentation/screens/onboarding_screen.dart'; 1158 | 1159 | 1160 | class AppRouter { 1161 | Route? generateRoute(RouteSettings settings) { 1162 | switch (settings.name) { 1163 | case Routes.splashScreen: 1164 | return _createRoute(const SplashScreen()); 1165 | case Routes.onBoardingScreen: 1166 | return _createRoute(const OnboardingScreen()); 1167 | 1168 | 1169 | default: 1170 | return null; 1171 | } 1172 | } 1173 | 1174 | 1175 | PageRouteBuilder _createRoute(Widget page) { 1176 | return PageRouteBuilder( 1177 | transitionDuration: const Duration(milliseconds: 400), 1178 | pageBuilder: (context, animation, secondaryAnimation) => page, 1179 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 1180 | return FadeTransition( 1181 | opacity: animation, 1182 | child: child, 1183 | ); 1184 | }, 1185 | ); 1186 | } 1187 | } 1188 | '''; 1189 | 1190 | String _routesCode() => ''' 1191 | class Routes { 1192 | static const String splashScreen = '/splashScreen'; 1193 | static const String onBoardingScreen = '/onBoardingScreen'; 1194 | static const String loginScreen = '/loginScreen'; 1195 | static const String signupScreen = '/signupScreen'; 1196 | static const String homeScreen = '/homeScreen'; 1197 | } 1198 | '''; 1199 | 1200 | String _appThemeCode() => ''' 1201 | import 'package:flutter/material.dart'; 1202 | import 'app_colors.dart'; 1203 | 1204 | 1205 | class AppTheme { 1206 | static ThemeData get lightTheme => ThemeData( 1207 | primaryColor: AppColor.primaryColor, 1208 | scaffoldBackgroundColor: AppColor.backgroundColor, 1209 | fontFamily: 'Nunito', 1210 | brightness: Brightness.light, 1211 | appBarTheme: const AppBarTheme( 1212 | backgroundColor: AppColor.white, 1213 | elevation: 0, 1214 | centerTitle: true, 1215 | iconTheme: IconThemeData(color: AppColor.black), 1216 | titleTextStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: AppColor.black), 1217 | ), 1218 | ); 1219 | 1220 | 1221 | static ThemeData get darkTheme => ThemeData( 1222 | primaryColor: AppColor.primaryColor, 1223 | scaffoldBackgroundColor: Colors.black, 1224 | fontFamily: 'Nunito', 1225 | brightness: Brightness.dark, 1226 | appBarTheme: const AppBarTheme( 1227 | backgroundColor: Colors.black, 1228 | elevation: 0, 1229 | centerTitle: true, 1230 | iconTheme: IconThemeData(color: AppColor.white), 1231 | titleTextStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: AppColor.white), 1232 | ), 1233 | ); 1234 | } 1235 | '''; 1236 | 1237 | String _appColorsCode() => ''' 1238 | import 'package:flutter/material.dart'; 1239 | 1240 | 1241 | class AppColor { 1242 | static const Color primaryColor = Color(0xff1F21A8); 1243 | static const Color backgroundColor = Color(0xffF8FBFC); 1244 | static const Color grayColor = Colors.grey; 1245 | static const Color white = Colors.white; 1246 | static const Color black = Colors.black; 1247 | } 1248 | '''; 1249 | 1250 | String _mainCode() => ''' 1251 | import 'package:flutter/material.dart'; 1252 | import 'package:flutter_bloc/flutter_bloc.dart'; 1253 | import 'package:easy_localization/easy_localization.dart'; 1254 | import 'core/constants/app_constants.dart'; 1255 | import 'core/cubit/locale/locale_cubit.dart'; 1256 | import 'core/cubit/theme/theme_cubit.dart'; 1257 | import 'core/utils/app_shared_preferences.dart'; 1258 | import 'core/routing/app_router.dart'; 1259 | import 'app.dart'; 1260 | import 'app_bloc_observer.dart'; 1261 | 1262 | 1263 | void main() async { 1264 | WidgetsFlutterBinding.ensureInitialized(); 1265 | await EasyLocalization.ensureInitialized(); 1266 | Bloc.observer = AppBlocObserver(); 1267 | await AppPreferences().init(); 1268 | 1269 | 1270 | runApp(EasyLocalization( 1271 | supportedLocales: AppConstants.supportedLocales, 1272 | path: 'assets/lang', 1273 | fallbackLocale: const Locale('en'), 1274 | child: MultiBlocProvider( 1275 | providers: [ 1276 | BlocProvider(create: (_) => LocaleCubit()), 1277 | BlocProvider(create: (_) => ThemeCubit()), 1278 | ], 1279 | child: MyApp(appRouter: AppRouter()), 1280 | ), 1281 | )); 1282 | } 1283 | '''; 1284 | 1285 | String _appCode() => ''' 1286 | import 'package:flutter/material.dart'; 1287 | import 'package:flutter_bloc/flutter_bloc.dart'; 1288 | import 'package:easy_localization/easy_localization.dart'; 1289 | import 'core/theme/app_theme.dart'; 1290 | import 'core/routing/app_router.dart'; 1291 | import 'core/constants/app_constants.dart'; 1292 | import 'core/cubit/theme/theme_cubit.dart'; 1293 | 1294 | 1295 | class MyApp extends StatelessWidget { 1296 | final AppRouter appRouter; 1297 | const MyApp({super.key, required this.appRouter}); 1298 | 1299 | 1300 | @override 1301 | Widget build(BuildContext context) { 1302 | return BlocBuilder( 1303 | builder: (context, themeState) => MaterialApp( 1304 | debugShowCheckedModeBanner: false, 1305 | title: AppConstants.appName, 1306 | theme: AppTheme.lightTheme, 1307 | darkTheme: AppTheme.darkTheme, 1308 | themeMode: themeState.themeMode, 1309 | locale: context.locale, 1310 | supportedLocales: context.supportedLocales, 1311 | localizationsDelegates: context.localizationDelegates, 1312 | onGenerateRoute: appRouter.generateRoute, 1313 | initialRoute: '/', 1314 | ), 1315 | ); 1316 | } 1317 | } 1318 | '''; 1319 | 1320 | String _blocObserverCode() => ''' 1321 | import 'package:flutter_bloc/flutter_bloc.dart'; 1322 | 1323 | 1324 | class AppBlocObserver extends BlocObserver { 1325 | @override 1326 | void onCreate(BlocBase bloc) { 1327 | super.onCreate(bloc); 1328 | print('🔍 Bloc Created: \${bloc.runtimeType}'); 1329 | } 1330 | 1331 | 1332 | @override 1333 | void onChange(BlocBase bloc, Change change) { 1334 | super.onChange(bloc, change); 1335 | print('🔁 Bloc Change in \${bloc.runtimeType}: \$change'); 1336 | } 1337 | 1338 | 1339 | @override 1340 | void onError(BlocBase bloc, Object error, StackTrace stackTrace) { 1341 | print('❌ Bloc Error in \${bloc.runtimeType}: \$error'); 1342 | super.onError(bloc, error, stackTrace); 1343 | } 1344 | 1345 | 1346 | @override 1347 | void onClose(BlocBase bloc) { 1348 | print('🛑 Bloc Closed: \${bloc.runtimeType}'); 1349 | super.onClose(bloc); 1350 | } 1351 | } 1352 | '''; 1353 | String _onboardingScreenCode(String projectName) => ''' 1354 | import 'package:flutter/material.dart'; 1355 | 1356 | 1357 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 1358 | import 'package:google_fonts/google_fonts.dart'; 1359 | 1360 | 1361 | class OnboardingScreen extends StatefulWidget { 1362 | const OnboardingScreen({super.key}); 1363 | 1364 | 1365 | @override 1366 | State createState() => _OnboardingScreenState(); 1367 | } 1368 | 1369 | 1370 | class _OnboardingScreenState extends State { 1371 | final controller = PageController(); 1372 | bool isLastPage = false; 1373 | 1374 | 1375 | @override 1376 | void dispose() { 1377 | controller.dispose(); 1378 | super.dispose(); 1379 | } 1380 | 1381 | 1382 | @override 1383 | Widget build(BuildContext context) { 1384 | return Scaffold( 1385 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 1386 | body: Padding( 1387 | padding: EdgeInsets.symmetric(horizontal: 24.w), 1388 | child: Column( 1389 | children: [ 1390 | SizedBox(height: 60.h), 1391 | Expanded( 1392 | child: PageView( 1393 | controller: controller, 1394 | onPageChanged: (index) => setState(() => isLastPage = index == 2), 1395 | children: const [ 1396 | OnboardPage(title: 'Welcome', description: 'This is onboarding 1'), 1397 | OnboardPage(title: 'Explore', description: 'This is onboarding 2'), 1398 | OnboardPage(title: 'Start', description: 'This is onboarding 3'), 1399 | ], 1400 | ), 1401 | ), 1402 | 1403 | SizedBox(height: 20.h), 1404 | SizedBox( 1405 | width: double.infinity, 1406 | child: ElevatedButton( 1407 | onPressed: () { 1408 | if (isLastPage) { 1409 | } else { 1410 | controller.nextPage( 1411 | duration: const Duration(milliseconds: 500), 1412 | curve: Curves.easeInOut, 1413 | ); 1414 | } 1415 | }, 1416 | child: Text(isLastPage ? 'Get Started' : 'Next'), 1417 | ), 1418 | ), 1419 | SizedBox(height: 40.h), 1420 | ], 1421 | ), 1422 | ), 1423 | ); 1424 | } 1425 | } 1426 | 1427 | 1428 | class OnboardPage extends StatelessWidget { 1429 | final String title; 1430 | final String description; 1431 | const OnboardPage({super.key, required this.title, required this.description}); 1432 | 1433 | 1434 | @override 1435 | Widget build(BuildContext context) { 1436 | return Padding( 1437 | padding: EdgeInsets.symmetric(horizontal: 24.w), 1438 | child: Column( 1439 | mainAxisAlignment: MainAxisAlignment.center, 1440 | children: [ 1441 | Icon(Icons.flutter_dash, size: 120.r), 1442 | SizedBox(height: 20.h), 1443 | Text( 1444 | title, 1445 | style: GoogleFonts.nunito( 1446 | fontSize: 26.sp, 1447 | fontWeight: FontWeight.bold, 1448 | ), 1449 | ), 1450 | SizedBox(height: 12.h), 1451 | Text( 1452 | description, 1453 | textAlign: TextAlign.center, 1454 | style: GoogleFonts.nunito(fontSize: 16.sp), 1455 | ), 1456 | ], 1457 | ), 1458 | ); 1459 | } 1460 | } 1461 | 1462 | 1463 | '''; 1464 | 1465 | String _loginScreenCode(String projectName) => ''' 1466 | import 'package:flutter/material.dart'; 1467 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 1468 | import 'package:$projectName/core/routing/routes.dart'; 1469 | 1470 | 1471 | class LoginScreen extends StatelessWidget { 1472 | const LoginScreen({super.key}); 1473 | 1474 | 1475 | @override 1476 | Widget build(BuildContext context) { 1477 | return Scaffold( 1478 | appBar: AppBar(title: const Text('Login')), 1479 | body: Padding( 1480 | padding: EdgeInsets.all(20.w), 1481 | child: Column( 1482 | crossAxisAlignment: CrossAxisAlignment.stretch, 1483 | children: [ 1484 | TextField(decoration: const InputDecoration(labelText: 'Email')), 1485 | SizedBox(height: 16.h), 1486 | TextField(obscureText: true, decoration: const InputDecoration(labelText: 'Password')), 1487 | SizedBox(height: 24.h), 1488 | ElevatedButton( 1489 | onPressed: () => Navigator.pushReplacementNamed(context, Routes.homeScreen), 1490 | child: const Text('Login'), 1491 | ), 1492 | SizedBox(height: 8.h), 1493 | TextButton( 1494 | onPressed: () => Navigator.pushNamed(context, Routes.signupScreen), 1495 | child: const Text("Don\'t have an account? Sign up"), 1496 | ), 1497 | ], 1498 | ), 1499 | ), 1500 | ); 1501 | } 1502 | } 1503 | '''; 1504 | 1505 | String _signupScreenCode() => ''' 1506 | import 'package:flutter/material.dart'; 1507 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 1508 | 1509 | 1510 | class SignUpScreen extends StatelessWidget { 1511 | const SignUpScreen({super.key}); 1512 | 1513 | 1514 | @override 1515 | Widget build(BuildContext context) { 1516 | return Scaffold( 1517 | appBar: AppBar(title: const Text('Sign Up')), 1518 | body: Padding( 1519 | padding: EdgeInsets.all(20.w), 1520 | child: Column( 1521 | crossAxisAlignment: CrossAxisAlignment.stretch, 1522 | children: [ 1523 | const TextField(decoration: InputDecoration(labelText: 'Name')), 1524 | SizedBox(height: 16.h), 1525 | const TextField(decoration: InputDecoration(labelText: 'Email')), 1526 | SizedBox(height: 16.h), 1527 | const TextField(obscureText: true, decoration: InputDecoration(labelText: 'Password')), 1528 | SizedBox(height: 24.h), 1529 | ElevatedButton( 1530 | onPressed: () {}, 1531 | child: const Text('Create Account'), 1532 | ), 1533 | ], 1534 | ), 1535 | ), 1536 | ); 1537 | } 1538 | } 1539 | '''; 1540 | String _themeStateCode() => ''' 1541 | part of 'theme_cubit.dart'; 1542 | 1543 | 1544 | class ThemeState { 1545 | final ThemeMode themeMode; 1546 | 1547 | 1548 | const ThemeState(this.themeMode); 1549 | 1550 | 1551 | @override 1552 | bool operator ==(Object other) { 1553 | if (identical(this, other)) return true; 1554 | return other is ThemeState && other.themeMode == themeMode; 1555 | } 1556 | 1557 | 1558 | @override 1559 | int get hashCode => themeMode.hashCode; 1560 | } 1561 | '''; 1562 | 1563 | String _localeStateCode() => ''' 1564 | part of 'locale_cubit.dart'; 1565 | 1566 | 1567 | class LocaleState { 1568 | final Locale locale; 1569 | 1570 | 1571 | const LocaleState(this.locale); 1572 | 1573 | 1574 | @override 1575 | bool operator ==(Object other) { 1576 | if (identical(this, other)) return true; 1577 | return other is LocaleState && other.locale == locale; 1578 | } 1579 | 1580 | 1581 | @override 1582 | int get hashCode => locale.hashCode; 1583 | } 1584 | '''; 1585 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `extension.js` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `extension.js` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `extension.js`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) 31 | * Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` 32 | * See the output of the test result in the Test Results view. 33 | * Make changes to `test/extension.test.js` or create new test files inside the `test` folder. 34 | * The provided test runner will only consider files matching the name pattern `**.test.js`. 35 | * You can create folders inside the `test` folder to structure your tests any way you want. 36 | 37 | ## Go further 38 | 39 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 40 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 41 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 42 | * Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users. 43 | --------------------------------------------------------------------------------