diff --git a/.gitignore b/.gitignore index 24476c5..f2335df 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/.metadata b/.metadata index 148c84a..5b39692 100644 --- a/.metadata +++ b/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - channel: stable + revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" + channel: "stable" project_type: app @@ -13,17 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - platform: android - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - platform: ios - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + - platform: linux + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + - platform: macos + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - platform: web - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + - platform: windows + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 # User provided section diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2ba986f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f83718b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Hamad Anwar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 22cb116..11d08db 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,84 @@ -# flutter_portfolio +# Responsive Flutter Portfolio Application & Website -A new Flutter project. +Thank you for visiting my portfolio website repository. This Flutter-based website serves as a showcase of my skills, projects, certifications, and ways to get in touch with me. The website is designed with a strong focus on responsiveness, ensuring that it looks and works flawlessly across a range of devices, from large desktop screens to compact Android devices. + +## Live Demo + +You can explore the live version of the website [Click to see live demo](https://hamad-anwar.github.io/Portfolio/#/). + +## Screenshots + + + +## Table of Contents + +- [Key Features](#key-features) +- [Technologies and Packages Used](#technologies-and-packages-used) +- [Getting Started](#getting-started) +- [Usage Guide](#usage-guide) +- [Contributions](#contributions) +- [Contact Me](#contact-me) +- [License](#license) + +## Key Features + +- **Responsive Design:** The portfolio website is meticulously designed to provide a consistent and visually pleasing experience across a wide variety of devices. Whether you're accessing the website on a large desktop screen, a laptop, a tablet, or a small Android smartphone, the layout and content will adapt gracefully to ensure optimal usability. + +- **Project Showcase:** The heart of the portfolio lies in its project showcase. Each project is presented with a captivating card that provides a glimpse of the project's essence. Visitors have the option to click on these cards to delve deeper into the details of each project. Furthermore, a direct link to the corresponding GitHub repository allows visitors to explore the codebase and gain a comprehensive understanding of the project's technical aspects. + +- **Certifications and Achievements:** I believe in continuous learning and growth, which is why the portfolio features a dedicated section showcasing my certifications and achievements. This provides insight into my professional journey, highlighting the skills and expertise I've acquired along the way. + +- **Contact and Interaction:** To facilitate easy communication, the portfolio provides multiple avenues to get in touch with me. The contact section features information such as my email address, LinkedIn profile, and Twitter handle. Whether you're a potential collaborator, an employer, or just someone interested in connecting, I'm always open to meaningful conversations. + +- **Elegant UI and Animations:** The user interface of the portfolio is thoughtfully designed to not only be functional but also visually appealing. Subtle animations are integrated throughout the website to create an engaging and delightful browsing experience. These animations are carefully balanced to enhance user engagement without overwhelming the content. + +## Technologies and Packages Used + +The portfolio website is built using Flutter, a powerful open-source UI software development toolkit. The following packages were utilized to enhance various aspects of the website: + +- [google_fonts](https://pub.dev/packages/google_fonts): Incorporates visually appealing and readable fonts from the Google Fonts library into the website. +- [flutter_svg](https://pub.dev/packages/flutter_svg): Enables the seamless integration and rendering of SVG images, ensuring high-quality graphics across all devices. +- [get](https://pub.dev/packages/get): Empowers efficient state management, simplifying the process of handling and updating UI components. +- [photo_view](https://pub.dev/packages/photo_view): Provides an elegant and user-friendly image viewer for an enhanced visual experience. +- [url_launcher](https://pub.dev/packages/url_launcher): Enables easy integration with external links, allowing visitors to quickly navigate to external resources. +- [font_awesome_flutter](https://pub.dev/packages/font_awesome_flutter): Introduces a wide variety of customizable icons from the FontAwesome library, enhancing the visual representation of the website's features. ## Getting Started -This project is a starting point for a Flutter application. +To explore and interact with the portfolio website on your local machine, follow these steps: + +1. **Clone the Repository:** + git clone https://github.com/Hamad-Anwar/Flutter-Responsive-Portfolio-WebApp.git +2. **Install Dependencies:** + flutter pub get +3. **Run Application** + flutter run + +## Usage Guide + +Once the website is up and running, you'll find a range of sections to explore: + +- **Home:** The landing page welcomes visitors with an overview of the website's contents and purpose. +- **Projects:** Navigate through my various projects, each displayed as an interactive card. Clicking on a card reveals in-depth information and a direct link to the GitHub repository. +- **Certifications:** Explore my certifications, gaining insight into my professional development journey. +- **Contact:** Reach out to me through provided contact details or social media links. + + +## Contributions + +I welcome contributions and suggestions from the community! If you come across any issues, have ideas for improvements, or wish to contribute in any way, feel free to open an issue or submit a pull request. Let's collaborate to make this portfolio even better! + +## Contact Me + +Your feedback and thoughts are highly valued. Feel free to connect with me through: + +- **Email:** rh676838@gmail.com +- **LinkedIn:** [Hamad Anwar](https://www.linkedin.com/in/hamad-anwar) + +## License -A few resources to get you started if this is your first Flutter project: +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +--- -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +### Designed and developed with ❤️ by [Hamad Anwar](https://www.linkedin.com/in/hamad-anwar/). diff --git a/assets/icons/arrow-ios-back-svgrepo-com.svg b/assets/icons/arrow-ios-back-svgrepo-com.svg new file mode 100644 index 0000000..2ac5d03 --- /dev/null +++ b/assets/icons/arrow-ios-back-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/copy-svgrepo-com (1).svg b/assets/icons/copy-svgrepo-com (1).svg new file mode 100644 index 0000000..bc1f01e --- /dev/null +++ b/assets/icons/copy-svgrepo-com (1).svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/github-svgrepo-com.svg b/assets/icons/github-svgrepo-com.svg new file mode 100644 index 0000000..696a5b6 --- /dev/null +++ b/assets/icons/github-svgrepo-com.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/github-svgrepo-com_gray.svg b/assets/icons/github-svgrepo-com_gray.svg new file mode 100644 index 0000000..6386dbc --- /dev/null +++ b/assets/icons/github-svgrepo-com_gray.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/gmail-icon-logo-svgrepo-com.svg b/assets/icons/gmail-icon-logo-svgrepo-com.svg new file mode 100644 index 0000000..cce06d3 --- /dev/null +++ b/assets/icons/gmail-icon-logo-svgrepo-com.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/icons/icons8-github.png b/assets/icons/icons8-github.png new file mode 100644 index 0000000..7a4b2cb Binary files /dev/null and b/assets/icons/icons8-github.png differ diff --git a/assets/icons/linkedin-svgrepo-com.svg b/assets/icons/linkedin-svgrepo-com.svg new file mode 100644 index 0000000..6f9ebb0 --- /dev/null +++ b/assets/icons/linkedin-svgrepo-com.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/icons/phone-svgrepo-com.svg b/assets/icons/phone-svgrepo-com.svg new file mode 100644 index 0000000..1fb80ee --- /dev/null +++ b/assets/icons/phone-svgrepo-com.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/icons/twitter.svg b/assets/icons/twitter.svg deleted file mode 100644 index a36ba4e..0000000 --- a/assets/icons/twitter.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/whatsapp-svgrepo-com.svg b/assets/icons/whatsapp-svgrepo-com.svg new file mode 100644 index 0000000..303c79a --- /dev/null +++ b/assets/icons/whatsapp-svgrepo-com.svg @@ -0,0 +1,6 @@ + + + +whatsapp + + \ No newline at end of file diff --git a/assets/images/alarm.jpg b/assets/images/alarm.jpg deleted file mode 100644 index 17057a2..0000000 Binary files a/assets/images/alarm.jpg and /dev/null differ diff --git a/assets/images/car.png b/assets/images/car.png deleted file mode 100644 index 5d5b189..0000000 Binary files a/assets/images/car.png and /dev/null differ diff --git a/assets/images/chat.png b/assets/images/chat.png deleted file mode 100644 index 064028a..0000000 Binary files a/assets/images/chat.png and /dev/null differ diff --git a/assets/images/coffee.png b/assets/images/coffee.png deleted file mode 100644 index c516376..0000000 Binary files a/assets/images/coffee.png and /dev/null differ diff --git a/assets/images/cui.png b/assets/images/cui.png deleted file mode 100644 index bebb8f5..0000000 Binary files a/assets/images/cui.png and /dev/null differ diff --git a/assets/images/doctor.png b/assets/images/doctor.png deleted file mode 100644 index a8c6daa..0000000 Binary files a/assets/images/doctor.png and /dev/null differ diff --git a/assets/images/player.png b/assets/images/player.png deleted file mode 100644 index ffa04af..0000000 Binary files a/assets/images/player.png and /dev/null differ diff --git a/assets/images/profile.png b/assets/images/profile.png index 3566557..3dc2080 100644 Binary files a/assets/images/profile.png and b/assets/images/profile.png differ diff --git a/assets/images/recipe.png b/assets/images/recipe.png deleted file mode 100644 index 111ee00..0000000 Binary files a/assets/images/recipe.png and /dev/null differ diff --git a/assets/images/task.png b/assets/images/task.png deleted file mode 100644 index 76dd5c7..0000000 Binary files a/assets/images/task.png and /dev/null differ diff --git a/assets/images/triange_icon.png b/assets/images/triange_icon.png deleted file mode 100644 index 648c179..0000000 Binary files a/assets/images/triange_icon.png and /dev/null differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/flutter_native_splash.yaml b/flutter_native_splash.yaml new file mode 100644 index 0000000..edecf50 --- /dev/null +++ b/flutter_native_splash.yaml @@ -0,0 +1,156 @@ +flutter_native_splash: + # This package generates native code to customize Flutter's default white native splash screen + # with background color and splash image. + # Customize the parameters below, and run the following command in the terminal: + # dart run flutter_native_splash:create + # To restore Flutter's default white splash screen, run the following command in the terminal: + # dart run flutter_native_splash:remove + + # IMPORTANT NOTE: These parameter do not affect the configuration of Android 12 and later, which + # handle splash screens differently that prior versions of Android. Android 12 and later must be + # configured specifically in the android_12 section below. + + # color or background_image is the only required parameter. Use color to set the background + # of your splash screen to a solid color. Use background_image to set the background of your + # splash screen to a png image. This is useful for gradients. The image will be stretch to the + # size of the app. Only one parameter can be used, color and background_image cannot both be set. + color: "#000515" + + # background_image: "assets/images/splash_background_light.png" + # background_image_dark: "assets/images/splash_background_dark.png" + + # Optional parameters are listed below. To enable a parameter, uncomment the line by removing + # the leading # character. + + # The image parameter allows you to specify an image used in the splash screen. It must be a + # png file and should be sized for 4x pixel density. + #image: assets/splash.png + + # The branding property allows you to specify an image used as branding in the splash screen. + # It must be a png file. It is supported for Android, iOS and the Web. For Android 12, + # see the Android 12 section below. + #branding: assets/dart.png + + # To position the branding image at the bottom of the screen you can use bottom, bottomRight, + # and bottomLeft. The default values is bottom if not specified or specified something else. + #branding_mode: bottom + + # Set the branding padding from the bottom of the screen. The default value is 0 + # (Not supported on web yet) + # branding_bottom_padding: 24 + + # The color_dark, background_image_dark, image_dark, branding_dark are parameters that set the background + # and image when the device is in dark mode. If they are not specified, the app will use the + # parameters from above. If there is no parameter above, the app will use the light mode values. + # If the image_dark parameter is specified, color_dark or background_image_dark must be specified. + # color_dark and background_image_dark cannot both be set. + #color_dark: "#042a49" + #background_image_dark: "assets/dark-background.png" + #image_dark: assets/splash-invert.png + #branding_dark: assets/dart_dark.png + + # From Android 12 onwards, the splash screen is handled differently than in previous versions. + # Please visit https://developer.android.com/guide/topics/ui/splash-screen + # Following are specific parameters for Android 12+. + android_12: + # The image parameter sets the splash screen icon image. If this parameter is not specified, + # the app's launcher icon will be used instead. + # Please note that the splash screen will be clipped to a circle on the center of the screen. + # App icon with an icon background: This should be 960×960 pixels, and fit within a circle + # 640 pixels in diameter. + # App icon without an icon background: This should be 1152×1152 pixels, and fit within a circle + # 768 pixels in diameter. To fit a 1152x1152 image within a circle with a 768 diameter, simply + # ensure that the most important design elements of your image are placed within a circular area + # with a 768 diameter at the center of the 1152x1152 canvas. + #image: assets/android12splash.png + + # Splash screen background color. + color: "#000515" + + # App icon background color. + #icon_background_color: "#111111" + + # The branding property allows you to specify an image used as branding in the splash screen. + #branding: assets/dart.png + + # The image_dark, color_dark, icon_background_color_dark, and branding_dark set values that + # apply when the device is in dark mode. If they are not specified, the app will use the + # parameters from above. If there is no parameter above, the app will use the light mode values. + #image_dark: assets/android12splash-invert.png + #color_dark: "#042a49" + #icon_background_color_dark: "#eeeeee" + + # The android, ios and web parameters can be used to disable generating a splash screen on a given + # platform. + android: false + ios: false + web: true + + # Platform specific images can be specified with the following parameters, which will override + # the respective parameter. You may specify all, selected, or none of these parameters: + #color_android: "#42a5f5" + #color_dark_android: "#042a49" + #color_ios: "#42a5f5" + #color_dark_ios: "#042a49" + #color_web: "#42a5f5" + #color_dark_web: "#042a49" + #image_android: assets/splash-android.png + #image_dark_android: assets/splash-invert-android.png + #image_ios: assets/splash-ios.png + #image_dark_ios: assets/splash-invert-ios.png + #image_web: assets/splash-web.gif + #image_dark_web: assets/splash-invert-web.gif + #background_image_android: "assets/background-android.png" + #background_image_dark_android: "assets/dark-background-android.png" + #background_image_ios: "assets/background-ios.png" + #background_image_dark_ios: "assets/dark-background-ios.png" + #background_image_web: "assets/background-web.png" + #background_image_dark_web: "assets/dark-background-web.png" + #branding_android: assets/brand-android.png + #branding_bottom_padding_android: 24 + #branding_dark_android: assets/dart_dark-android.png + #branding_ios: assets/brand-ios.png + #branding_bottom_padding_ios: 24 + #branding_dark_ios: assets/dart_dark-ios.png + #branding_web: assets/brand-web.gif + #branding_dark_web: assets/dart_dark-web.gif + + # The position of the splash image can be set with android_gravity, ios_content_mode, and + # web_image_mode parameters. All default to center. + # + # android_gravity can be one of the following Android Gravity (see + # https://developer.android.com/reference/android/view/Gravity): bottom, center, + # center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal, + # fill_vertical, left, right, start, or top. android_gravity can be combined using the | operator to achieve multiple effects. + # For example: + # `android_gravity: fill|clip_vertical` - This will fill the width while maintaining the image's vertical aspect ratio + #android_gravity: center + # + # ios_content_mode can be one of the following iOS UIView.ContentMode (see + # https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill, + # scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight, + # bottomLeft, or bottomRight. + #ios_content_mode: center + # + # web_image_mode can be one of the following modes: center, contain, stretch, and cover. + #web_image_mode: center + + # The screen orientation can be set in Android with the android_screen_orientation parameter. + # Valid parameters can be found here: + # https://developer.android.com/guide/topics/manifest/activity-element#screen + #android_screen_orientation: sensorLandscape + + # To hide the notification bar, use the fullscreen parameter. Has no effect in web since web + # has no notification bar. Defaults to false. + # NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads. + # To show the notification bar, add the following code to your Flutter app: + # WidgetsFlutterBinding.ensureInitialized(); + # SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top], ); + #fullscreen: true + + # If you have changed the name(s) of your info.plist file(s), you can specify the filename(s) + # with the info_plist_files parameter. Remove only the # characters in the three lines below, + # do not remove any spaces: + #info_plist_files: + # - 'ios/Runner/Info-Debug.plist' + # - 'ios/Runner/Info-Release.plist' \ No newline at end of file diff --git a/lib/font_clamper_extension.dart b/lib/font_clamper_extension.dart new file mode 100644 index 0000000..d3c3339 --- /dev/null +++ b/lib/font_clamper_extension.dart @@ -0,0 +1,5 @@ +extension MyClamper on double { + fontClamper(double baseFont) { + return clamp(baseFont * 1, baseFont * 1.20); + } +} diff --git a/lib/main.dart b/lib/main.dart index 00366bc..c5ec5b3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,32 +1,39 @@ import 'package:flutter/material.dart'; import 'package:flutter_portfolio/res/constants.dart'; -import 'package:flutter_portfolio/view/splash/splash_view.dart'; +import 'package:flutter_portfolio/routing/app_router.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; void main() { runApp(const MyApp()); } + class MyApp extends StatelessWidget { const MyApp({super.key}); + @override Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - scaffoldBackgroundColor: bgColor, - useMaterial3: true, - textTheme: GoogleFonts.openSansTextTheme(Theme.of(context).textTheme) - .apply(bodyColor: Colors.white,) - .copyWith( - bodyText1: const TextStyle(color: bodyTextColor), - bodyText2: const TextStyle(color: bodyTextColor), - ), - ), - - home: SplashView() + return ScreenUtilInit( + designSize: const Size(600, 960), + minTextAdapt: true, + splitScreenMode: true, + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + scaffoldBackgroundColor: bgColor, + useMaterial3: true, + textTheme: + GoogleFonts.openSansTextTheme(Theme.of(context).textTheme) + .apply( + bodyColor: Colors.white, + ) + .copyWith( + bodyLarge: const TextStyle(color: bodyTextColor), + bodyMedium: const TextStyle(color: bodyTextColor), + ), + ), + routerConfig: AppRouter.router), ); } } - - diff --git a/lib/model/certificate_model.dart b/lib/model/certificate_model.dart index 85d3283..71a0e4c 100644 --- a/lib/model/certificate_model.dart +++ b/lib/model/certificate_model.dart @@ -5,7 +5,7 @@ class CertificateModel { final String skills; final String credential; - CertificateModel({ + const CertificateModel({ required this.name, required this.organization, required this.date, @@ -13,109 +13,3 @@ class CertificateModel { required this.credential, }); } - -List certificateList = [ - CertificateModel( - name: 'Getting started with Flutter Development', - organization: 'Coursera', - date: 'AUG 2023', - skills: 'Flutter . Dart', - credential: 'https://www.coursera.org/account/accomplishments/certificate/HQ4LFHSF4LKZ', - ), - - CertificateModel( - name: 'Flutter Essential Training: Build for Multiple Platforms', - organization: 'LinkedIn', - date: 'JUL 2023', - skills: 'Flutter . iOS Development . Android Development', - credential: 'https://www.linkedin.com/learning/certificates/450fc4e2f495726aea50a067caf586869ccf0cb92ebcc5a4c7b5648a95754a8f', - ), - - CertificateModel( - name: 'Complete Dart Learning', - organization: 'Udemy', - date: 'JUN 2023', - skills: 'Flutter . Dart . Programming', - credential: 'https://www.udemy.com/certificate/UC-5b01c756-0d20-4342-94e6-9d5860d1c95e/', - ), - - CertificateModel( - name: 'Flutter REST Movie App', - organization: 'Udemy', - date: 'AUG 2023', - skills: 'Flutter . Rest API\'s . Cloud', - credential: 'https://www.udemy.com/certificate/UC-22efc7ca-3df5-4f26-8025-4a1bd2672f19/', - ), - - CertificateModel( - name: 'Modularizing and Organizing Flutter Code', - organization: 'LinkedIn', - date: 'JUL 2023', - skills: 'Flutter . Clean Architecture', - credential: 'https://www.linkedin.com/learning/certificates/686276fa42629d4f1291da79ea46bfde0222954b60297a2e728c770768f23407', - ), - - - - CertificateModel( - name: 'Powering Your App with Live Web Data', - organization: 'LinkedIn', - date: 'JUL 2023', - skills: 'Flutter . Dart . Firebase . API\'s', - credential: 'https://www.linkedin.com/learning/certificates/643f05463ae529f24bd9ea66a6ead9a20469bdb875a9ddda048c698eda3ee7c1', - ), - - - - CertificateModel( - name: 'Firebase Cloud Firestore', - organization: 'LinkedIn', - date: 'JUL 2023', - skills: 'Flutter . Dart . Firebase . FireStore', - credential: 'https://www.linkedin.com/learning/certificates/8f8be25531d2bcdbab1972482150277f9a239a13ba4d314c0574638bf28d07d2', - ), - - CertificateModel( - name: 'Android App Security', - organization: 'LinkedIn', - date: 'JUL 2023', - skills: 'Pentesting . Android App', - credential: 'https://www.linkedin.com/learning/certificates/1c6581b35d06edfbd6275d09e84b068e813880bf7d217b703716962d7aca3518', - ), - - - CertificateModel( - name: 'Foundations of Cybersecurity', - organization: 'Grow with Google on Coursera', - date: 'JUL 2023', - skills: 'Ethical Hacking . Linux . Cyber Security ', - credential: 'https://coursera.org/share/67e5cb0dd7c478f1d7ec81079c3a40b8', - ), - - - - CertificateModel( - name: 'HTML, CSS, and Javascript for Web Developers', - organization: 'JUL Coursera', - date: 'JUL 2023', - skills: 'HTML . CSS . JS . Web Development', - credential: 'https://coursera.org/share/67e5cb0dd7c478f1d7ec81079c3a40b8', - ), - - CertificateModel( - name: 'Network Defense Essentials (NDE)', - organization: 'EC-Council', - date: 'JUL 2023', - skills: 'Cyber Security . Networking ', - credential: 'https://codered.eccouncil.org/certificate/43a2d2a7-40ed-4230-9e65-a9aa0935e651?logged=false', - ), - - - - - - - - - -]; diff --git a/lib/model/certifications_models_list.dart b/lib/model/certifications_models_list.dart new file mode 100644 index 0000000..3db57b2 --- /dev/null +++ b/lib/model/certifications_models_list.dart @@ -0,0 +1,60 @@ +import 'package:flutter_portfolio/model/certificate_model.dart'; + +List certificateList = const [ + CertificateModel( + name: 'Complete Flutter & Dart Development Course', + organization: 'Udemy', + date: 'June 2024', + skills: 'Flutter & Dart', + credential: + 'https://www.udemy.com/certificate/UC-b8522ac0-4831-4b14-9ef3-8ac58286bcd4/', + ), + CertificateModel( + name: 'Flutter Advanced Course Bloc and MVVM Pattern', + organization: 'Udemy', + date: 'Aug 2024', + skills: 'MVVM & BLOC', + credential: + 'https://www.udemy.com/certificate/UC-1c511e74-f3f2-41b0-9245-56f453b08132/', + ), + CertificateModel( + name: 'Mastering Flutter: Responsive & Adaptive UI Design', + organization: 'Udemy', + date: 'Sept 2024', + skills: 'Responsive & Adaptive Design Development', + credential: + 'https://www.udemy.com/certificate/UC-94461710-cac8-4f88-b06b-a6acc960b354/', + ), + CertificateModel( + name: 'Master Git & GitHub: Essential Skills for Developers', + organization: 'Udemy', + date: 'Dec 2024', + skills: 'Git & Github', + credential: + 'https://www.udemy.com/certificate/UC-5019f0fe-0ed7-4130-b657-0794991aa6b5/', + ), + CertificateModel( + name: 'Flutter Payment Integration: Stripe, PayPal & More!', + organization: 'Udemy', + date: 'April 2025', + skills: 'Payment Integration', + credential: + 'https://www.udemy.com/certificate/UC-1319dfa9-6134-44f5-a26a-0412a4ffce53/', + ), + CertificateModel( + name: 'SOLID Design Principles', + organization: 'Udemy', + date: 'June 2025', + skills: 'SOLID Principles', + credential: + 'https://www.udemy.com/certificate/UC-c6de6eaf-c0eb-4aed-9978-005df5aaf4fb/', + ), + CertificateModel( + name: 'Deep Dive into Clean Architecture in Flutter', + organization: 'Udemy', + date: 'Aug 2025', + skills: 'Clean Architecture', + credential: + 'https://www.udemy.com/certificate/UC-236b18d3-0c1d-4112-9156-b3e437c7b694/', + ), +]; diff --git a/lib/model/chefio_urls_helper.dart b/lib/model/chefio_urls_helper.dart new file mode 100644 index 0000000..eee5c2e --- /dev/null +++ b/lib/model/chefio_urls_helper.dart @@ -0,0 +1,391 @@ +abstract class ChefioUrlHelper { + static final ChefioUrlsLightMode _lightMode = ChefioUrlsLightMode(); + static final ChefioUrlsDarkMode _darkMode = ChefioUrlsDarkMode(); + static final ChefioVideoUrls _chefioVideo = ChefioVideoUrls(); + + static final ChefioUrlsPushNotifications _pushNotifications = + ChefioUrlsPushNotifications(); + static final List toBeUsedImages = [ + ..._lightImagesToBeUsed, + ..._pushNotificationImages, + ..._darkImagesToBeUsed, + ]; + static final List toBeUsedVideos = [ + _chefioVideo.generalVideo, + _chefioVideo.pushNotificationsVideo, + ]; + static final List _lightImagesToBeUsed = [ + _lightMode.onboarding, + _lightMode.logIn, + _lightMode.home, + _lightMode.search, + _lightMode.recipeDetailsUpper, + _lightMode.recipeDetialsLittleMid, + _lightMode.recipeDetailsIngredentsWithStep, + _lightMode.recipeDetailsSteps, + _lightMode.stepDialog, + _lightMode.profile, + _lightMode.profileLiked, + _lightMode.profileImage, + _lightMode.followersBottomSheet, + _lightMode.notifications, + _lightMode.uploadFirstStep, + _lightMode.uploadSecondStep, + _lightMode.editStepDialog, + _lightMode.uploadSuccessDialog, + _lightMode.editProfile, + _lightMode.settings, + _lightMode.themeSelection, + _lightMode.languageSelection, + _lightMode.signUp, + _lightMode.otp, + _lightMode.forgot, + _lightMode.resetPassword, + ]; + static final List _darkImagesToBeUsed = [ + _darkMode.onboarding, + _darkMode.logIn, + _darkMode.home, + _darkMode.search, + _darkMode.recipeDetailsUpper, + _darkMode.recipeDetialsLittleMid, + _darkMode.recipeDetailsIngredentsWithStep, + _darkMode.recipeDetailsSteps, + _darkMode.stepDialog, + _darkMode.profile, + _darkMode.profileLiked, + _darkMode.profileImage, + _darkMode.followersBottomSheet, + _darkMode.notifications, + _darkMode.uploadFirstStep, + _darkMode.uploadSecondStep, + _darkMode.editStepDialog, + _darkMode.uploadSuccessDialog, + _darkMode.editProfile, + _darkMode.settings, + _darkMode.themeSelection, + _darkMode.languageSelection, + _darkMode.signUp, + _darkMode.otp, + _darkMode.forgot, + _darkMode.resetPassword, + ]; + + static final List _pushNotificationImages = [ + _pushNotifications.fullNotification, + _pushNotifications.popupNotification, + ]; +} + +abstract class ChefioBasicImages { + String get onboarding; + String get resetPassword; + String get forgot; + String get signUp; + String get logIn; + String get otp; + + String get home; + String get search; + + String get uploadFirstStep; + String get uploadFirstStepBottom; + String get uploadSecondStep; + String get uploadSecondStepBottom; + String get uploadSuccessDialog; + + String get recipeDetailsUpper; + String get recipeDetialsLittleMid; + String get recipeDetailsIngredentsWithStep; + String get recipeDetailsSteps; + + String get profile; + String get profileLiked; + String get profileImage; + String get followersBottomSheet; + String get editProfile; + + String get editStepDialog; + String get notifications; + String get settings; + String get themeSelection; + String get languageSelection; + String get stepDialog; +} + +class ChefioUrlsPushNotifications { + static const ChefioUrlsPushNotifications _instance = + ChefioUrlsPushNotifications._(); + const ChefioUrlsPushNotifications._(); + factory ChefioUrlsPushNotifications() => _instance; + + final String fullNotification = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849903/Screenshot_1758835096_fa0s6l.png'; + final String popupNotification = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849906/Screenshot_1758835057_pxko4f.png'; +} + +class ChefioVideoUrls { + static const ChefioVideoUrls _instance = ChefioVideoUrls._(); + const ChefioVideoUrls._(); + factory ChefioVideoUrls() => _instance; + + final String pushNotificationsVideo = + 'https://res.cloudinary.com/deshi2o56/video/upload/v1758855634/2025-09-25_23-56-24_qdoqjy.mp4'; + final String generalVideo = + 'https://res.cloudinary.com/deshi2o56/video/upload/v1758855583/Screenrecorder-2025-09-25-05-17-43-258_1_swrbau.mp4'; +} + +class ChefioUrlsLightMode implements ChefioBasicImages { + static const ChefioUrlsLightMode _instance = ChefioUrlsLightMode._(); + const ChefioUrlsLightMode._(); + factory ChefioUrlsLightMode() => _instance; + + @override + final String onboarding = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1757058469/Screenshot_1749703206_botarb.png'; + @override + final String resetPassword = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1757058457/Screenshot_1749707798_qgc2pn.png'; + @override + final String forgot = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1757058457/Screenshot_1749707182_p5adw1.png'; + @override + final String signUp = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1757058457/Screenshot_1749704247_gfelky.png'; + @override + final String logIn = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1757058456/Screenshot_1749706470_yqpku4.png'; + @override + final String otp = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758847039/Screenshot_2025-09-21-19-34-10-701_com.example.chefio_app_ong5pk.jpg'; + + @override + final String home = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758847192/Screenshot_2025-09-21-22-26-59-231_com.example.chefio_app_bkde9a.jpg'; + @override + final String search = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758847417/Screenshot_2025-09-21-04-38-56-714_com.example.chefio_app_toovrt.jpg'; + + @override + final String uploadFirstStep = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758847635/Screenshot_2025-09-21-21-54-20-181_com.example.chefio_app_wcwqdr.jpg'; + @override + final String uploadFirstStepBottom = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848181/Screenshot_2025-09-21-21-54-21-950_com.example.chefio_app_mhvukl.jpg'; + @override + final String uploadSecondStep = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848232/Screenshot_2025-09-21-21-54-39-367_com.example.chefio_app_ydwzah.jpg'; + @override + final String uploadSecondStepBottom = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848224/Screenshot_2025-09-21-21-54-40-740_com.example.chefio_app_upyrch.jpg'; + @override + final String uploadSuccessDialog = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848269/Screenshot_2025-09-21-04-40-57-815_com.example.chefio_app_wqcwro.jpg'; + + @override + final String recipeDetailsUpper = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848527/Screenshot_2025-09-21-04-37-18-114_com.example.chefio_app_phkwav.jpg'; + @override + final String recipeDetialsLittleMid = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848525/Screenshot_2025-09-21-04-37-32-744_com.example.chefio_app_ewdr9d.jpg'; + + @override + final String recipeDetailsIngredentsWithStep = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848583/Screenshot_2025-09-21-04-37-41-410_com.example.chefio_app_q4exc9.jpg'; + @override + final String recipeDetailsSteps = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848587/Screenshot_2025-09-21-04-37-44-534_com.example.chefio_app_qi41l9.jpg'; + + @override + final String profile = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848401/Screenshot_2025-09-21-21-51-54-523_com.example.chefio_app_fz0shb.jpg'; + @override + final String profileLiked = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848405/Screenshot_2025-09-21-21-52-20-712_com.example.chefio_app_clrl6h.jpg'; + @override + final String profileImage = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848401/Screenshot_2025-09-21-04-38-06-868_com.example.chefio_app_s6gkkd.jpg'; + @override + final String followersBottomSheet = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848485/Screenshot_2025-09-21-04-38-18-448_com.example.chefio_app_pzctzl.jpg'; + @override + final String editProfile = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848688/Screenshot_2025-09-21-04-39-12-539_com.example.chefio_app_vz74gl.jpg'; + + @override + final String editStepDialog = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852943/Screenshot_2025-09-26-05-04-21-536_com.example.chefio_app_vgvoqo.jpg'; + @override + final String notifications = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848622/Screenshot_2025-09-26-03-00-01-921_com.example.chefio_app_teu89u.jpg'; + @override + final String settings = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848603/Screenshot_2025-09-21-21-55-00-873_com.example.chefio_app_z1lcgz.jpg'; + @override + final String themeSelection = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848610/Screenshot_2025-09-21-21-55-09-722_com.example.chefio_app_g1xnws.jpg'; + @override + final String languageSelection = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848601/Screenshot_2025-09-21-04-35-05-695_com.example.chefio_app_kzvp7u.jpg'; + @override + final String stepDialog = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758848524/Screenshot_2025-09-21-04-37-47-441_com.example.chefio_app_bdpqsk.jpg'; + + List get orderedImages => [ + onboarding, + signUp, + logIn, + forgot, + resetPassword, + otp, + home, + search, + uploadFirstStep, + uploadFirstStepBottom, + uploadSecondStep, + uploadSecondStepBottom, + uploadSuccessDialog, + recipeDetailsUpper, + recipeDetialsLittleMid, + recipeDetailsIngredentsWithStep, + recipeDetailsSteps, + profile, + profileImage, + profileLiked, + followersBottomSheet, + editProfile, + editStepDialog, + notifications, + settings, + themeSelection, + languageSelection, + ]; +} + +class ChefioUrlsDarkMode implements ChefioBasicImages { + static const ChefioUrlsDarkMode _instance = ChefioUrlsDarkMode._(); + const ChefioUrlsDarkMode._(); + factory ChefioUrlsDarkMode() => _instance; + + @override + final String onboarding = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849058/Screenshot_2025-09-16-01-41-27-912_com.example.chefio_app_lwrwpe.jpg'; + @override + final String resetPassword = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758859133/Screenshot_2025-09-21-22-25-37-039_com.example.chefio_app_k2ggp7.jpg'; + @override + final String forgot = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849051/Screenshot_2025-09-19-23-04-26-656_com.example.chefio_app_w0boi0.jpg'; + @override + final String signUp = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849059/Screenshot_2025-09-16-01-41-30-919_com.example.chefio_app_kkcwta.jpg'; + @override + final String logIn = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849050/Screenshot_2025-09-16-01-41-35-552_com.example.chefio_app_-_Copy_uzb1pd.jpg'; + @override + final String otp = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849052/Screenshot_2025-09-19-23-06-04-185_com.example.chefio_app_jgiek7.jpg'; + + @override + final String home = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849167/Screenshot_2025-09-20-02-04-40-671_com.example.chefio_app_rnmrqi.jpg'; + @override + final String search = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849213/Screenshot_2025-09-16-01-48-45-356_com.example.chefio_app_dj0qyf.jpg'; + + @override + final String uploadFirstStep = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852853/Screenshot_2025-09-26-04-37-37-543_com.example.chefio_app_gxbdue.jpg'; + @override + final String uploadFirstStepBottom = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852885/Screenshot_2025-09-26-04-37-41-288_com.example.chefio_app_d9if4m.jpg'; + @override + final String uploadSecondStep = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852912/Screenshot_2025-09-26-05-05-26-521_com.example.chefio_app_ixgdon.jpg'; + @override + final String uploadSecondStepBottom = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852917/Screenshot_2025-09-26-05-05-28-016_com.example.chefio_app_lyj0lm.jpg'; + @override + final String uploadSuccessDialog = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852934/Screenshot_2025-09-21-04-44-33-374_com.example.chefio_app_lpa2wp.jpg'; + @override + final String stepDialog = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849675/Screenshot_2025-09-19-23-18-33-063_com.example.chefio_app_wb26ru.jpg'; + + @override + final String recipeDetailsUpper = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849625/Screenshot_2025-09-19-23-14-56-915_com.example.chefio_app_dwcmdf.jpg'; + @override + final String recipeDetialsLittleMid = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849670/Screenshot_2025-09-19-23-15-39-442_com.example.chefio_app_eha2fn.jpg'; + @override + @override + final String recipeDetailsIngredentsWithStep = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849668/Screenshot_2025-09-19-23-15-33-909_com.example.chefio_app_o75kju.jpg'; + @override + final String recipeDetailsSteps = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849630/Screenshot_2025-09-19-23-15-29-593_com.example.chefio_app_w1hikx.jpg'; + + @override + final String profile = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852922/Screenshot_2025-09-26-05-09-59-787_com.example.chefio_app_fvesq0.jpg'; + @override + final String profileLiked = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852925/Screenshot_2025-09-26-05-10-03-130_com.example.chefio_app_nllmfm.jpg'; + @override + final String profileImage = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852851/Screenshot_2025-09-26-05-10-06-001_com.example.chefio_app_uwypue.jpg'; + @override + final String followersBottomSheet = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849519/Screenshot_2025-09-21-04-42-55-958_com.example.chefio_app_adx36d.jpg'; + @override + final String editProfile = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852909/Screenshot_2025-09-26-04-59-40-146_com.example.chefio_app_ylzfi5.jpg'; + + @override + final String editStepDialog = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758852896/Screenshot_2025-09-26-04-54-11-645_com.example.chefio_app_wq8zvm.jpg'; + @override + final String notifications = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849822/Screenshot_2025-09-26-03-01-31-158_com.example.chefio_app_sijl3u.jpg'; + @override + final String settings = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849812/Screenshot_2025-09-21-21-55-14-540_com.example.chefio_app_yyelok.jpg'; + @override + final String themeSelection = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849803/Screenshot_2025-09-21-21-55-11-592_com.example.chefio_app_whe8ax.jpg'; + @override + final String languageSelection = + 'https://res.cloudinary.com/deshi2o56/image/upload/v1758849710/Screenshot_2025-09-16-01-43-14-376_com.example.chefio_app_rz4ecd.jpg'; + + List get orderedImages => [ + onboarding, + signUp, + logIn, + forgot, + resetPassword, + otp, + home, + search, + uploadFirstStep, + uploadFirstStepBottom, + uploadSecondStep, + uploadSecondStepBottom, + uploadSuccessDialog, + recipeDetailsUpper, + recipeDetialsLittleMid, + recipeDetailsIngredentsWithStep, + recipeDetailsSteps, + profile, + profileImage, + profileLiked, + followersBottomSheet, + editProfile, + editStepDialog, + notifications, + settings, + themeSelection, + languageSelection, + ]; +} diff --git a/lib/model/feature_model.dart b/lib/model/feature_model.dart new file mode 100644 index 0000000..4b60431 --- /dev/null +++ b/lib/model/feature_model.dart @@ -0,0 +1,5 @@ +class FeatureModel { + final String feature; + final List featurePoints; + const FeatureModel({required this.feature, required this.featurePoints}); +} diff --git a/lib/model/project_model.dart b/lib/model/project_model.dart index 9eecf02..bc982ac 100644 --- a/lib/model/project_model.dart +++ b/lib/model/project_model.dart @@ -1,58 +1,18 @@ +import 'package:flutter_portfolio/model/feature_model.dart'; + class Project { final String name; final String description; - final String image; + final List images; + final List videos; + final List featureModelsList; final String link; - - Project(this.name, this.description, this.image, this.link); + const Project({ + required this.name, + required this.description, + required this.images, + required this.videos, + required this.link, + required this.featureModelsList, + }); } - -List projectList = [ - Project( - 'Coffee Application', - 'Presenting "Coffee" - a Flutter UI application that invites you to indulge in a delightful coffee adventure. Immerse yourself in the world of rich aroma and flavor as you browse through an exquisite selection of specialty coffees. With a user-friendly interface, exploring different blends and discovering their unique details becomes a delightful experience.', - 'assets/images/coffee.png', - 'https://github.com/Hamad-Anwar/Coffe-Shop-Beautifull-UI', - ), - Project( - 'Car Controller Application UI', - 'Car Control Dashboard is a user-friendly mobile application built using Flutter and powered by GetX for efficient state management. Enjoy a modern and intuitive design that adapts to various screen sizes, Interact with dynamic car controls, including speed, steering, and temperature. Realistic animations enhance the visual appeal, making it feel like you are controlling a real car!', - 'assets/images/car.png', - 'https://github.com/Hamad-Anwar/Car-Controller-Getx-Flutter', - ), - Project( - 'Neumorphic Clockify', - 'Neumorphic TimeKit is an open-source project that brings together the elegance of a beautifully designed alarm system, an analog clock with a neumorphic touch, and a feature-rich stopwatch. Whether you\'re looking for a functional alarm tool, a stylish desktop clock, or a precise stopwatch, TimeKit has you covered.', - 'assets/images/alarm.jpg', - 'https://github.com/Hamad-Anwar/Neumorphic-Analog-Clockify'), - Project( - 'CUI Aider', - 'CUI AIDER is an extraordinary application that brings together augmented reality (AR), a robust student portal, intuitive class management, and a comprehensive GPA system, providing an all-encompassing solution for students.Using AR technology, CUI AIDER offers an immersive experience, allowing users to explore the complete university campus virtually. Through AR, students can visualize buildings, navigate with interactive directions, and gain a better understanding of their surroundings.', - 'assets/images/cui.png', - 'https://github.com/Hamad-Anwar/'), - Project( - 'Flutter Music Streaming Application', - 'A Flutter music streaming application that allows users to play audio files from local storage. The app features a beautiful neumorphic UI design and provides a smooth user experience for browsing and playing music.', - 'assets/images/player.png', - 'https://github.com/Hamad-Anwar/Neumorphic-Music_Player-Flutter'), - Project( - 'Food Recipe App', - 'This repository contains a Flutter implementation of a Food Recipe app with a captivating introduction section and impressive animations. It also features a signup and signin page with animations, elegantly presented on a bottom sheet.', - 'assets/images/recipe.png', - 'https://github.com/Hamad-Anwar/Food-Recipe-App-Flutter'), - Project( - 'Task Sync Pro', - 'Welcome to the Beautiful Task Scheduler App repository! This Flutter-based task management application combines elegant design with a robust backend, ensuring a seamless and organized task management experience. From stunning UI to real-time synchronization, this app has you covered.', - 'assets/images/task.png', - 'https://github.com/Hamad-Anwar/Task-Sync-Pro-Flutter'), - Project( - 'Flutter Chat Application with Firebase', - 'Welcome to our innovative Flutter chat application! This feature-rich messaging platform allows users to connect and communicate seamlessly through text and images. The app is built using Flutter for the frontend and integrates with Firebase for backend services, including authentication, real-time database, and storage.', - 'assets/images/chat.png', - 'https://github.com/Hamad-Anwar/Messenger-App-Backend-Firebase'), - Project( - 'Doctor Appointment Application', - 'Introducing the extraordinary "Doctor Appointment System" - a state-of-the-art Flutter UI application that redefines healthcare accessibility and efficiency. Seamlessly crafted, this app empowers users to effortlessly select doctors based on categories, engage in smooth messaging, and access detailed profiles.', - 'assets/images/doctor.png', - 'https://github.com/Hamad-Anwar/Doctor-Appointment-Application-UI'), -]; diff --git a/lib/model/projects_models_list.dart b/lib/model/projects_models_list.dart new file mode 100644 index 0000000..87efbd3 --- /dev/null +++ b/lib/model/projects_models_list.dart @@ -0,0 +1,355 @@ +import 'package:flutter_portfolio/model/chefio_urls_helper.dart'; +import 'package:flutter_portfolio/model/feature_model.dart'; +import 'package:flutter_portfolio/model/project_model.dart'; + +List projectList = [ + Project( + name: 'Chefio – Recipe Sharing App', + description: + 'Chefio is a Full-Featured Recipe Sharing App.\nIt allows users to discover, upload, and share recipes with a beautifully crafted UI and smooth user experience.\n\nCore features include browsing and searching recipes, detailed recipe pages with step-by-step instructions, profile management with social interactions (follow/unfollow), and a secure authentication system with reusable OTP. \n\nThe app also supports push notifications, deep linking for shareable recipes and profiles, multi-language localization, light/dark theming with persistence\n\nArchitected with MVVM, Bloc (Cubit) state management, and clean modular structure.', + images: ChefioUrlHelper.toBeUsedImages, + videos: ChefioUrlHelper.toBeUsedVideos, + featureModelsList: [ + const FeatureModel( + feature: 'Home & Recipe Discovery', + featurePoints: [ + 'Browse all recipes or filter by category with infinite scrolling (pagination)', + 'Recipe Item showcasing image, name, duration, category, chef avatar, and like button', + 'Search integration from AppBar for quick access', + 'Optimized smooth scroll experience across large datasets', + ], + ), + const FeatureModel( + feature: 'Recipe Details', + featurePoints: [ + 'Large recipe image header with context-specific actions: Share, and (Edit/Delete for owner)', + 'Rich Infos: name, description, duration, chef details, likes count', + 'Interactive Like system with ability to view all likers', + 'Full ingredients list with clear formatting', + 'Step-by-step cooking instructions with optional images for better UX', + ], + ), + const FeatureModel( + feature: 'Upload & Edit Recipes', + featurePoints: [ + 'Multi-step recipe creation flow: add images, name, description, duration, and category', + 'Dedicated step/ingredients editor with delete, and update functionality', + 'Edit recipes with pre-filled data and image management (add/remove)', + 'Success dialog and clear feedback after submission', + ], + ), + const FeatureModel( + feature: 'Profiles & Social Features', + featurePoints: [ + 'Personal profile and other chefs’ profiles with separate state handling', + 'Stats dashboard: total recipes, followers, and following', + 'Follow/Unfollow system with instant UI updates', + 'Tabs for uploaded recipes and liked recipes', + 'Followers and following lists displayed in smooth bottom sheets', + ], + ), + const FeatureModel( + feature: 'Authentication & Session Management', + featurePoints: [ + 'Implemented Sign Up, Login, and Forgot Password flows', + 'Email-based OTP verification for sign-up and password reset', + 'Reusable OTP Feature built with Open/Closed Principle for easy extension', + 'Secure token handling and caching with flutter_secure_storage', + 'Robust refresh token & session management with API interceptor', + ], + ), + const FeatureModel( + feature: 'Push Notifications', + featurePoints: [ + 'Integrated Firebase Cloud Messaging for real-time notifications', + 'Notifications for: 💖 Someone Liked your recipe, ➕ Someone followed you, 🆕 A chef you follow uploaded a new recipe.', + 'Notification screen to view all received notifications', + ], + ), + const FeatureModel( + feature: 'Deep Linking & Sharing', + featurePoints: [ + 'Generated shareable links for recipes and profiles', + 'Supported deep linking to open links directly inside the app', + 'Integrated smoothly with GoRouter for seamless navigation', + ], + ), + const FeatureModel( + feature: 'Theming & Localization', + featurePoints: [ + 'Light and dark themes with persistence powered by HydratedCubit', + 'Multi-language support implemented using easy_localization', + 'Settings screen for theme and language switching', + 'Consistent theming and typography across modules', + ], + ), + const FeatureModel( + feature: 'Onboarding', + featurePoints: [ + 'Stylish one-screen onboarding with custom plate design illustration', + ], + ), + const FeatureModel( + feature: 'MVVM Architecture & Bloc (Cubit) State Management', + featurePoints: [ + 'MVVM architecture with Bloc (Cubit) for predictable state management', + 'Dependency Injection with get_it for decoupled and testable modules', + 'Scalable modular folder structure with clean feature separation', + ], + ), + const FeatureModel( + feature: 'Performance & UX Optimizations', + featurePoints: [ + 'Added pagination to all scrollable views for efficient data handling', + 'Reduced jank with image loading and caching using Cached Network Image', + 'Implemented skeleton loaders and smooth animations for better user experience', + ], + ), + ], + link: 'https://github.com/Eslam-Hossam1/Chefio-Recipe-Sharing-App', + ), + const Project( + name: 'Vibes – Music Player App', + description: + 'A music player app developed using Flutter, allowing users to play audio files stored on their device. The app includes features like creating playlists, marking favorite songs, and playing music in the background. The app provides a smooth user experience with an intuitive and interactive design.', + images: [ + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539193/1745538677557_xpru47.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539197/1745538677539_vjue2z.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539186/1745538677408_omgude.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539191/1745538677443_argsdj.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539198/1745538677518_y2mynz.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539197/1745539060215_gawptx.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539180/1745538677340_tqbnso.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539179/1745538677324_vgjaa8.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539184/1745538677307_b86kbe.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745539186/1745538677375_tgqdjj.jpg', + ], + videos: [ + 'https://res.cloudinary.com/deshi2o56/video/upload/v1745537853/music_player_application_y3eu0e.mp4', + ], + featureModelsList: [ + FeatureModel( + feature: 'Audio Controls', + featurePoints: [ + 'Developed custom controls for play, pause, skip, previous, and seek', + 'Displayed real-time track duration and playback position', + ], + ), + FeatureModel( + feature: 'Background Playback', + featurePoints: [ + 'Integrated background audio playback to ensure uninterrupted listening even when the app is minimized', + 'Used appropriate Flutter packages for audio service and lifecycle handling', + ], + ), + FeatureModel( + feature: 'Playlist Management (CRUD)', + featurePoints: [ + 'Implemented full Create, Read, Update, and Delete functionality for user playlists', + 'Enabled playlist renaming, reordering, and deletion with smooth animations', + ], + ), + FeatureModel( + feature: 'Favorites Musics', + featurePoints: [ + 'Added support for marking and unmarking songs as favorites', + 'Created a dedicated favorites section for easy access to preferred tracks', + ], + ), + FeatureModel( + feature: 'State Persistence', + featurePoints: [ + 'Saved and restored last played track after app restarts', + 'Used local storage for data persistence', + ], + ), + ], + link: 'https://github.com/Eslam-Hossam1/Vibes-Music-Player-App', + ), + const Project( + name: 'Notes App', + description: + 'A simple yet effective notes application developed with Flutter. The app allows users to create, edit, and delete notes, with the added functionality of choosing a color for each note. Notes are stored in a local database using Hive, ensuring fast and efficient data retrieval. The app provides a clean and intuitive interface for managing notes on the go.', + images: [ + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548819/1745547745230_nlzrhd.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548819/1745547745179_xcgg9q.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548815/1745547745113_ktiyhi.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548816/1745547745085_ro6oih.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548816/1745547745026_zlendi.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548820/1745547744849_wgqhwq.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548812/1745547744981_bfb6rl.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548817/1745547744958_bco8yk.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548826/1745547744896_jvbaqa.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745548820/1745547744816_x4zej6.jpg', + ], + videos: [ + "https://res.cloudinary.com/deshi2o56/video/upload/v1745548787/Screenrecorder-2025-04-25-05-10-16-287_hm1ill.mp4", + ], + featureModelsList: [ + FeatureModel( + feature: 'Note Management (CRUD)', + featurePoints: [ + 'Implemented full Create, Read, Update, and Delete functionality for notes', + 'Allowed users to easily edit and remove notes with confirmation prompts', + ], + ), + FeatureModel( + feature: 'Search Functionality', + featurePoints: [ + 'Enabled real-time search across all saved notes', + 'Filtered notes based on keywords with optimized performance', + ], + ), + FeatureModel( + feature: 'State Management', + featurePoints: [ + 'Used Cubit for efficient and lightweight state management', + 'Ensured responsive UI updates upon note modifications', + ], + ), + FeatureModel( + feature: 'Local Data Storage', + featurePoints: [ + 'Stored notes locally using Hive for fast and reliable access', + 'Ensured data persistence even after the app is closed or restarted', + ], + ), + FeatureModel( + feature: 'Note Sorting', + featurePoints: [ + 'Provided sorting options by date or alphabetical order', + 'Improved user experience by keeping recent notes easily accessible', + ], + ), + FeatureModel( + feature: 'UI/UX Design', + featurePoints: [ + 'Built a clean and minimalistic user interface for easy note management', + 'Ensured smooth user interactions and organized layout for better readability', + ], + ), + ], + link: 'https://github.com/Eslam-Hossam1/notes_app', + ), + const Project( + name: 'Bookly – Book Browsing App', + description: + 'A book browsing application developed with Flutter that integrates with the Google Books API. The app allows users to search for books, view detailed previews. It provides a smooth and intuitive user experience with seamless integration of external data, offering real-time book information directly from Google Books.', + images: [ + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745588013/1745587020466_awvijs.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745588021/1745587020450_eguyqq.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745588019/1745587020432_qs5cuo.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745588014/1745587020414_hns203.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745588021/1745587020397_btcibk.jpg', + ], + videos: [ + 'https://res.cloudinary.com/deshi2o56/video/upload/v1745588142/VID_20250425_161627_ba8vpi.mp4' + ], + featureModelsList: [ + FeatureModel( + feature: 'Book Browsing', + featurePoints: [ + 'Fetched and displayed a collection of free Google Books with cover images and titles', + 'Used Dio for efficient HTTP requests and data handling from the Google Books API', + ], + ), + FeatureModel( + feature: 'Book Preview', + featurePoints: [ + 'Integrated free preview functionality allowing users to explore book content before downloading or buying', + 'Handled deep linking and redirection for seamless access to external preview links', + ], + ), + FeatureModel( + feature: 'Search Functionality', + featurePoints: [ + 'Implemented a real-time book search feature with keyword filtering', + 'Displayed relevant results dynamically while maintaining performance', + ], + ), + FeatureModel( + feature: 'Network Image Optimization', + featurePoints: [ + 'Used CachedNetworkImage to efficiently load and cache book covers', + 'Improved image loading performance and reduced data usage', + ], + ), + FeatureModel( + feature: 'State Management', + featurePoints: [ + 'Managed UI states using Cubit from the Flutter Bloc library', + 'Ensured consistent and reactive UI updates based on state changes', + ], + ), + FeatureModel( + feature: 'MVVM Architecture', + featurePoints: [ + 'Applied the MVVM architectural pattern for scalable and maintainable code structure', + 'Separated logic into well-defined layers: models, views, view models, and services', + ], + ), + FeatureModel( + feature: 'Project Structure & Code Quality', + featurePoints: [ + 'Maintained modular and clean code organization with separation of concerns', + 'Followed best practices for naming, file structuring, and reusable components', + ], + ), + FeatureModel( + feature: 'UI/UX Design', + featurePoints: [ + 'Designed a clean and modern interface focused on book discovery and readability', + 'Utilized CustomScrollView for a smooth and native scrolling experience with flexible layouts', + ], + ), + ], + link: 'https://github.com/Eslam-Hossam1/bookly_app', + ), + const Project( + name: 'Calculator App', + description: + 'A simple yet powerful calculator application built with Flutter. It supports basic arithmetic operations including addition, subtraction, multiplication, and division. The app offers both light and dark themes for better user comfort and leverages Cubit state management for clean logic separation.', + images: [ + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745593597/1745593363010_z6ad3y.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745593597/1745593362994_p88pyx.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745593597/1745593362953_szi8ux.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745593599/1745593362975_yvomle.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745593593/1745593362924_vbq1hl.jpg', + 'https://res.cloudinary.com/deshi2o56/image/upload/v1745593594/1745593362901_tg6ihr.jpg', + ], + videos: [ + 'https://res.cloudinary.com/deshi2o56/video/upload/v1745593655/Screenrecorder-2025-04-25-18-00-54-403_tcpth0.mp4', + ], + featureModelsList: [ + FeatureModel( + feature: 'Basic Arithmetic', + featurePoints: [ + 'Implemented addition, subtraction, multiplication, and division operations', + 'Handled edge cases like division by zero and large numbers', + ], + ), + FeatureModel( + feature: 'Dark and Light Mode', + featurePoints: [ + 'Added theme toggle support for light and dark mode', + 'Used ThemeData with Cubit to manage and persist theme state', + ], + ), + FeatureModel( + feature: 'State Management', + featurePoints: [ + 'Used Cubit to manage calculator logic and UI states cleanly', + 'Separated calculation logic from presentation layer', + ], + ), + FeatureModel( + feature: 'Code Quality & Structure', + featurePoints: [ + 'Organized project with clear separation between UI and logic', + 'Used clean architecture principles for maintainability and scalability', + ], + ), + ], + link: 'https://github.com/Eslam-Hossam1/calculator_app', + ), +]; diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart new file mode 100644 index 0000000..449f8f9 --- /dev/null +++ b/lib/routing/app_router.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/routing/routs.dart'; +import 'package:flutter_portfolio/view/main/main_view.dart'; +import 'package:flutter_portfolio/view/projects/components/project_details_view.dart'; +import 'package:flutter_portfolio/view/projects/components/project_media_widgets.dart'; +import 'package:flutter_portfolio/view/splash/splash_view.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_portfolio/model/projects_models_list.dart'; + +class AppRouter { + static final rootNavigatorKey = GlobalKey(); + + static final router = GoRouter( + initialLocation: RoutePaths.splash, + navigatorKey: rootNavigatorKey, + debugLogDiagnostics: true, + routes: [ + GoRoute( + path: RoutePaths.splash, + builder: (context, state) => const SplashView(), + ), + GoRoute( + path: RoutePaths.main, + builder: (context, state) => const MainView(), + ), + GoRoute( + path: RoutePaths.projectDetails, + builder: (context, state) { + final idStr = state.pathParameters['id']!; + final id = int.parse(idStr); + final p = projectList[id]; + return ProjectDetailsView( + projectId: id, + name: p.name, + description: p.description, + images: p.images, + videos: p.videos, + featureModels: p.featureModelsList, + githubLink: p.link, + ); + }, + routes: [ + GoRoute( + path: 'viewer/:imageIndex', + builder: (context, state) { + final idStr = state.pathParameters['id']!; + final id = int.parse(idStr); + final indexStr = state.pathParameters['imageIndex']!; + final imageIndex = int.parse(indexStr); + final images = projectList[id].images; + return ImageGalleryDialog( + images: images, + initialIndex: imageIndex, + ); + }, + ), + ], + ), + ], + ); +} diff --git a/lib/routing/browser_routing_helper.dart b/lib/routing/browser_routing_helper.dart new file mode 100644 index 0000000..5fbabf3 --- /dev/null +++ b/lib/routing/browser_routing_helper.dart @@ -0,0 +1,71 @@ +import 'dart:html' as html; + +import 'package:flutter/foundation.dart' show kIsWeb; + +class BrowserRoutingHelper { + // Push a new state to browser history without navigation + static void pushState(String url, {String? title, dynamic data}) { + if (kIsWeb) { + html.window.history.pushState(data, title ?? '', url); + } + } + + // Replace current history state + static void replaceState(String url, {String? title, dynamic data}) { + if (kIsWeb) { + html.window.history.replaceState(data, title ?? '', url); + } + } + + // Go back in browser history + static void back() { + if (kIsWeb) { + html.window.history.back(); + } + } + + // Go forward in browser history + static void forward() { + if (kIsWeb) { + html.window.history.forward(); + } + } + + // Go to specific position in history (negative = back, positive = forward) + static void go(int delta) { + if (kIsWeb) { + html.window.history.go(delta); + } + } + + // Get current history length + static int get historyLength { + if (kIsWeb) { + return html.window.history.length ?? 0; + } + return 0; + } + + // Listen to browser back/forward button + static void onPopState(void Function(html.PopStateEvent) callback) { + if (kIsWeb) { + html.window.onPopState.listen(callback); + } + } + + // Get current URL + static String get currentUrl { + if (kIsWeb) { + return html.window.location.href; + } + return ''; + } + + // Get current pathname + static String get currentPath { + if (kIsWeb) { + return html.window.location.pathname ?? '/'; + } + return '/'; + } +} diff --git a/lib/routing/routs.dart b/lib/routing/routs.dart new file mode 100644 index 0000000..d9f447f --- /dev/null +++ b/lib/routing/routs.dart @@ -0,0 +1,5 @@ +abstract class RoutePaths { + static const splash = "/"; + static const main = "/main"; + static const projectDetails = "/project-details/:id"; +} diff --git a/lib/view model/controller.dart b/lib/view model/controller.dart index e088711..43061d3 100644 --- a/lib/view model/controller.dart +++ b/lib/view model/controller.dart @@ -1,3 +1,69 @@ import 'package:flutter/material.dart'; -final PageController controller=PageController(); \ No newline at end of file +/// One-page scroll controller (Option A) +final ScrollController scrollController = ScrollController(); + +/// Tracks the currently selected section index for styling active navigation buttons. +final ValueNotifier currentPageIndex = ValueNotifier(0); + +/// Prevents spamming navigation while an animation is in progress. +final ValueNotifier isNavigating = ValueNotifier(false); + +/// Anchor keys for sections in the one-page scroll. +final GlobalKey introSectionKey = GlobalKey(debugLabel: 'intro_section'); +final GlobalKey projectsSectionKey = GlobalKey(debugLabel: 'projects_section'); +final GlobalKey certificationsSectionKey = GlobalKey(debugLabel: 'certifications_section'); + +List get _sectionKeys => [introSectionKey, projectsSectionKey, certificationsSectionKey]; + +/// Smoothly scroll to a section by index. +Future goToSection(int index, {Duration duration = const Duration(milliseconds: 550), Curve curve = Curves.easeInOutCubicEmphasized}) async { + if (index < 0 || index >= _sectionKeys.length) return; + if (isNavigating.value) return; + final ctx = _sectionKeys[index].currentContext; + if (ctx == null) return; + isNavigating.value = true; + try { + await Scrollable.ensureVisible( + ctx, + duration: duration, + curve: curve, + alignment: 0.05, // bias a bit from the very top + ); + currentPageIndex.value = index; + } finally { + isNavigating.value = false; + } +} +/// currentPageIndex in sync while the user scrolls manually. +bool _sectionListenerAttached = false; +void setupSectionScrollListener() { + if (_sectionListenerAttached) return; + _sectionListenerAttached = true; + scrollController.addListener(() { + // Determine the section whose top is closest to (but not far above) the viewport top + try { + final positions = _sectionKeys.map((k) { + final ctx = k.currentContext; + if (ctx == null) return double.infinity; + final box = ctx.findRenderObject() as RenderBox?; + if (box == null || !box.attached) return double.infinity; + final offset = box.localToGlobal(Offset.zero); + return offset.dy.abs(); + }).toList(); + int minIndex = 0; + double minVal = positions[0]; + for (int i = 1; i < positions.length; i++) { + if (positions[i] < minVal) { + minVal = positions[i]; + minIndex = i; + } + } + if (currentPageIndex.value != minIndex) { + currentPageIndex.value = minIndex; + } + } catch (_) { + // Best-effort; ignore measure errors during layout changes + } + }); +} \ No newline at end of file diff --git a/lib/view model/getx_controllers/certification_controller.dart b/lib/view model/getx_controllers/certification_controller.dart index 817f26b..9299bbe 100644 --- a/lib/view model/getx_controllers/certification_controller.dart +++ b/lib/view model/getx_controllers/certification_controller.dart @@ -19,6 +19,4 @@ class CertificationController extends GetxController{ onHover(int index,bool value){ hovers[index]=value; } - - -} \ No newline at end of file +} diff --git a/lib/view/certifications/certifications.dart b/lib/view/certifications/certifications.dart deleted file mode 100644 index dcf3763..0000000 --- a/lib/view/certifications/certifications.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_portfolio/view%20model/getx_controllers/certification_controller.dart'; -import 'package:flutter_portfolio/view/projects/components/title_text.dart'; -import 'package:get/get.dart'; -import '../../res/constants.dart'; -import '../../view model/responsive.dart'; -import 'components/certification_grid.dart'; - -class Certifications extends StatelessWidget { - final controller=Get.put(CertificationController()); - Certifications({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if(Responsive.isLargeMobile(context))const SizedBox( - height: defaultPadding, - ), - const TitleText(prefix: 'Certifications & ', title: 'License'), - const SizedBox( - height: defaultPadding, - ), - Expanded( - child: Responsive( - desktop: CertificateGrid(crossAxisCount: 3,ratio: 1.5,), - extraLargeScreen: CertificateGrid(crossAxisCount: 4,ratio: 1.6), - largeMobile: CertificateGrid(crossAxisCount: 1,ratio: 1.8), - mobile: CertificateGrid(crossAxisCount: 1,ratio: 1.4), - tablet: CertificateGrid(ratio: 1.7,crossAxisCount: 2,))) - ], - ), - ); - } -} - - - - - - - - - - diff --git a/lib/view/certifications/components/certificates_details.dart b/lib/view/certifications/components/certificates_details.dart index 9374f8f..7fd9db6 100644 --- a/lib/view/certifications/components/certificates_details.dart +++ b/lib/view/certifications/components/certificates_details.dart @@ -1,103 +1,166 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:flutter_portfolio/model/certifications_models_list.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../model/certificate_model.dart'; + import '../../../res/constants.dart'; -import '../../../view model/getx_controllers/certification_controller.dart'; -class CertificateStack extends StatelessWidget { - final controller = Get.put(CertificationController()); - CertificateStack({super.key, required this.index}); +class CertificateStack extends StatefulWidget { + const CertificateStack({super.key, required this.index}); final int index; + + @override + State createState() => _CertificateStackState(); +} + +class _CertificateStackState extends State { + bool isHovered = false; @override Widget build(BuildContext context) { - var size=MediaQuery.sizeOf(context); - return InkWell( - onHover: (value) { - controller.onHover(index, value); - }, + return GestureDetector( onTap: () { + launchUrl(Uri.parse(certificateList[widget.index].credential)); }, - borderRadius: BorderRadius.circular(30), - child: AnimatedContainer( - padding: const EdgeInsets.all(defaultPadding), - height: double.infinity, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: bgColor), - duration: const Duration(milliseconds: 500), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - certificateList[index].name, - style: Theme.of(context) - .textTheme - .subtitle2! - .copyWith( - color: Colors.white, - fontWeight: FontWeight.bold), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: defaultPadding,), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: ClipRRect( + borderRadius: BorderRadius.circular(30), + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.symmetric( + vertical: defaultPadding, horizontal: defaultPadding), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + gradient: const LinearGradient(colors: [ + Colors.pinkAccent, + Colors.blue, + ]), + boxShadow: [ + BoxShadow( + color: Colors.pink, + offset: const Offset(-2, 0), + blurRadius: isHovered ? 20 : 10, + ), + BoxShadow( + color: Colors.blue, + offset: const Offset(2, 0), + blurRadius: isHovered ? 20 : 10, + ), + ]), + child: Container( + padding: const EdgeInsets.all(defaultPadding), + height: double.infinity, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), color: bgColor), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(certificateList[index].organization,style: const TextStyle(color: Colors.amber),), - Text(certificateList[index].date,style: const TextStyle(color: Colors.grey,fontSize: 12),), - ], - ), - const SizedBox(height: defaultPadding/2,), - Text.rich( - maxLines: 1, - TextSpan( - text: 'Skills : ',style: const TextStyle(color: Colors.white,), - children: [ - TextSpan( - text: certificateList[index].skills,style: const TextStyle(color: Colors.grey,overflow: TextOverflow.ellipsis),) - ] - ),), - const SizedBox(height: defaultPadding,), - InkWell( - onTap: () { - launchUrl(Uri.parse(certificateList[index].credential)); - }, - child: Container( - height: 40, - width: 150, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - gradient: LinearGradient( - colors: [ - Colors.pink, - Colors.blue.shade900, - ] - ), - boxShadow:const [ - BoxShadow(color: Colors.blue,offset: Offset(0, -1),blurRadius: 5), - BoxShadow(color: Colors.red,offset: Offset(0, 1),blurRadius: 5), - ] + Text( + certificateList[widget.index].name, + style: Theme.of(context).textTheme.titleSmall!.copyWith( + color: Colors.white, fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox( + height: defaultPadding, ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Credentials',style: TextStyle(color: Colors.white,fontSize: 10),), - SizedBox(width: 5,), - Icon( - CupertinoIcons.arrow_turn_up_right,color: Colors.white,size: 10, - ) + Text( + certificateList[widget.index].organization, + style: const TextStyle(color: Colors.amber), + ), + Text( + certificateList[widget.index].date, + style: + const TextStyle(color: Colors.grey, fontSize: 12), + ), ], ), - ), - ), + const SizedBox( + height: defaultPadding / 2, + ), + Text.rich( + maxLines: 1, + TextSpan( + text: 'Skills : ', + style: const TextStyle( + color: Colors.white, + ), + children: [ + TextSpan( + text: certificateList[widget.index].skills, + style: const TextStyle( + color: Colors.grey, + overflow: TextOverflow.ellipsis), + ) + ]), + ), + const SizedBox( + height: defaultPadding, + ), + Spacer(), + CredentialsButton(widget: widget), + ], + )), + ), + ), + ), + ); + } +} + +class CredentialsButton extends StatelessWidget { + const CredentialsButton({ + super.key, + required this.widget, + }); - ], + final CertificateStack widget; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + launchUrl(Uri.parse(certificateList[widget.index].credential)); + }, + child: Container( + height: 40, + width: 150, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + gradient: LinearGradient(colors: [ + Colors.pink, + Colors.blue.shade900, + ]), + boxShadow: const [ + BoxShadow( + color: Colors.blue, offset: Offset(0, -1), blurRadius: 5), + BoxShadow(color: Colors.red, offset: Offset(0, 1), blurRadius: 5), + ]), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Credentials', + style: TextStyle(color: Colors.white, fontSize: 10), + ), + SizedBox( + width: 5, ), - )), + Icon( + CupertinoIcons.arrow_turn_up_right, + color: Colors.white, + size: 10, + ) + ], + ), + ), ); } -} \ No newline at end of file +} diff --git a/lib/view/certifications/components/certification_grid.dart b/lib/view/certifications/components/certification_grid.dart index a0b0c69..21c0d8e 100644 --- a/lib/view/certifications/components/certification_grid.dart +++ b/lib/view/certifications/components/certification_grid.dart @@ -1,46 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../../../model/certificate_model.dart'; -import '../../../res/constants.dart'; -import '../../../view model/getx_controllers/certification_controller.dart'; +import 'package:flutter_portfolio/model/certifications_models_list.dart'; + import 'certificates_details.dart'; + class CertificateGrid extends StatelessWidget { final int crossAxisCount; final double ratio; - CertificateGrid({super.key, this.crossAxisCount = 3, this.ratio=1.3}); - final controller = Get.put(CertificationController()); + const CertificateGrid({super.key, this.crossAxisCount = 3, this.ratio = 1.3}); @override Widget build(BuildContext context) { - return GridView.builder( + return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 30), - itemCount: certificateList.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, childAspectRatio: ratio), - itemBuilder: (context, index) { - return Obx(() => AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.symmetric( - vertical: defaultPadding, horizontal: defaultPadding), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - gradient: const LinearGradient(colors: [ - Colors.pinkAccent, - Colors.blue, - ]), - boxShadow: [ - BoxShadow( - color: Colors.pink, - offset: const Offset(-2, 0), - blurRadius: controller.hovers[index] ? 20 : 10, - ), - BoxShadow( - color: Colors.blue, - offset: const Offset(2, 0), - blurRadius: controller.hovers[index] ? 20 : 10,), - ]), - child: CertificateStack(index: index) - )); - }, + sliver: SliverGrid.builder( + itemCount: certificateList.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, childAspectRatio: ratio), + itemBuilder: (context, index) { + return CertificateStack(index: index); + }, + ), ); } -} \ No newline at end of file +} diff --git a/lib/view/home/home.dart b/lib/view/home/home.dart deleted file mode 100644 index 53a60cb..0000000 --- a/lib/view/home/home.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_portfolio/view/certifications/certifications.dart'; -import 'package:flutter_portfolio/view/intro/introduction.dart'; -import 'package:flutter_portfolio/view/main/main_view.dart'; -import 'package:flutter_portfolio/view/projects/project_view.dart'; - -class HomePage extends StatelessWidget { - const HomePage({super.key}); - - @override - Widget build(BuildContext context) { - return MainView(pages: [ - const Introduction(), - ProjectsView(), - Certifications(), - ]); - } -} diff --git a/lib/view/intro/components/animated_image_container.dart b/lib/view/intro/components/animated_image_container.dart new file mode 100644 index 0000000..903d7e7 --- /dev/null +++ b/lib/view/intro/components/animated_image_container.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import '../../../res/constants.dart'; +import '../../../view model/responsive.dart'; + +class AnimatedImageContainer extends StatefulWidget { + const AnimatedImageContainer({Key? key, this.height = 300, this.width = 250}) + : super(key: key); + final double? width; + final double? height; + @override + AnimatedImageContainerState createState() => AnimatedImageContainerState(); +} + +class AnimatedImageContainerState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1000), + )..repeat(reverse: true); // Repeat the animation loop + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + final value = _controller.value; + return Transform.translate( + offset: Offset(0, 2 * value), // Move the container up and down + child: Container( + height: widget.height!, + width: widget.width!, + padding: const EdgeInsets.all(defaultPadding / 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + gradient: const LinearGradient(colors: [ + Colors.pinkAccent, + Colors.blue, + ]), + boxShadow: const [ + BoxShadow( + color: Colors.pink, + offset: Offset(-2, 0), + blurRadius: 20, + ), + BoxShadow( + color: Colors.blue, + offset: Offset(2, 0), + blurRadius: 20, + ), + ], + ), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(30 - (defaultPadding / 4)), + ), + child: Image.asset( + 'assets/images/image.png', + height: Responsive.isLargeMobile(context) + ? MediaQuery.sizeOf(context).width * 0.2 + : Responsive.isTablet(context) + ? MediaQuery.sizeOf(context).width * 0.14 + : 200, + width: Responsive.isLargeMobile(context) + ? MediaQuery.sizeOf(context).width * 0.2 + : Responsive.isTablet(context) + ? MediaQuery.sizeOf(context).width * 0.14 + : 200, + fit: BoxFit.cover, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/view/intro/components/animated_texts_componenets.dart b/lib/view/intro/components/animated_texts_componenets.dart deleted file mode 100644 index 3f0e9e1..0000000 --- a/lib/view/intro/components/animated_texts_componenets.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../res/constants.dart'; -import '../../../view model/responsive.dart'; - -class AnimatedImageContainer extends StatefulWidget { - const AnimatedImageContainer({Key? key, this.height = 300, this.width = 250}) - : super(key: key); - - final double? width; - final double? height; - - @override - _AnimatedImageContainerState createState() => _AnimatedImageContainerState(); -} - -class _AnimatedImageContainerState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1000), - )..repeat(reverse: true); // Repeat the animation loop - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - final value = _controller.value; - return Transform.translate( - offset: Offset(0, 2* value), // Move the container up and down - child: Container( - height: widget.height!, - width: widget.width!, - padding: const EdgeInsets.all(defaultPadding / 4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - gradient: const LinearGradient(colors: [ - Colors.pinkAccent, - Colors.blue, - ]), - boxShadow: const [ - BoxShadow( - color: Colors.pink, - offset: Offset(-2, 0), - blurRadius: 20, - ), - BoxShadow( - color: Colors.blue, - offset: Offset(2, 0), - blurRadius: 20, - ), - ], - ), - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(30), - ), - child: Image.asset( - 'assets/images/image.png', - height: Responsive.isLargeMobile(context) - ? MediaQuery.sizeOf(context).width * 0.2 - : Responsive.isTablet(context) - ? MediaQuery.sizeOf(context).width * 0.14 - : 200, - width: Responsive.isLargeMobile(context) - ? MediaQuery.sizeOf(context).width * 0.2 - : Responsive.isTablet(context) - ? MediaQuery.sizeOf(context).width * 0.14 - : 200, - fit: BoxFit.cover, - ), - ), - ), - ); - }, - ); - } -} - - -class MyPortfolioText extends StatelessWidget { - const MyPortfolioText({super.key, required this.start, required this.end}); - - final double start; - final double end; - - @override - Widget build(BuildContext context) { - return Builder(builder: (context) { - return TweenAnimationBuilder( - tween: Tween(begin: start, end: end), - duration: const Duration(milliseconds: 200), - builder: (context, value, child) { - return Text('My Personal Portfolio', - style: Theme.of(context).textTheme.headlineLarge!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - height: 0, - fontSize: value)); - }, - ); - }); - } -} - -class AnimatedSubtitleText extends StatelessWidget { - final double start; - final double end; - final String text; - final bool gradient; - const AnimatedSubtitleText( - {super.key, required this.start, required this.end, required this.text, this.gradient=false,}); - - @override - Widget build(BuildContext context) { - return TweenAnimationBuilder( - tween: Tween(begin: start, end: end), - duration: const Duration(milliseconds: 200), - builder: (context, value, child) { - return Text( - text, - style: Theme.of(context).textTheme.headlineLarge!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w900, - shadows: gradient? [ - Shadow(color: Colors.pink,offset: Offset(0, 2),blurRadius: 10), - Shadow(color: Colors.pink,offset: Offset(0, -2),blurRadius: 10), - ] :[] , - height: 0, - fontSize: value), - ); - }, - ); - } -} - -class AnimatedDescriptionText extends StatelessWidget { - const AnimatedDescriptionText( - {super.key, required this.start, required this.end}); - - final double start; - final double end; - - @override - Widget build(BuildContext context) { - return TweenAnimationBuilder( - tween: Tween(begin: start, end: end), - duration: const Duration(milliseconds: 200), - builder: (context, value, child) { - return Text( - 'I\'m capable of creating excellent mobile apps, handling${Responsive.isLargeMobile(context) ? '\n' : ''}every step from ${!Responsive.isLargeMobile(context) ? '\n' : ''}concept to deployment.', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: Colors.grey, wordSpacing: 2, fontSize: value), - ); - }, - ); - } -} diff --git a/lib/view/intro/components/combine_subtitle.dart b/lib/view/intro/components/combine_subtitle.dart new file mode 100644 index 0000000..c1f22fe --- /dev/null +++ b/lib/view/intro/components/combine_subtitle.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/view/intro/components/subtitle_text.dart'; + +import '../../../view model/responsive.dart'; + +class CombineSubtitleText extends StatelessWidget { + const CombineSubtitleText({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Responsive( + desktop: AnimatedSubtitleText(start: 30, end: 40, text: 'Flutter '), + largeMobile: + AnimatedSubtitleText(start: 30, end: 25, text: 'Flutter '), + mobile: AnimatedSubtitleText(start: 25, end: 20, text: 'Flutter '), + tablet: AnimatedSubtitleText(start: 40, end: 30, text: 'Flutter '), + ), + ShaderMask( + shaderCallback: (bounds) { + return const LinearGradient(colors: [ + Colors.pink, + Colors.blue, + ]).createShader(bounds); + }, + child: const Responsive( + desktop: AnimatedSubtitleText( + start: 30, end: 40, text: 'Developer ', gradient: false), + largeMobile: AnimatedSubtitleText( + start: 30, end: 25, text: 'Developer ', gradient: false), + mobile: AnimatedSubtitleText( + start: 25, end: 20, text: 'Developer ', gradient: false), + tablet: AnimatedSubtitleText( + start: 40, end: 30, text: 'Developer ', gradient: false), + ), + ) + ], + ); + } +} diff --git a/lib/view/intro/components/description_text.dart b/lib/view/intro/components/description_text.dart new file mode 100644 index 0000000..96077f9 --- /dev/null +++ b/lib/view/intro/components/description_text.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import '../../../view model/responsive.dart'; + +class AnimatedDescriptionText extends StatelessWidget { + const AnimatedDescriptionText( + {super.key, required this.start, required this.end}); + final double start; + final double end; + @override + Widget build(BuildContext context) { + final isVerySmall = MediaQuery.of(context).size.width < 358; + return TweenAnimationBuilder( + tween: Tween(begin: start, end: end), + duration: const Duration(milliseconds: 200), + builder: (context, value, child) { + return Text( + 'I\'m capable of creating excellent mobile apps, handling${Responsive.isLargeMobile(context) ? '\n' : ' '}every step from ${!Responsive.isLargeMobile(context) ? '\n' : ''}idea to deployment.', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.grey, + wordSpacing: 2, + fontSize: isVerySmall ? 10.5 : value, + ), + ); + }, + ); + } +} diff --git a/lib/view/intro/components/download_button.dart b/lib/view/intro/components/download_button.dart index c0616f6..6b17e6c 100644 --- a/lib/view/intro/components/download_button.dart +++ b/lib/view/intro/components/download_button.dart @@ -4,46 +4,76 @@ import 'package:url_launcher/url_launcher.dart'; import '../../../res/constants.dart'; -class DownloadButton extends StatelessWidget { +class DownloadButton extends StatefulWidget { const DownloadButton({super.key}); + + @override + State createState() => _DownloadButtonState(); +} + +class _DownloadButtonState extends State { + bool _isHovered = false; @override Widget build(BuildContext context) { - return InkWell( + return GestureDetector( onTap: () { - launchUrl(Uri.parse('https://drive.google.com/file/d/1HSIe7rdk8VtrAL4DQuybfMHQgDrQ6xNs/view?usp=sharing')); + launchUrl(Uri.parse( + 'https://drive.google.com/file/d/1uKkGWhZxPWlzuk3jp4wxqre-NBMqZ5Pl/view')); }, - child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: defaultPadding/1.5,horizontal: defaultPadding*2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow:const [ - BoxShadow(color: Colors.blue,offset: Offset(0, -1),blurRadius: 5), - BoxShadow(color: Colors.red,offset: Offset(0, 1),blurRadius: 5), + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + alignment: Alignment.center, + padding: const EdgeInsets.symmetric( + vertical: defaultPadding / 1.5, horizontal: defaultPadding * 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.blue, + offset: const Offset(0, -1), + blurRadius: _isHovered + ? defaultPadding / 1.75 + : defaultPadding / 3.5), + BoxShadow( + color: Colors.red, + offset: const Offset(0, 1), + blurRadius: _isHovered + ? defaultPadding / 1.75 + : defaultPadding / 3.5), ], - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Colors.pink, - Colors.blue.shade900, - ]), - ), - child: Row( - children: [ - Text( - 'Download CV', - style: Theme.of(context).textTheme.labelSmall!.copyWith( - color: Colors.white, - letterSpacing: 1.2, - fontWeight: FontWeight.bold), - ), - const SizedBox(width: defaultPadding/3,), - const Icon(FontAwesomeIcons.download,color: Colors.white70,size: 15,) - - ], + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.pink, + Colors.blue.shade900, + ]), + ), + child: Row( + children: [ + Text( + 'Download CV', + style: Theme.of(context).textTheme.labelSmall!.copyWith( + color: Colors.white, + letterSpacing: 1.2, + fontWeight: FontWeight.bold), + ), + const SizedBox( + width: defaultPadding / 3, + ), + const Icon( + FontAwesomeIcons.download, + color: Colors.white70, + size: 15, + ) + ], + ), ), ), ); } -} \ No newline at end of file +} diff --git a/lib/view/intro/components/headline_text.dart b/lib/view/intro/components/headline_text.dart new file mode 100644 index 0000000..2bb9842 --- /dev/null +++ b/lib/view/intro/components/headline_text.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class MyPortfolioText extends StatelessWidget { + const MyPortfolioText({super.key, required this.start, required this.end}); + final double start; + final double end; + @override + Widget build(BuildContext context) { + return Builder(builder: (context) { + return TweenAnimationBuilder( + tween: Tween(begin: start, end: end), + duration: const Duration(milliseconds: 200), + builder: (context, value, child) { + return Text('Eslam Hossam', + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + height: 0, + fontSize: value)); + }, + ); + }); + } +} diff --git a/lib/view/intro/components/intro_body.dart b/lib/view/intro/components/intro_body.dart index a1de9d3..00269c9 100644 --- a/lib/view/intro/components/intro_body.dart +++ b/lib/view/intro/components/intro_body.dart @@ -1,135 +1,84 @@ -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/view/intro/components/my_animated_image.dart'; +import 'package:flutter_portfolio/view/intro/components/social_media_list.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + import '../../../res/constants.dart'; import '../../../view model/responsive.dart'; -import 'animated_texts_componenets.dart'; +import 'animated_image_container.dart'; +import 'combine_subtitle.dart'; +import 'description_text.dart'; import 'download_button.dart'; +import 'headline_text.dart'; + class IntroBody extends StatelessWidget { const IntroBody({super.key}); @override Widget build(BuildContext context) { var size = MediaQuery.sizeOf(context); + // double getHeight(){ + // double width = MediaQuery.sizeOf(context).width; + // double height = MediaQuery.sizeOf(context).height; + // if(width>1000){ + // return height-100; + // }else if(width>5) + // } return Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!Responsive.isDesktop(context)) - SizedBox( - height: size.height * 0.06, - ), - if (!Responsive.isDesktop(context)) - Row( - children: [ - SizedBox( - width: size.width * 0.23, - ), - const AnimatedImageContainer( - width: 150, - height: 200, - ), - ], - ), - if (!Responsive.isDesktop(context)) - SizedBox( - height: size.height * 0.1, - ), - const Responsive( - desktop: MyPortfolioText(start: 40, end: 50), - largeMobile: MyPortfolioText(start: 40, end: 35), - mobile: MyPortfolioText(start: 35, end: 30), - tablet: MyPortfolioText(start: 50, end: 40)), - if (kIsWeb && Responsive.isLargeMobile(context)) - Container( - height: defaultPadding, - color: Colors.transparent, - ), + SizedBox(width: MediaQuery.sizeOf(context).width * 0.05), + if (!Responsive.isLargeMobile(context)) const SocialMediaIconList(), + SizedBox(width: MediaQuery.sizeOf(context).width * 0.05), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 40.h), + if (Responsive.isDesktop(context)) SizedBox(height: 160.h), + if (!Responsive.isDesktop(context)) + SizedBox( + height: size.height * 0.06, + ), + if (!Responsive.isDesktop(context)) Row( - mainAxisAlignment: MainAxisAlignment.start, children: [ - const Responsive( - desktop: AnimatedSubtitleText( - start: 30, end: 40, text: 'Flutter '), - largeMobile: AnimatedSubtitleText( - start: 30, end: 25, text: 'Flutter '), - mobile: AnimatedSubtitleText( - start: 25, end: 20, text: 'Flutter '), - tablet: AnimatedSubtitleText( - start: 40, end: 30, text: 'Flutter '), + SizedBox( + width: size.width * 0.23, + ), + const MyAnimatedImage( + width: 150, + height: 200, ), - (kIsWeb && Responsive.isLargeMobile(context) - ? const Responsive( - desktop: AnimatedSubtitleText( - start: 30, - end: 40, - text: 'Developer ', - gradient: true), - largeMobile: AnimatedSubtitleText( - start: 30, - end: 25, - text: 'Developer ', - gradient: true), - mobile: AnimatedSubtitleText( - start: 25, - end: 20, - text: 'Developer ', - gradient: true), - tablet: AnimatedSubtitleText( - start: 40, - end: 30, - text: 'Developer ', - gradient: true), - ) - : ShaderMask( - shaderCallback: (bounds) { - return const LinearGradient(colors: [ - Colors.pink, - Colors.blue, - ]).createShader(bounds); - }, - child: const Responsive( - desktop: AnimatedSubtitleText( - start: 30, - end: 40, - text: 'Developer ', - gradient: false), - largeMobile: AnimatedSubtitleText( - start: 30, - end: 25, - text: 'Developer ', - gradient: false), - mobile: AnimatedSubtitleText( - start: 25, - end: 20, - text: 'Developer ', - gradient: true), - tablet: AnimatedSubtitleText( - start: 40, - end: 30, - text: 'Developer ', - gradient: false), - ), - )) ], ), - const SizedBox(height: defaultPadding / 2), - const Responsive( - desktop: AnimatedDescriptionText(start: 14, end: 15), - largeMobile: AnimatedDescriptionText(start: 14, end: 12), - mobile: AnimatedDescriptionText(start: 14, end: 12), - tablet: AnimatedDescriptionText(start: 17, end: 14), - ), - const SizedBox( - height: defaultPadding * 2, + if (!Responsive.isDesktop(context)) + SizedBox( + height: size.height * 0.1, ), - const DownloadButton(), - ], - ), + const Responsive( + desktop: MyPortfolioText(start: 40, end: 50), + largeMobile: MyPortfolioText(start: 40, end: 35), + mobile: MyPortfolioText(start: 35, end: 30), + tablet: MyPortfolioText(start: 50, end: 40)), + const CombineSubtitleText(), + const SizedBox(height: defaultPadding / 2), + const Responsive( + desktop: AnimatedDescriptionText(start: 14, end: 15), + largeMobile: AnimatedDescriptionText(start: 14, end: 12), + mobile: AnimatedDescriptionText(start: 14, end: 12), + tablet: AnimatedDescriptionText(start: 17, end: 14), + ), + const SizedBox( + height: defaultPadding * 2, + ), + const DownloadButton(), + const SizedBox( + height: defaultPadding * 2, + ), + ], ), const Spacer(), - if (Responsive.isDesktop(context)) const AnimatedImageContainer(), + if (Responsive.isDesktop(context)) const MyAnimatedImage(), const Spacer() ], ); diff --git a/lib/view/intro/components/my_animated_image.dart b/lib/view/intro/components/my_animated_image.dart new file mode 100644 index 0000000..e1236a4 --- /dev/null +++ b/lib/view/intro/components/my_animated_image.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/widgets/image_with_placeholder.dart'; +import '../../../res/constants.dart'; + +class MyAnimatedImage extends StatefulWidget { + const MyAnimatedImage({ + Key? key, + this.height = 300, + this.width = 250, + }) : super(key: key); + + final double? width; + final double? height; + + @override + MyAnimatedImageState createState() => MyAnimatedImageState(); +} + +class MyAnimatedImageState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1000), + )..repeat(reverse: true); + + _animation = Tween( + begin: const Offset(0, 0), + end: const Offset(0, 0.02), // small vertical float + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SlideTransition( + position: _animation, + child: Container( + height: widget.height!, + width: widget.width!, + padding: const EdgeInsets.all(defaultPadding / 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + gradient: const LinearGradient( + colors: [ + Colors.pinkAccent, + Colors.blue, + ], + ), + boxShadow: const [ + BoxShadow( + color: Colors.pink, + offset: Offset(-2, 0), + blurRadius: 20, + ), + BoxShadow( + color: Colors.blue, + offset: Offset(2, 0), + blurRadius: 20, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(25), + child: ImageWithBackgroundPlaceholder( + imagePath: 'assets/images/profile.png', + fit: BoxFit.cover, + ), + ), + ), + ); + } +} diff --git a/lib/view/intro/components/side_menu_button.dart b/lib/view/intro/components/side_menu_button.dart deleted file mode 100644 index 8f825ac..0000000 --- a/lib/view/intro/components/side_menu_button.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../../res/constants.dart'; - -class MenuButton extends StatelessWidget { - final VoidCallback? onTap; - const MenuButton({super.key, this.onTap}); - @override - Widget build(BuildContext context) { - return Column( - children: [ - const Spacer(), - TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: const Duration(milliseconds: 200), - builder: (context, value, child) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(20), - child: Container( - height: defaultPadding * 2.0 * value, - width: defaultPadding * 2.0 * value, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.black, - boxShadow: [ - BoxShadow( - color: Colors.pinkAccent.withOpacity(.5), - offset: const Offset(1, 1)), - BoxShadow( - color: Colors.blue.withOpacity(.5), - offset: const Offset(-1, -1)), - ]), - child: Center( - child: ShaderMask( - shaderCallback: (bounds) { - return LinearGradient( - colors: [Colors.pink, Colors.blue.shade900]) - .createShader(bounds); - }, - child: Icon( - Icons.menu, - color: Colors.white, - size: defaultPadding * 1.2 * value, - ), - ) - ), - ), - ); - }, - ), - const Spacer( - flex: 5, - ) - ], - ); - } -} diff --git a/lib/view/intro/components/social_icon.dart b/lib/view/intro/components/social_icon.dart new file mode 100644 index 0000000..48f85a6 --- /dev/null +++ b/lib/view/intro/components/social_icon.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../res/constants.dart'; + +class SocialMediaIcon extends StatelessWidget { + const SocialMediaIcon({super.key, required this.icon, this.onTap}); + final String icon; + final VoidCallback? onTap; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: defaultPadding * 0.4), + child: SvgPicture.asset( + icon, + color: Colors.white, + height: 15, + width: 15, + ), + ), + ); + } +} diff --git a/lib/view/intro/components/social_media_coloumn.dart b/lib/view/intro/components/social_media_coloumn.dart new file mode 100644 index 0000000..a5557fe --- /dev/null +++ b/lib/view/intro/components/social_media_coloumn.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/view/intro/components/social_icon.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SocialMediaIconColumn extends StatelessWidget { + const SocialMediaIconColumn({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SocialMediaIcon( + icon: 'assets/icons/linkedin.svg', + onTap: () => launchUrl(Uri.parse( + 'https://www.linkedin.com/in/eslam-hossam-591708316/'))), + SocialMediaIcon( + icon: 'assets/icons/github.svg', + onTap: () => + launchUrl(Uri.parse('https://github.com/Eslam-Hossam1/')), + ), + SocialMediaIcon( + icon: 'assets/icons/whatsapp-svgrepo-com.svg', + onTap: () { + launchUrl(Uri.parse('https://wa.me/201028735105')); + }, + ), + ], + ); + } +} diff --git a/lib/view/intro/components/social_media_list.dart b/lib/view/intro/components/social_media_list.dart index e988486..5d8ea5d 100644 --- a/lib/view/intro/components/social_media_list.dart +++ b/lib/view/intro/components/social_media_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:url_launcher/url_launcher.dart'; - +import 'package:flutter_portfolio/view%20model/responsive.dart'; +import 'package:flutter_portfolio/view/intro/components/social_media_coloumn.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../res/constants.dart'; class SocialMediaIconList extends StatelessWidget { @@ -9,53 +9,41 @@ class SocialMediaIconList extends StatelessWidget { @override Widget build(BuildContext context) { - final size=MediaQuery.sizeOf(context); - return TweenAnimationBuilder(tween: Tween(begin: 0.0,end: 1.0), duration: const Duration(milliseconds: 200), builder: (context, value, child) { - return Transform.scale(scale: value, - child: Column( - children: [ - const Spacer(), - RotatedBox( - quarterTurns: -3, - child: Text('Follow Me',style: Theme.of(context).textTheme.titleSmall!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w500, - ),), - ), - Container( - height: size.height*0.06, - width: 3, - margin: const EdgeInsets.symmetric(vertical: defaultPadding * 0.5), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(defaultPadding) - ), + final size = MediaQuery.sizeOf(context); + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 200), + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Column( + children: [ + SizedBox(height: 40.h), + if (Responsive.isDesktop(context)) SizedBox(height: 160.h), + RotatedBox( + quarterTurns: -3, + child: Text( + 'Follow Me', + style: Theme.of(context).textTheme.titleSmall!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + Container( + height: size.height * 0.06, + width: 3, + margin: + const EdgeInsets.symmetric(vertical: defaultPadding * 0.5), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(defaultPadding)), + ), + const SocialMediaIconColumn(), + ], ), - SocialMediaIcon(icon: 'assets/icons/linkedin.svg',onTap: ()=>launchUrl(Uri.parse('https://www.linkedin.com/in/hamad-anwar/'))), - SocialMediaIcon(icon: 'assets/icons/github.svg',onTap: () => launchUrl(Uri.parse('https://github.com/Hamad-Anwar')),), - const SocialMediaIcon(icon: 'assets/icons/dribble.svg',), - const SocialMediaIcon(icon: 'assets/icons/twitter.svg'), - const SocialMediaIcon(icon: 'assets/icons/linkedin.svg'), - const Spacer(), - ], - ), - ); - },); - } -} -class SocialMediaIcon extends StatelessWidget { - const SocialMediaIcon({super.key, required this.icon, this.onTap}); - final String icon; - final VoidCallback? onTap; - @override - Widget build(BuildContext context) { - return InkWell( - onTap:onTap, - borderRadius: BorderRadius.circular(20), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: defaultPadding * 0.4), - child: SvgPicture.asset(icon,color: Colors.white,height: 15,width: 15,), - ), + ); + }, ); } -} \ No newline at end of file +} diff --git a/lib/view/intro/components/subtitle_text.dart b/lib/view/intro/components/subtitle_text.dart new file mode 100644 index 0000000..595db2e --- /dev/null +++ b/lib/view/intro/components/subtitle_text.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class AnimatedSubtitleText extends StatelessWidget { + final double start; + final double end; + final String text; + final bool gradient; + const AnimatedSubtitleText({ + super.key, + required this.start, + required this.end, + required this.text, + this.gradient = false, + }); + @override + Widget build(BuildContext context) { + return TweenAnimationBuilder( + tween: Tween(begin: start, end: end), + duration: const Duration(milliseconds: 200), + builder: (context, value, child) { + return Text( + text, + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + shadows: gradient + ? [ + const Shadow( + color: Colors.pink, + offset: Offset(0, 2), + blurRadius: 10), + const Shadow( + color: Colors.pink, + offset: Offset(0, -2), + blurRadius: 10), + ] + : [], + height: 0, + fontSize: value), + ); + }, + ); + } +} diff --git a/lib/view/intro/introduction.dart b/lib/view/intro/introduction.dart deleted file mode 100644 index 964d945..0000000 --- a/lib/view/intro/introduction.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_portfolio/view%20model/responsive.dart'; -import 'package:flutter_portfolio/view/intro/components/intro_body.dart'; -import 'package:flutter_portfolio/view/intro/components/side_menu_button.dart'; -import 'package:flutter_portfolio/view/intro/components/social_media_list.dart'; -class Introduction extends StatelessWidget { - const Introduction({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - body: Row( - children: [ - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.01, - ), - if (!Responsive.isLargeMobile(context)) MenuButton(onTap: () => Scaffold.of(context).openDrawer(),), - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.02, - ), - if (!Responsive.isLargeMobile(context)) const SocialMediaIconList(), - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.07, - ), - const Expanded( - child: IntroBody(), - ), - ], - ), - ); - } -} - - - diff --git a/lib/view/main/components/connect_button.dart b/lib/view/main/components/connect_button.dart deleted file mode 100644 index cab2c4a..0000000 --- a/lib/view/main/components/connect_button.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; -import '../../../res/constants.dart'; - -class ConnectButton extends StatelessWidget { - const ConnectButton({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: defaultPadding), - child: InkWell( - onTap: () { - launchUrl(Uri.parse('https://wa.me/03054200605')); - }, - borderRadius: BorderRadius.circular(defaultPadding +10), - child: Container( - height: 60, - width: 150, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(defaultPadding), - gradient: LinearGradient(colors: [ - Colors.pink, - Colors.blue.shade900, - ]), - boxShadow:const [ - BoxShadow(color: Colors.blue,offset: Offset(0, -1),blurRadius: defaultPadding/4), - BoxShadow(color: Colors.red,offset: Offset(0, 1),blurRadius: defaultPadding/4), - ] - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(FontAwesomeIcons.whatsapp,color: Colors.greenAccent,size: 15,), - const SizedBox(width : defaultPadding/4), - Text('Whatsapp',style: Theme.of(context).textTheme.labelSmall!.copyWith( - color: Colors.white, - letterSpacing: 1.2, - fontWeight: FontWeight.bold - ),), - ], - ) - ), - ), - ); - } -} diff --git a/lib/view/main/components/drawer/about.dart b/lib/view/main/components/drawer/about.dart index 2fbe2c2..36a1b90 100644 --- a/lib/view/main/components/drawer/about.dart +++ b/lib/view/main/components/drawer/about.dart @@ -1,49 +1,46 @@ import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/font_clamper_extension.dart'; import 'package:flutter_portfolio/res/constants.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'drawer_image.dart'; + class About extends StatelessWidget { const About({super.key}); @override Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 1.23, + return Center( child: Container( - color: bgColor, + color: bgColor, child: Column( children: [ - const Spacer(flex: 2,), - Container( - height: 100, - width: 100, - padding: const EdgeInsets.all(defaultPadding/6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50), - gradient: LinearGradient( - colors: [ - Colors.pink, - Colors.blue.shade900, - ], - ), - boxShadow: const [ - BoxShadow(color: Colors.pink,blurRadius: 10,offset: Offset(0, 2)), - BoxShadow(color: Colors.blue,blurRadius: 10,offset: Offset(0, -2)), - ] - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(50), - child: Transform.rotate( - angle: 0.1, - child: Image.asset('assets/images/profile.png',fit: BoxFit.cover,))), - ), - const Spacer(), - Text('Hamad Anwar',style: Theme.of(context).textTheme.titleSmall,), - const SizedBox(height: defaultPadding/4,), - const Text('Flutter Developer & The Student of\nSoftware Engineering', + SizedBox( + height: (60.h).clamp(40, 80), + ), + const DrawerImage(), + SizedBox( + height: (40.h).clamp(30, 45), + ), + Text( + 'Eslam Hossam', + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith(fontSize: 24.sp.fontClamper(28)), + ), + SizedBox( + height: 8.h.clamp(4, 16), + ), + Text( + 'Flutter Developer', textAlign: TextAlign.center, style: TextStyle( + fontSize: 18.sp.fontClamper(20), fontWeight: FontWeight.w200, - height: 1.5 - ),), - const Spacer(flex: 2,), + height: 1.5), + ), + SizedBox( + height: (60.h).clamp(40, 80), + ), ], ), ), diff --git a/lib/view/main/components/drawer/drawer.dart b/lib/view/main/components/drawer/drawer.dart index 7711cf4..e08b70c 100644 --- a/lib/view/main/components/drawer/drawer.dart +++ b/lib/view/main/components/drawer/drawer.dart @@ -1,68 +1,53 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_portfolio/view/main/components/drawer/drawer_bottom_social_buttons.dart'; +import 'package:flutter_portfolio/view/main/components/drawer/personal_info.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../../res/constants.dart'; -import 'header_info.dart'; import 'knowledges.dart'; import 'about.dart'; import 'my_skill.dart'; + class CustomDrawer extends StatelessWidget { const CustomDrawer({super.key}); @override Widget build(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + bool isDesktop = width >= 1000; + bool isTablet = width >= 600 && width < 1000; + bool isBigMobile = width >= 420 && width < 600; + bool isMobile = width >= 350 && width < 420; return Drawer( - backgroundColor: primaryColor, - child: Column( - children: [ - const About(), - Expanded( - child: Container( - color: bgColor, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(defaultPadding/2), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: defaultPadding/2,), - const AreaInfoText(title: 'Contact', text: '03166657602'), - const AreaInfoText(title: 'Email', text: 'rh676838@gmail.com'), - const AreaInfoText(title: 'LinkedIn', text: '@hamad-anwar'), - const AreaInfoText(title: 'Github', text: '@hamad-anwar'), - const SizedBox( - height: defaultPadding, - ), - - const Text('Skills',style: TextStyle(color: Colors.white),), - const SizedBox( - height: defaultPadding, - ), - const MySKills(), - const Knowledges(), - const Divider(), - const SizedBox(height: defaultPadding,), - Container( - margin: const EdgeInsets.only(top: defaultPadding), - child: Row( - children: [ - const Spacer(), - IconButton(onPressed: () {launchUrl(Uri.parse('https://linkedin.com/in/hamad-anwar'));}, icon: SvgPicture.asset('assets/icons/linkedin.svg')), - IconButton(onPressed: () {launchUrl(Uri.parse('https://github.com/hamad-anwar'));}, icon: SvgPicture.asset('assets/icons/github.svg')), - const Spacer(), - ], - ), - ) - ], - ), + width: isDesktop + ? width * .4 + : isTablet + ? width * .6 + : isMobile + ? width * .9 + : isBigMobile + ? width * .8 + : null, + backgroundColor: bgColor, + child: SingleChildScrollView( + child: Container( + color: bgColor, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: defaultPadding / 2, horizontal: 10.w.clamp(10, 20)), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + About(), + PersonalInfo(), + Divider(), + MySKills(), + Knowledges(), + Divider(), + DrawerBottomSocialButton(), + ], ), ), - )) - ], + ), ), ); } } - - - - diff --git a/lib/view/main/components/drawer/drawer_bottom_social_buttons.dart b/lib/view/main/components/drawer/drawer_bottom_social_buttons.dart new file mode 100644 index 0000000..e2cfefb --- /dev/null +++ b/lib/view/main/components/drawer/drawer_bottom_social_buttons.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../../res/constants.dart'; + +class DrawerBottomSocialButton extends StatelessWidget { + const DrawerBottomSocialButton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: + const EdgeInsets.only(top: defaultPadding, bottom: defaultPadding), + child: Row( + children: [ + const Spacer( + flex: 2, + ), + IconButton( + onPressed: () { + launchUrl(Uri.parse( + 'https://www.linkedin.com/in/eslam-hossam-591708316/')); + }, + icon: SvgPicture.asset( + 'assets/icons/linkedin.svg', + width: 30, + )), + const Spacer(), + IconButton( + onPressed: () { + launchUrl(Uri.parse('https://github.com/Eslam-Hossam1/')); + }, + icon: SvgPicture.asset( + 'assets/icons/github.svg', + width: 30, + )), + const Spacer(), + IconButton( + onPressed: () { + launchUrl(Uri.parse('https://wa.me/201028735105')); + }, + icon: SvgPicture.asset( + 'assets/icons/whatsapp-svgrepo-com.svg', + width: 30, + colorFilter: const ColorFilter.mode( + Color(0xff8C8C8E), + BlendMode.srcIn, + ), + )), + const Spacer( + flex: 2, + ), + ], + ), + ); + } +} diff --git a/lib/view/main/components/drawer/drawer_image.dart b/lib/view/main/components/drawer/drawer_image.dart new file mode 100644 index 0000000..510e1b5 --- /dev/null +++ b/lib/view/main/components/drawer/drawer_image.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../res/constants.dart'; + +class DrawerImage extends StatefulWidget { + const DrawerImage({super.key}); + + @override + State createState() => _DrawerImageState(); +} + +class _DrawerImageState extends State { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => _showProfileDialog(context), + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: (120.w).clamp(100, 170), + width: (120.w).clamp(100, 170), + padding: const EdgeInsets.all(defaultPadding / 5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.r), + gradient: LinearGradient( + colors: [ + Colors.pink, + Colors.blue.shade900, + ], + ), + boxShadow: [ + BoxShadow( + color: Colors.pink, + blurRadius: _isHovered ? 20 : 10, + offset: const Offset(0, 2)), + BoxShadow( + color: Colors.blue, + blurRadius: _isHovered ? 20 : 10, + offset: const Offset(0, -2)), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(30.r - (defaultPadding / 5)), + child: Hero( + tag: 'profile-image-hero', + child: Image.asset( + 'assets/images/profile.png', + fit: BoxFit.cover, + ), + ), + ), + ), + ), + ); + } +} + +void _showProfileDialog(BuildContext context) { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: 'Close', + barrierColor: Colors.black.withOpacity(0.6), + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (_, __, ___) => const SizedBox.shrink(), + transitionBuilder: (context, anim, secondaryAnim, child) { + final curved = CurvedAnimation(parent: anim, curve: Curves.easeOutCubic); + return Opacity( + opacity: anim.value, + child: Transform.scale( + scale: 0.95 + 0.05 * curved.value, + child: Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Material( + color: Colors.transparent, + child: ClipRRect( + borderRadius: BorderRadius.circular(24.r), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(40.r), + gradient: LinearGradient( + colors: [ + Colors.pink, + Colors.blue.shade900, + ], + ), + ), + constraints: BoxConstraints( + maxWidth: 800.w, + maxHeight: 800.h, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(40.r - 12), + child: Stack( + children: [ + Hero( + tag: 'profile-image-hero', + child: InteractiveViewer( + minScale: 0.8, + maxScale: 4.0, + child: Image.asset( + 'assets/images/profile.png', + fit: BoxFit.contain, + ), + ), + ), + Positioned( + top: 8, + right: 8, + child: IconButton( + onPressed: () => Navigator.of(context).maybePop(), + icon: + const Icon(Icons.close, color: Colors.white), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ); + }, + ); +} diff --git a/lib/view/main/components/drawer/header_info.dart b/lib/view/main/components/drawer/header_info.dart index e4295e7..f0fc05a 100644 --- a/lib/view/main/components/drawer/header_info.dart +++ b/lib/view/main/components/drawer/header_info.dart @@ -1,21 +1,241 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_portfolio/font_clamper_extension.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../../res/constants.dart'; -class AreaInfoText extends StatelessWidget { - const AreaInfoText({super.key, required this.title, required this.text}); +class CopyInfoText extends StatelessWidget { + const CopyInfoText( + {super.key, + required this.title, + required this.text, + required this.svgImage, + this.imageHeight, + this.imageWidth, + this.widthSpace, + required this.textToBeCopy}); final String title; final String text; + final String svgImage; + final double? imageHeight; + final double? imageWidth; + final String textToBeCopy; + final double? widthSpace; @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(bottom: defaultPadding/2), + padding: const EdgeInsets.only(bottom: defaultPadding / 2), + child: GestureDetector( + onLongPress: () { + // Copy the displayed value + Clipboard.setData(ClipboardData(text: textToBeCopy)); + // Use overlay-based FToast to support web without plugins + showCopiedToast(context, textToBeCopy); + }, + child: Row( + children: [ + SvgPicture.asset( + svgImage, + height: imageHeight ?? 15.w.clamp(15, 20), + width: imageWidth ?? 15.w.clamp(15, 20), + fit: BoxFit.cover, + ), + SizedBox( + width: widthSpace ?? 4.w.clamp(4, 16), + ), + Text( + title, + style: const TextStyle(color: Colors.white) + .copyWith(fontSize: 10.sp.fontClamper(14)), + ), + const Spacer(), + Text( + text, + style: TextStyle( + fontSize: 10.sp.fontClamper(14), + color: const Color(0xff8B8B8D)), + ), + if (MediaQuery.sizeOf(context).width >= 360) + SizedBox( + width: widthSpace ?? 4.w.clamp(4, 16), + ), + if (MediaQuery.sizeOf(context).width >= 360) + InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: textToBeCopy)); + showCopiedToast(context, textToBeCopy); + }, + child: SvgPicture.asset( + 'assets/icons/copy-svgrepo-com (1).svg', + height: imageHeight ?? 15.w.clamp(15, 20), + width: imageWidth ?? 15.w.clamp(15, 20), + fit: BoxFit.cover, + ), + ), + ], + ), + ), + ); + } + + void showCopiedToast(BuildContext context, String textToBeCopied) { + final fToast = FToast(); + fToast.init(context); + fToast.showToast( + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + child: Container( + margin: const EdgeInsets.only(bottom: 24), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: const Color(0xFF1E1E1E).withOpacity(0.95), + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: Colors.black26, blurRadius: 8, offset: Offset(0, 4)), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.check_circle, color: Colors.white, size: 18), + const SizedBox(width: 4), + Text( + 'Copied: $textToBeCopy', + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + ], + ), + ), + ); + } +} + +class ClickableInfo extends StatelessWidget { + const ClickableInfo( + {super.key, + required this.linkText, + required this.svgImage, + this.imageHeight, + this.imageWidth, + this.widthSpace, + required this.infoTitle, + required this.linkUrl}); + final String linkText; + final String svgImage; + final double? imageHeight; + final double? imageWidth; + final double? widthSpace; + final String infoTitle; + final String linkUrl; + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: defaultPadding / 2), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(title,style: const TextStyle(color: Colors.white),), - Text(text), + InfoType( + svgImage: svgImage, + imageHeight: imageHeight, + imageWidth: imageWidth, + widthSpace: widthSpace, + infoTitle: infoTitle, + linkUrl: linkUrl), + const Spacer(), + Link(linkUrl: linkUrl, linkText: linkText), ], ), ); } -} \ No newline at end of file +} + +class Link extends StatefulWidget { + const Link({ + super.key, + required this.linkUrl, + required this.linkText, + }); + final String linkUrl; + final String linkText; + + @override + State createState() => _LinkState(); +} + +class _LinkState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + launchUrl(Uri.parse(widget.linkUrl)); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Text( + widget.linkText, + style: TextStyle( + fontSize: 10.sp.fontClamper(14), + color: const Color(0xff8B8B8D), + ), + ), + ), + ); + } +} + +class InfoType extends StatefulWidget { + const InfoType({ + super.key, + required this.svgImage, + required this.imageHeight, + required this.imageWidth, + required this.widthSpace, + required this.infoTitle, + required this.linkUrl, + }); + + final String svgImage; + final double? imageHeight; + final double? imageWidth; + final double? widthSpace; + final String infoTitle; + final String linkUrl; + @override + State createState() => _InfoTypeState(); +} + +class _InfoTypeState extends State { + bool isHovered = false; + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + launchUrl(Uri.parse(widget.linkUrl)); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Row( + children: [ + SvgPicture.asset( + widget.svgImage, + height: widget.imageHeight ?? 15.w.clamp(15, 20), + width: widget.imageWidth ?? 15.w.clamp(15, 20), + fit: BoxFit.cover, + ), + SizedBox( + width: widget.widthSpace ?? 4.w.clamp(4, 16), + ), + Text( + widget.infoTitle, + style: const TextStyle(color: Colors.white) + .copyWith(fontSize: 10.sp.fontClamper(14)), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/main/components/drawer/knowledge.dart b/lib/view/main/components/drawer/knowledge.dart index 1905b4d..b90b6dd 100644 --- a/lib/view/main/components/drawer/knowledge.dart +++ b/lib/view/main/components/drawer/knowledge.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/font_clamper_extension.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/svg.dart'; import '../../../../res/constants.dart'; @@ -8,15 +10,19 @@ class KnowledgeText extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(bottom: defaultPadding/2), + padding: const EdgeInsets.only(bottom: defaultPadding / 2), child: Row( children: [ SvgPicture.asset('assets/icons/check.svg'), - SizedBox(width: defaultPadding/2,), - Text(knowledge), + const SizedBox( + width: defaultPadding / 2, + ), + Text( + knowledge, + style: TextStyle(fontSize: 12.sp.fontClamper(12)), + ), ], ), ); } } - diff --git a/lib/view/main/components/drawer/knowledges.dart b/lib/view/main/components/drawer/knowledges.dart index 9f728f1..cc2bfb2 100644 --- a/lib/view/main/components/drawer/knowledges.dart +++ b/lib/view/main/components/drawer/knowledges.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/font_clamper_extension.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'knowledge.dart'; @@ -7,17 +9,22 @@ class Knowledges extends StatelessWidget { @override Widget build(BuildContext context) { - return const Column( + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Divider(), + const Divider(), Padding( - padding: EdgeInsets.symmetric(vertical: 10), - child: Text('Knowledge',style: TextStyle(color: Colors.white),), + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text( + 'Knowledge', + style: + TextStyle(color: Colors.white, fontSize: 14.sp.fontClamper(14)), + ), ), - KnowledgeText(knowledge: 'Flutter, Dart'), - KnowledgeText(knowledge: 'Networking, Cyber Security'), - KnowledgeText(knowledge: 'GIT KNOWLEDGE'), + const KnowledgeText(knowledge: 'SOLID Principles'), + const KnowledgeText(knowledge: 'Architecture & Design Patterns'), + const KnowledgeText(knowledge: 'System Design & Analysis'), + const KnowledgeText(knowledge: 'Clean Code'), ], ); } diff --git a/lib/view/main/components/drawer/my_skill.dart b/lib/view/main/components/drawer/my_skill.dart index 3c98de0..f2ddb4a 100644 --- a/lib/view/main/components/drawer/my_skill.dart +++ b/lib/view/main/components/drawer/my_skill.dart @@ -1,37 +1,44 @@ import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/font_clamper_extension.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../../res/constants.dart'; class AnimatedLinearProgressIndicator extends StatelessWidget { - const AnimatedLinearProgressIndicator({super.key, required this.percentage, required this.title, this.image}); + const AnimatedLinearProgressIndicator( + {super.key, required this.percentage, required this.title, this.image}); final double percentage; final String title; final String? image; @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(bottom: defaultPadding/2), - child: TweenAnimationBuilder(tween: Tween(begin: 0.0,end: percentage), duration: const Duration(seconds: 1), builder: (context, value, child) { - return Column( - children: [ - Row( - children: [ - Image.asset(image!,height: 15,width: 15,fit: BoxFit.cover,), - const SizedBox(width: 5,), - Text(title,style: const TextStyle(color: Colors.white),), - const Spacer(), - Text('${(value*100).toInt().toString()}%'), - ], - ), - const SizedBox(height: defaultPadding/2,), - LinearProgressIndicator( - value: value, - backgroundColor: Colors.black, - color: Colors.amberAccent, - ), - ], - ); - },), + padding: const EdgeInsets.only(bottom: defaultPadding / 2), + child: Column( + children: [ + Row( + children: [ + Image.asset( + image!, + height: 15, + width: 15, + fit: BoxFit.cover, + ), + const SizedBox( + width: 8, + ), + Text( + title, + style: TextStyle( + color: Colors.white, fontSize: 12.sp.fontClamper(12)), + ), + ], + ), + const SizedBox( + height: defaultPadding / 2, + ), + ], + ), ); } } @@ -41,17 +48,53 @@ class MySKills extends StatelessWidget { @override Widget build(BuildContext context) { - return const Column( + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AnimatedLinearProgressIndicator(percentage: 0.7, title: 'Flutter',image: 'assets/icons/flutter.png',), - AnimatedLinearProgressIndicator(percentage: 0.9, title: 'Dart',image: 'assets/icons/dart.png'), - AnimatedLinearProgressIndicator(percentage: 0.6, title: 'Firebase',image: 'assets/icons/firebase.png'), - AnimatedLinearProgressIndicator(percentage: 0.85, title: 'Sqlite',image: 'assets/icons/dart.png'), - AnimatedLinearProgressIndicator(percentage: 0.8, title: 'Responsive Design',image: 'assets/icons/flutter.png'), - AnimatedLinearProgressIndicator(percentage: 0.9, title: 'Clean Architecture',image: 'assets/icons/flutter.png'), - AnimatedLinearProgressIndicator(percentage: 0.5, title: 'Bloc',image: 'assets/icons/bloc.png'), - AnimatedLinearProgressIndicator(percentage: 0.93, title: 'Getx',image: 'assets/icons/dart.png'), - ],); + const SizedBox( + height: defaultPadding / 2, + ), + Text( + 'Skills', + style: + TextStyle(color: Colors.white, fontSize: 14.sp.fontClamper(14)), + ), + const SizedBox( + height: defaultPadding / 2, + ), + const AnimatedLinearProgressIndicator( + percentage: 0.7, + title: 'Flutter', + image: 'assets/icons/flutter.png', + ), + const AnimatedLinearProgressIndicator( + percentage: 0.9, title: 'Dart', image: 'assets/icons/dart.png'), + const AnimatedLinearProgressIndicator( + percentage: 0.6, + title: 'Firebase', + image: 'assets/icons/firebase.png'), + const AnimatedLinearProgressIndicator( + percentage: 0.8, + title: 'Responsive & Adaptive Design', + image: 'assets/icons/flutter.png'), + const AnimatedLinearProgressIndicator( + percentage: 0.9, + title: 'Clean Architecture', + image: 'assets/icons/flutter.png'), + const AnimatedLinearProgressIndicator( + percentage: 0.5, + title: 'State Management', + image: 'assets/icons/bloc.png', + ), + const AnimatedLinearProgressIndicator( + percentage: 0.9, + title: 'Github', + image: 'assets/icons/icons8-github.png'), + const AnimatedLinearProgressIndicator( + percentage: 0.9, + title: 'CI/CD', + image: 'assets/icons/icons8-github.png'), + ], + ); } } diff --git a/lib/view/main/components/drawer/personal_info.dart b/lib/view/main/components/drawer/personal_info.dart new file mode 100644 index 0000000..5aae26a --- /dev/null +++ b/lib/view/main/components/drawer/personal_info.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import '../../../../res/constants.dart'; +import 'header_info.dart'; + +class PersonalInfo extends StatelessWidget { + const PersonalInfo({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: defaultPadding / 2, + ), + CopyInfoText( + svgImage: 'assets/icons/phone-svgrepo-com.svg', + title: 'Phone', + text: '+20 1028735105', + textToBeCopy: '+201028735105', + ), + CopyInfoText( + svgImage: 'assets/icons/gmail-icon-logo-svgrepo-com.svg', + title: 'Email', + text: 'eslamhossam.tech@gmail.com', + textToBeCopy: 'eslamhossam.tech@gmail.com',), + ClickableInfo( + svgImage: 'assets/icons/github-svgrepo-com_gray.svg', + infoTitle: 'Github', + linkText: 'Github.com/Eslam-Hossam1', + linkUrl: 'https://github.com/Eslam-Hossam1'), + ClickableInfo( + svgImage: 'assets/icons/linkedin-svgrepo-com.svg', + infoTitle: 'Linkedin', + linkText: 'Linkedin.com/eslam-hossam', + linkUrl: 'https://www.linkedin.com/in/eslam-hossam-591708316/'), + SizedBox( + height: defaultPadding / 2, + ), + ], + ); + } +} diff --git a/lib/view/main/components/navigation_bar.dart b/lib/view/main/components/navigation_bar.dart deleted file mode 100644 index 14133fd..0000000 --- a/lib/view/main/components/navigation_bar.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_portfolio/view%20model/responsive.dart'; -import 'package:flutter_portfolio/view/intro/components/side_menu_button.dart'; -import 'package:flutter_portfolio/view/main/components/connect_button.dart'; -import '../../../res/constants.dart'; -import 'navigation_button_list.dart'; -class TopNavigationBar extends StatelessWidget { - const TopNavigationBar({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - body: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - Padding( - padding: const EdgeInsets.all(defaultPadding), - child:!Responsive.isLargeMobile(context)? Image.asset('assets/images/triange_icon.png') : MenuButton(onTap: () => Scaffold.of(context).openDrawer(),), - ), - // if(Responsive.isLargeMobile(context)) MenuButton(), - const Spacer(flex: 2,), - if(!Responsive.isLargeMobile(context)) const NavigationButtonList(), - const Spacer(flex: 2,), - const ConnectButton(), - const Spacer(), - ], - ), - ); - } -} diff --git a/lib/view/main/components/navigation_button.dart b/lib/view/main/components/navigation_button.dart index a05df4b..cc59e8a 100644 --- a/lib/view/main/components/navigation_button.dart +++ b/lib/view/main/components/navigation_button.dart @@ -1,22 +1,133 @@ import 'package:flutter/material.dart'; -class NavigationTextButton extends StatelessWidget { +class NavigationTextButton extends StatefulWidget { final VoidCallback onTap; final String text; + final bool isActive; - const NavigationTextButton( - {super.key, required this.onTap, required this.text}); + const NavigationTextButton({ + super.key, + required this.onTap, + required this.text, + required this.isActive, + }); + + @override + State createState() => _NavigationTextButtonState(); +} + +class _NavigationTextButtonState extends State { + bool _isHovered = false; @override Widget build(BuildContext context) { - return TextButton( - onPressed: onTap, - child: Text( - text, - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith(fontWeight: FontWeight.bold, color: Colors.white), - )); + double width = MediaQuery.sizeOf(context).width; + final isDesktop = width > 800; + final isTiny = width <= 355; + final baseStyle = isTiny + ? Theme.of(context) + .textTheme + .titleSmall! + .copyWith(fontWeight: FontWeight.bold, fontSize: 10) + : isDesktop + ? Theme.of(context) + .textTheme + .labelLarge! + .copyWith(fontWeight: FontWeight.w800) + : Theme.of(context) + .textTheme + .labelMedium! + .copyWith(fontWeight: FontWeight.bold); + + final active = widget.isActive; + final hovered = _isHovered; + + // Colors for effects + const gradientColors = [Colors.pinkAccent, Colors.blueAccent]; + + Widget textWidget = Text( + widget.text, + style: baseStyle.copyWith( + color: active ? Colors.white : Colors.white70, + letterSpacing: active ? 0.6 : 0.2, + ), + ); + + // Apply a subtle gradient to active text + if (active) { + textWidget = ShaderMask( + shaderCallback: (bounds) => const LinearGradient( + colors: gradientColors, + ).createShader(bounds), + blendMode: BlendMode.srcIn, + child: textWidget, + ); + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: GestureDetector( + onTap: widget.onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOut, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + // color: active + // ? Colors.pink.withOpacity(0.1) + // : hovered + // ? Colors.pink.withOpacity(0.04) + // : Colors.transparent, + boxShadow: hovered || active + ? [ + BoxShadow( + color: Colors.pink.withOpacity(0.25), + offset: const Offset(-2, 0), + blurRadius: active ? 16 : 10, + ), + BoxShadow( + color: Colors.blue.withOpacity(0.25), + offset: const Offset(2, 0), + blurRadius: active ? 16 : 10, + ), + ] + : null, + border: Border.all( + color: active + ? Colors.white.withOpacity(0.25) + : Colors.white.withOpacity(0.08), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedScale( + duration: const Duration(milliseconds: 160), + scale: hovered ? 1.05 : 1.0, + child: textWidget, + ), + const SizedBox(height: 6), + // Underline indicator animates in on hover or when active + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 3, + width: active + ? 28 + : hovered + ? 20 + : 0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + gradient: const LinearGradient(colors: gradientColors), + ), + ), + ], + ), + ), + ), + ); } } diff --git a/lib/view/main/components/navigation_button_list.dart b/lib/view/main/components/navigation_button_list.dart index 046f07a..94b174c 100644 --- a/lib/view/main/components/navigation_button_list.dart +++ b/lib/view/main/components/navigation_button_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_portfolio/view%20model/controller.dart'; -import 'package:flutter_portfolio/view%20model/responsive.dart'; +import 'package:flutter_portfolio/view/main/components/whatsapp_colored_button.dart'; +import 'package:flutter_portfolio/widgets/menu_button.dart'; import 'navigation_button.dart'; @@ -8,19 +9,67 @@ class NavigationButtonList extends StatelessWidget { const NavigationButtonList({super.key}); @override Widget build(BuildContext context) { - return TweenAnimationBuilder(tween: Tween(begin: 0.0,end: 1.0), duration: const Duration(milliseconds: 200), builder: (context, value, child) { - return Transform.scale( - scale: value, - child: Row( - children: [ - NavigationTextButton(onTap: () {controller.animateToPage(0, duration: const Duration(milliseconds: 500), curve: Curves.easeIn);}, text: 'Home'), - if(!Responsive.isLargeMobile(context)) NavigationTextButton(onTap: () {}, text: 'About us'), - NavigationTextButton(onTap: () {controller.animateToPage(1, duration: const Duration(milliseconds: 500), curve: Curves.easeIn);}, text: 'Projects'), - NavigationTextButton(onTap: () {controller.animateToPage(2, duration: const Duration(milliseconds: 500), curve: Curves.easeIn);}, text: 'Certifications'), - NavigationTextButton(onTap: () {}, text: 'Achievements'), - ], - ), - ); - },); + double width = MediaQuery.sizeOf(context).width; + final isDesktop = width > 1040; + final isTiny = width <= 380; + final isLittleTiny = width <= 450; + final isBiggerTiny = width <= 500; + double bigPadding = MediaQuery.sizeOf(context).width * 0.1; + double innerButtonsGap = MediaQuery.sizeOf(context).width * 0.02; + return Padding( + padding: EdgeInsets.symmetric( + horizontal: isTiny + ? 12 + : isLittleTiny + ? 24 + : isBiggerTiny + ? 30 + : bigPadding), + child: Row( + children: [ + MenuButton( + onTap: () => Scaffold.of(context).openDrawer(), + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: currentPageIndex, + builder: (context, activeIndex, _) { + return ValueListenableBuilder( + valueListenable: isNavigating, + builder: (context, navigating, __) { + return IgnorePointer( + ignoring: navigating, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + NavigationTextButton( + isActive: activeIndex == 0, + onTap: () => goToSection(0), + text: 'Home', + ), + SizedBox(width: innerButtonsGap), + NavigationTextButton( + isActive: activeIndex == 1, + onTap: () => goToSection(1), + text: 'Projects', + ), + SizedBox(width: innerButtonsGap), + NavigationTextButton( + isActive: activeIndex == 2, + onTap: () => goToSection(2), + text: 'Certifications', + ), + ], + ), + ); + }, + ); + }, + ), + ), + width > 800 ? const WhatsappColoredButton() : const SizedBox.shrink(), + ], + ), + ); } -} \ No newline at end of file +} diff --git a/lib/view/main/components/whatsapp_colored_button.dart b/lib/view/main/components/whatsapp_colored_button.dart new file mode 100644 index 0000000..c6382e8 --- /dev/null +++ b/lib/view/main/components/whatsapp_colored_button.dart @@ -0,0 +1,76 @@ +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../../res/constants.dart'; + +class WhatsappColoredButton extends StatefulWidget { + const WhatsappColoredButton({super.key}); + + @override + State createState() => _WhatsappColoredButtonState(); +} + +class _WhatsappColoredButtonState extends State { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: defaultPadding), + child: InkWell( + onTap: () { + launchUrl(Uri.parse('https://wa.me/201028735105')); + }, + borderRadius: BorderRadius.circular(defaultPadding + 10), + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: 40, + width: 150, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(defaultPadding), + gradient: LinearGradient(colors: [ + Colors.pink, + Colors.blue.shade900, + ]), + boxShadow: [ + BoxShadow( + color: Colors.blue, + offset: const Offset(0, -1), + blurRadius: _isHovered + ? defaultPadding / 1.75 + : defaultPadding / 3.5), + BoxShadow( + color: Colors.red, + offset: const Offset(0, 1), + blurRadius: _isHovered + ? defaultPadding / 1.75 + : defaultPadding / 3.5), + ]), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + FontAwesomeIcons.whatsapp, + color: Colors.greenAccent, + size: 15, + ), + const SizedBox(width: defaultPadding / 4), + Text( + 'Whatsapp', + style: Theme.of(context).textTheme.labelSmall!.copyWith( + color: Colors.white, + letterSpacing: 1.2, + fontWeight: FontWeight.bold), + ), + ], + )), + ), + ), + ); + } +} diff --git a/lib/view/main/main_view.dart b/lib/view/main/main_view.dart index 07a91fd..c27b606 100644 --- a/lib/view/main/main_view.dart +++ b/lib/view/main/main_view.dart @@ -1,48 +1,110 @@ import 'package:flutter/material.dart'; -import 'package:flutter_portfolio/view%20model/controller.dart'; import 'package:flutter_portfolio/res/constants.dart'; -import 'package:flutter_portfolio/view/main/components/navigation_bar.dart'; -import '../../view model/responsive.dart'; +import 'package:flutter_portfolio/view%20model/controller.dart'; +import 'package:flutter_portfolio/view%20model/responsive.dart'; +import 'package:flutter_portfolio/view/certifications/components/certification_grid.dart'; +import 'package:flutter_portfolio/view/intro/components/intro_body.dart'; +import 'package:flutter_portfolio/view/main/components/navigation_button_list.dart'; +import 'package:flutter_portfolio/view/projects/components/projects_grid.dart'; +import 'package:flutter_portfolio/view/projects/components/title_text.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + import 'components/drawer/drawer.dart'; -import 'components/navigation_button_list.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -class MainView extends StatelessWidget { - const MainView({super.key, required this.pages}); - final List pages; + +class MainView extends StatefulWidget { + const MainView({ + super.key, + }); + // pages is ignored in one-page mode but kept for API compatibility + + @override + State createState() => _MainViewState(); +} + +class _MainViewState extends State { + @override + void initState() { + super.initState(); + setupSectionScrollListener(); + } + @override Widget build(BuildContext context) { + final minSectionHeight = MediaQuery.of(context).size.height - + (defaultPadding); // approx header spacing return Scaffold( drawer: const CustomDrawer(), body: Center( child: Column( children: [ - kIsWeb && !Responsive.isLargeMobile(context) ? const SizedBox(height:defaultPadding*2,) : const SizedBox(height:defaultPadding/2,), - const SizedBox( - height: 80, - child: TopNavigationBar(), - ), - if(Responsive.isLargeMobile(context)) const Row(children: [Spacer(),NavigationButtonList(),Spacer()],), + const SizedBox(height: defaultPadding / 2), + const NavigationButtonList(), + const SizedBox(height: defaultPadding / 2), Expanded( - flex: 9, - child: PageView( - scrollDirection: Axis.vertical, - controller: controller, - children: [ - ...pages - ], - ), - ) + flex: 9, + child: CustomScrollView( + controller: scrollController, + slivers: [ + // Intro Section Anchor + SliverToBoxAdapter( + child: KeyedSubtree( + key: introSectionKey, + child: const IntroBody(), + ), + ), + if (Responsive.isDesktop(context)) + SliverToBoxAdapter(child: SizedBox(height: 180.h)), + + SliverToBoxAdapter(child: SizedBox(height: 220.h)), + + SliverToBoxAdapter( + child: KeyedSubtree( + key: projectsSectionKey, + child: + const TitleText(prefix: 'Latest ', title: 'Projects'), + ), + ), + const SliverToBoxAdapter( + child: SizedBox(height: defaultPadding)), + Responsive( + desktop: ProjectGrid(crossAxisCount: 3), + extraLargeScreen: ProjectGrid(crossAxisCount: 4), + largeMobile: ProjectGrid(crossAxisCount: 1, ratio: 1.8), + mobile: ProjectGrid(crossAxisCount: 1, ratio: 1.5), + tablet: ProjectGrid(ratio: 1.4, crossAxisCount: 2), + ), + + SliverToBoxAdapter(child: SizedBox(height: 220.h)), + + // Certifications Section Anchor + Content + if (Responsive.isLargeMobile(context)) + const SliverToBoxAdapter( + child: SizedBox(height: defaultPadding)), + SliverToBoxAdapter( + child: KeyedSubtree( + key: certificationsSectionKey, + child: const TitleText(title: 'Certifications'), + ), + ), + const SliverToBoxAdapter( + child: SizedBox(height: defaultPadding)), + const Responsive( + desktop: CertificateGrid(crossAxisCount: 3, ratio: 1.4), + extraLargeScreen: + CertificateGrid(crossAxisCount: 4, ratio: 1.4), + largeMobile: CertificateGrid(crossAxisCount: 1, ratio: 1.8), + mobile: CertificateGrid(crossAxisCount: 1, ratio: 1.2), + tablet: CertificateGrid(ratio: 1.3, crossAxisCount: 2), + ), + + // Bottom padding + SliverToBoxAdapter(child: SizedBox(height: 200.h)), + ], + ), + ), ], ), ), ); } } - - - - - - - - diff --git a/lib/view/projects/components/feature_item.dart b/lib/view/projects/components/feature_item.dart new file mode 100644 index 0000000..3af63c7 --- /dev/null +++ b/lib/view/projects/components/feature_item.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/model/feature_model.dart'; + +class FeatureItem extends StatelessWidget { + const FeatureItem({ + super.key, + required this.featureModel, + required this.index, + }); + + final FeatureModel featureModel; + final int index; + @override + Widget build(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${index + 1}. ${featureModel.feature}", + style: const TextStyle(fontSize: 18, color: Colors.white), + ), + const SizedBox(height: 12), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Text( + "- ${featureModel.featurePoints[index]}", + style: const TextStyle(fontSize: 16, color: Colors.white70), + ); + }, + separatorBuilder: (context, index) { + return SizedBox( + height: width > 900 ? 8 : 12, + ); + }, + itemCount: featureModel.featurePoints.length, + ), + ], + ); + } +} diff --git a/lib/view/projects/components/features_list_view.dart b/lib/view/projects/components/features_list_view.dart new file mode 100644 index 0000000..46c9d9e --- /dev/null +++ b/lib/view/projects/components/features_list_view.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/model/feature_model.dart'; +import 'package:flutter_portfolio/view/projects/components/feature_item.dart'; + +class FeaturesListView extends StatelessWidget { + const FeaturesListView({ + super.key, + required this.featureModels, + }); + + final List featureModels; + + @override + Widget build(BuildContext context) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return FeatureItem(featureModel: featureModels[index], index: index); + }, + separatorBuilder: (context, index) { + return const SizedBox(height: 24); + }, + itemCount: featureModels.length, + ); + } +} diff --git a/lib/view/projects/components/hoverable_image_container.dart b/lib/view/projects/components/hoverable_image_container.dart new file mode 100644 index 0000000..033d609 --- /dev/null +++ b/lib/view/projects/components/hoverable_image_container.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/widgets/custom_project_grid_image.dart'; + +class HoverableImageContainer extends StatefulWidget { + final String imageUrl; + final VoidCallback onTap; + final double height; + final double width; + const HoverableImageContainer({ + required this.imageUrl, + required this.onTap, + this.height = 120, + this.width = 120, + Key? key, + }) : super(key: key); + + @override + State createState() => + _HoverableImageContainerState(); +} + +class _HoverableImageContainerState extends State { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: GestureDetector( + onTap: widget.onTap, + child: Stack( + children: [ + Container( + height: widget.height, + width: widget.width, + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: const LinearGradient( + colors: [Colors.pinkAccent, Colors.blue], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + // color: Colors.black, + boxShadow: [ + BoxShadow( + color: Colors.pink.withOpacity(0.5), + offset: const Offset(-2, 0), + blurRadius: 10, + ), + BoxShadow( + color: Colors.blue.withOpacity(0.5), + offset: const Offset(2, 0), + blurRadius: 10, + ), + ], + ), + // clipBehavior: Clip.hardEdge, + child: ClipRRect( + borderRadius: BorderRadius.circular(9), + child: CustomProjectGridImage( + imageUrl: widget.imageUrl, + fit: BoxFit.cover, + height: widget.height, + width: widget.width, + ), + ), + ), + if (_isHovered) + Container( + height: widget.height, + width: widget.width, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.black.withOpacity(0.45), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/projects/components/image_viewer.dart b/lib/view/projects/components/image_viewer.dart index 788023d..ed68e04 100644 --- a/lib/view/projects/components/image_viewer.dart +++ b/lib/view/projects/components/image_viewer.dart @@ -5,7 +5,7 @@ class ImageViewer { ImageViewer(BuildContext context, String image) { showGeneralDialog( barrierColor: Colors.black, - transitionDuration: Duration(milliseconds: 500), + transitionDuration: const Duration(milliseconds: 500), barrierDismissible: true, barrierLabel: 'Barrier', context: context, @@ -13,20 +13,18 @@ class ImageViewer { return Center( child: Hero( tag: 'IMAGEVIEW', - child: Container( + child: SizedBox( height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width, child: Scaffold( - backgroundColor: Colors.black, - body: Container( + backgroundColor: Colors.black, + body: Container( color: Colors.white, child: Center( - child: PhotoView(imageProvider: AssetImage(image)) - ), - )), - ), + child: PhotoView(imageProvider: AssetImage(image))), + )), ), - + ), ); }, ); diff --git a/lib/view/projects/components/media_viewer_desktop.dart b/lib/view/projects/components/media_viewer_desktop.dart new file mode 100644 index 0000000..84c3365 --- /dev/null +++ b/lib/view/projects/components/media_viewer_desktop.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/model/feature_model.dart'; +import 'package:flutter_portfolio/view/projects/components/project_info_section.dart'; +import 'project_media_widgets.dart'; + +class ProjectMediaDesktopLayout extends StatelessWidget { + final int projectId; + final String name; + final String description; + final List images; + final List videos; + final bool isTablet; + final double horizontalPadding; + final double verticalPadding; + final List featureModels; + final String link; + + const ProjectMediaDesktopLayout({ + required this.projectId, + required this.name, + required this.description, + required this.featureModels, + required this.images, + required this.videos, + required this.isTablet, + required this.horizontalPadding, + required this.verticalPadding, + required this.link, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + vertical: verticalPadding, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + VideoGalleryWithGradientContainer(videos: videos), + const SizedBox(height: 32), + ProjectInfoSection( + name: name, + description: description, + featureModels: featureModels, + link: link, + ), + ], + ), + ), + const SizedBox(width: 32), + Expanded( + flex: 1, + child: ImageGallery( + images: images, + projectId: projectId, + crossAxisCount: isTablet ? 2 : 3, + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/projects/components/media_viewer_mobile.dart b/lib/view/projects/components/media_viewer_mobile.dart new file mode 100644 index 0000000..ac25740 --- /dev/null +++ b/lib/view/projects/components/media_viewer_mobile.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/model/feature_model.dart'; +import 'package:flutter_portfolio/view/projects/components/project_info_section.dart'; +import 'project_media_widgets.dart'; + +class ProjectMediaMobileLayout extends StatelessWidget { + final int projectId; + final String description; + final String name; + final List images; + final List videos; + final int gridCrossAxisCount; + final bool isMobile; + final double horizontalPadding; + final double verticalPadding; + final List featureModels; + final String link; + + const ProjectMediaMobileLayout({ + required this.projectId, + required this.description, + required this.name, + required this.images, + required this.videos, + required this.gridCrossAxisCount, + required this.isMobile, + required this.horizontalPadding, + required this.verticalPadding, + required this.link, + Key? key, + required this.featureModels, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + vertical: verticalPadding, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + SizedBox( + height: 220, + child: Align( + alignment: Alignment.center, + child: VideoGalleryWithGradientContainer( + videos: videos, + )), + ), + const SizedBox(height: 24), + ImageGallery( + images: images, + projectId: projectId, + crossAxisCount: gridCrossAxisCount, + ), + const SizedBox(height: 32), + ProjectInfoSection( + name: name, + description: description, + featureModels: featureModels, + link: link, + ), + ], + ), + ); + } +} diff --git a/lib/view/projects/components/multi_video_player.dart b/lib/view/projects/components/multi_video_player.dart new file mode 100644 index 0000000..7d6c252 --- /dev/null +++ b/lib/view/projects/components/multi_video_player.dart @@ -0,0 +1,329 @@ +import 'package:flutter/material.dart'; +import 'package:chewie/chewie.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:video_player/video_player.dart'; + +// Global video state manager to preserve states across widget rebuilds +class VideoStateManager { + static final VideoStateManager _instance = VideoStateManager._internal(); + factory VideoStateManager() => _instance; + VideoStateManager._internal(); + + final Map _videoPositions = {}; + final Map _videoPlayingStates = {}; + + void saveVideoState(String videoUrl, Duration position, bool isPlaying) { + _videoPositions[videoUrl] = position; + _videoPlayingStates[videoUrl] = isPlaying; + } + + Duration? getVideoPosition(String videoUrl) { + return _videoPositions[videoUrl]; + } + + bool? getVideoPlayingState(String videoUrl) { + return _videoPlayingStates[videoUrl]; + } + + void clearVideoState(String videoUrl) { + _videoPositions.remove(videoUrl); + _videoPlayingStates.remove(videoUrl); + } +} + +class MultiVideoPlayer extends StatefulWidget { + final List videos; + final bool autoPlay; + final bool showControls; + + const MultiVideoPlayer({ + Key? key, + required this.videos, + this.autoPlay = false, + this.showControls = true, + }) : super(key: key); + + @override + State createState() => _MultiVideoPlayerState(); +} + +class _MultiVideoPlayerState extends State { + late PageController _pageController; + late List _controllers; + late List _chewieControllers; + int _currentVideoIndex = 0; + bool _isInitialized = false; + + final VideoStateManager _stateManager = VideoStateManager(); + + @override + void initState() { + super.initState(); + _pageController = PageController(); + _initializeVideos(); + } + + Future _initializeVideos() async { + _controllers = []; + _chewieControllers = List.filled(widget.videos.length, null); + + // Initialize all video controllers + for (int i = 0; i < widget.videos.length; i++) { + final controller = + VideoPlayerController.networkUrl(Uri.parse(widget.videos[i])); + _controllers.add(controller); + + try { + await controller.initialize(); + + final videoUrl = widget.videos[i]; + + // Restore previous position if available + final savedPosition = _stateManager.getVideoPosition(videoUrl); + if (savedPosition != null) { + controller.seekTo(savedPosition); + } + + _chewieControllers[i] = ChewieController( + videoPlayerController: controller, + autoPlay: widget.autoPlay && + i == 0 && + savedPosition == null, // Only autoplay if no previous position + looping: false, + showControls: widget.showControls, + allowFullScreen: true, + allowMuting: true, + showOptions: true, + ); + + // Add listener to track video position and playing state + controller.addListener(() { + _stateManager.saveVideoState( + videoUrl, controller.value.position, controller.value.isPlaying); + }); + + // Restore playing state if it was playing before + final savedPlayingState = _stateManager.getVideoPlayingState(videoUrl); + if (savedPlayingState == true) { + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted && controller.value.isInitialized) { + controller.play(); + } + }); + } + } catch (e) { + // Error initializing video - will show error state in UI + _chewieControllers[i] = null; + } + } + + if (mounted) { + setState(() { + _isInitialized = true; + }); + } + } + + @override + void dispose() { + // Save current video states before disposing + _saveVideoStates(); + + _pageController.dispose(); + for (var controller in _controllers) { + controller.dispose(); + } + for (var chewieController in _chewieControllers) { + chewieController?.dispose(); + } + super.dispose(); + } + + void _saveVideoStates() { + for (int i = 0; i < _controllers.length; i++) { + if (_controllers[i].value.isInitialized) { + final videoUrl = widget.videos[i]; + _stateManager.saveVideoState(videoUrl, _controllers[i].value.position, + _controllers[i].value.isPlaying); + } + } + } + + void _goToVideo(int index) { + if (index >= 0 && index < widget.videos.length) { + _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + void _pauseAllVideos() { + for (var chewieController in _chewieControllers) { + chewieController?.pause(); + } + } + + @override + Widget build(BuildContext context) { + if (!_isInitialized) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.black, + ), + clipBehavior: Clip.hardEdge, + alignment: Alignment.center, + child: const CircularProgressIndicator(color: Colors.white), + ); + } + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.black, + ), + clipBehavior: Clip.hardEdge, + child: Stack( + children: [ + // Video Player + PageView.builder( + controller: _pageController, + onPageChanged: (index) { + setState(() { + _pauseAllVideos(); + _currentVideoIndex = index; + }); + }, + itemCount: widget.videos.length, + itemBuilder: (context, index) { + final chewieController = _chewieControllers[index]; + + if (chewieController == null) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + color: Colors.white, + size: 48, + ), + SizedBox(height: 16), + Text( + 'Failed to load video', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + ); + } + + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Chewie(controller: chewieController), + ); + }, + ), + + // Navigation Controls + if (widget.videos.length > 1) ...[ + // Previous Button + if (_currentVideoIndex > 0) + Positioned( + left: 16, + top: 0, + bottom: 0, + child: Center( + child: GestureDetector( + onTap: () => _goToVideo(_currentVideoIndex - 1), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Container( + width: 36.w.clamp(28, 48), + height: 36.w.clamp(28, 48), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(25), + ), + child: Center( + child: SvgPicture.asset( + 'assets/icons/arrow-ios-back-svgrepo-com.svg', + colorFilter: const ColorFilter.mode( + Colors.white, BlendMode.srcIn), + width: 24.w.clamp(20, 28), + ), + ), + ), + ), + ), + ), + ), + + // Next Button + if (_currentVideoIndex < widget.videos.length - 1) + Positioned( + right: 16, + top: 0, + bottom: 0, + child: Center( + child: GestureDetector( + onTap: () => _goToVideo(_currentVideoIndex + 1), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Container( + width: 36.w.clamp(28, 48), + height: 36.w.clamp(28, 48), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(25), + ), + child: Transform.rotate( + angle: 3.14159265, + child: Center( + child: SvgPicture.asset( + 'assets/icons/arrow-ios-back-svgrepo-com.svg', + colorFilter: const ColorFilter.mode( + Colors.white, BlendMode.srcIn), + width: 24.w.clamp(20, 28), + ), + ), + ), + ), + ), + ), + ), + ), + + // Video Counter + Positioned( + top: 16, + right: 16, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(15), + ), + child: Text( + '${_currentVideoIndex + 1} / ${widget.videos.length}', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ], + ), + ); + } +} diff --git a/lib/view/projects/components/project_deatail.dart b/lib/view/projects/components/project_deatail.dart new file mode 100644 index 0000000..c6514f6 --- /dev/null +++ b/lib/view/projects/components/project_deatail.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/model/projects_models_list.dart'; +import 'package:flutter_portfolio/view/projects/components/project_link.dart'; + +import '../../../res/constants.dart'; +import '../../../view model/responsive.dart'; + +class ProjectDetail extends StatelessWidget { + final int index; + const ProjectDetail({super.key, required this.index}); + @override + Widget build(BuildContext context) { + var size = MediaQuery.sizeOf(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.topCenter, + child: Text( + projectList[index].name, + style: Theme.of(context) + .textTheme + .headlineSmall! + .copyWith(color: Colors.white, fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Responsive.isMobile(context) + ? const SizedBox( + height: defaultPadding / 2, + ) + : const SizedBox( + height: defaultPadding, + ), + Text( + removeBreakLines(projectList[index].description), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.grey, height: 1.5), + maxLines: size.width > 700 && size.width < 750 + ? 3 + : size.width < 470 + ? 2 + : size.width > 600 && size.width < 700 + ? 6 + : size.width > 900 && size.width < 1060 + ? 6 + : 4, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + ProjectLinks( + index: index, + ), + const SizedBox( + height: defaultPadding / 2, + ), + ], + ); + } + + removeBreakLines(String description) { + return description.replaceAll('\n', ' '); + } +} diff --git a/lib/view/projects/components/project_details_view.dart b/lib/view/projects/components/project_details_view.dart new file mode 100644 index 0000000..3058d50 --- /dev/null +++ b/lib/view/projects/components/project_details_view.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/model/feature_model.dart'; +import 'package:flutter_portfolio/res/constants.dart'; + +import 'media_viewer_desktop.dart'; +import 'media_viewer_mobile.dart'; + +class ProjectDetailsView extends StatefulWidget { + final int projectId; + final String name; + final String description; + final List images; + final List videos; + final List featureModels; + final String githubLink; + + const ProjectDetailsView({ + Key? key, + required this.projectId, + required this.name, + required this.description, + required this.images, + required this.videos, + required this.featureModels, + required this.githubLink, + }) : super(key: key); + + @override + State createState() => _ProjectDetailsViewState(); +} + +class _ProjectDetailsViewState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final isBigDesktop = width >= 1300; + final isDesktop = width >= 1000 && width < 1300; + final isTablet = width >= 600 && width < 1000; + final isBigMobile = width >= 500 && width < 600; + final isMobile = width < 700; + final gridCrossAxisCount = isDesktop + ? 3 + : (isTablet + ? 2 + : isBigMobile + ? 6 + : 4); + + return Scaffold( + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return CustomScrollView( + slivers: [ + SliverAppBar( + surfaceTintColor: Colors.transparent, + backgroundColor: bgColor, + floating: true, + automaticallyImplyLeading: false, + leading: isMobile + ? IconButton( + icon: + const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ) + : null, + title: Text( + widget.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 24, + ), + ), + centerTitle: true, + ), + SliverToBoxAdapter( + child: (isBigDesktop || isDesktop || isTablet) + ? ProjectMediaDesktopLayout( + link: widget.githubLink, + name: widget.name, + description: widget.description, + featureModels: widget.featureModels, + images: widget.images, + videos: widget.videos, + projectId: widget.projectId, + isTablet: isTablet, + horizontalPadding: isBigDesktop ? width * .07 : 24, + verticalPadding: isBigDesktop ? 36 : 16, + ) + : ProjectMediaMobileLayout( + link: widget.githubLink, + name: widget.name, + description: widget.description, + featureModels: widget.featureModels, + images: widget.images, + videos: widget.videos, + projectId: widget.projectId, + gridCrossAxisCount: gridCrossAxisCount, + isMobile: isMobile, + horizontalPadding: isBigMobile ? width * .05 : 24, + verticalPadding: isBigMobile ? 36 : 16, + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/view/projects/components/project_info.dart b/lib/view/projects/components/project_info.dart index a84a772..4ef6da5 100644 --- a/lib/view/projects/components/project_info.dart +++ b/lib/view/projects/components/project_info.dart @@ -1,68 +1,79 @@ import 'package:flutter/material.dart'; -import 'package:flutter_portfolio/view%20model/responsive.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:get/get.dart'; -import 'package:url_launcher/url_launcher.dart'; -import '../../../model/project_model.dart'; +import 'package:flutter_portfolio/view/projects/components/project_deatail.dart'; +import 'package:go_router/go_router.dart'; + import '../../../res/constants.dart'; -import '../../../view model/getx_controllers/projects_controller.dart'; -import 'image_viewer.dart'; -class ProjectStack extends StatelessWidget { - final controller = Get.put(ProjectController()); - ProjectStack({super.key, required this.index}); + +class ProjectStack extends StatefulWidget { final int index; + const ProjectStack({super.key, required this.index}); + + @override + State createState() => _ProjectStackState(); +} + +class _ProjectStackState extends State { + bool isHovered = false; @override Widget build(BuildContext context) { - var size=MediaQuery.sizeOf(context); - return InkWell( - onHover: (value) { - controller.onHover(index, value); - }, + return GestureDetector( onTap: () { - ImageViewer(context,projectList[index].image); + context.push('/project-details/${widget.index}'); }, - borderRadius: BorderRadius.circular(30), - child: AnimatedContainer( - padding: const EdgeInsets.only(left: defaultPadding,right: defaultPadding,top: defaultPadding), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: bgColor), - duration: const Duration(milliseconds: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Align(alignment: Alignment.topCenter,child: Text( - projectList[index].name, - style: Theme.of(context) - .textTheme - .headlineSmall! - .copyWith( - color: Colors.white, - fontWeight: FontWeight.bold), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ),), - Responsive.isMobile(context) ? const SizedBox(height: defaultPadding/2,) : const SizedBox(height: defaultPadding,), - Text(projectList[index].description,style: const TextStyle(color: Colors.grey,height: 1.5),maxLines: size.width>700 && size.width< 750 ? 3: size.width<470 ? 2 : size.width>600 && size.width<700 ? 6: size.width>900 && size.width <1060 ? 6: 4 ,overflow: TextOverflow.ellipsis,), - const Spacer(), - Row( - children: [ - Row( - children: [ - const Text('Check on Github',style: TextStyle(color: Colors.white),overflow: TextOverflow.ellipsis), - IconButton(onPressed: () {launchUrl(Uri.parse(projectList[index].link));}, icon: SvgPicture.asset('assets/icons/github.svg')), - ], + child: ClipRRect( + borderRadius: BorderRadius.circular(30), + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric( + vertical: defaultPadding, horizontal: defaultPadding), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + gradient: const LinearGradient(colors: [ + Colors.pinkAccent, + Colors.blue, + ]), + boxShadow: [ + BoxShadow( + color: Colors.pink, + offset: const Offset(-2, 0), + blurRadius: isHovered ? 20 : 10, + ), + BoxShadow( + color: Colors.blue, + offset: const Offset(2, 0), + blurRadius: isHovered ? 20 : 10, + ), + ]), + child: Container( + padding: const EdgeInsets.only( + left: defaultPadding, + right: defaultPadding, + top: defaultPadding), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: bgColor, + boxShadow: [ + BoxShadow( + color: Colors.pink.withOpacity(0.1), + offset: const Offset(-2, 0), + blurRadius: 10, + ), + BoxShadow( + color: Colors.blue.withOpacity(0.1), + offset: const Offset(2, 0), + blurRadius: 10, ), - const Spacer(), - TextButton( - onPressed: () { - launchUrl(Uri.parse(projectList[index].link)); - }, child: const Text('Read More>>',overflow: TextOverflow.ellipsis,style: TextStyle(color: Colors.amber,fontWeight: FontWeight.bold,fontSize: 10),)) ], ), - const SizedBox(height: defaultPadding/2,), - ], - )), + child: ProjectDetail(index: widget.index), + ), + ), + ), + ), ); } -} \ No newline at end of file +} diff --git a/lib/view/projects/components/project_info_section.dart b/lib/view/projects/components/project_info_section.dart new file mode 100644 index 0000000..bd6667f --- /dev/null +++ b/lib/view/projects/components/project_info_section.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/model/feature_model.dart'; +import 'package:flutter_portfolio/view/projects/components/features_list_view.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ProjectInfoSection extends StatelessWidget { + const ProjectInfoSection({ + super.key, + required this.name, + required this.description, + required this.featureModels, + required this.link, + }); + + final String name; + final String description; + final List featureModels; + final String link; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProjectClickableName(name: name, link: link), + const SizedBox(height: 16), + Text( + description, + style: const TextStyle(fontSize: 18, color: Colors.white70), + ), + const SizedBox(height: 24), + const Text( + 'Features', + style: TextStyle( + fontSize: 22, color: Colors.white, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + FeaturesListView(featureModels: featureModels), + const SizedBox(height: 16), + ], + ); + } +} + +class ProjectClickableName extends StatelessWidget { + const ProjectClickableName({ + super.key, + required this.name, + required this.link, + }); + final String link; + final String name; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + launchUrl(Uri.parse(link)); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Text( + name, + style: const TextStyle( + fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ); + } +} diff --git a/lib/view/projects/components/project_link.dart b/lib/view/projects/components/project_link.dart new file mode 100644 index 0000000..b013e19 --- /dev/null +++ b/lib/view/projects/components/project_link.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/model/projects_models_list.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ProjectLinks extends StatelessWidget { + final int index; + const ProjectLinks({super.key, required this.index}); + @override + Widget build(BuildContext context) { + return Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FittedBox( + child: Row( + children: [ + TextButton( + onPressed: () { + launchUrl(Uri.parse(projectList[index].link)); + }, + child: const Text('Check on Github', + style: TextStyle(color: Colors.white), + overflow: TextOverflow.ellipsis), + ), + IconButton( + onPressed: () { + launchUrl(Uri.parse(projectList[index].link)); + }, + icon: SvgPicture.asset('assets/icons/github.svg')), + ], + ), + ), + FittedBox( + child: TextButton( + onPressed: () { + launchUrl(Uri.parse(projectList[index].link)); + }, + child: const Text( + 'Read More>>', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.amber, + fontWeight: FontWeight.bold, + fontSize: 10), + )), + ) + ], + ), + ); + } +} diff --git a/lib/view/projects/components/project_media_widgets.dart b/lib/view/projects/components/project_media_widgets.dart new file mode 100644 index 0000000..13bc9b3 --- /dev/null +++ b/lib/view/projects/components/project_media_widgets.dart @@ -0,0 +1,367 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_portfolio/routing/browser_routing_helper.dart'; +import 'hoverable_image_container.dart'; +import 'package:photo_view/photo_view.dart'; +import 'multi_video_player.dart'; +import 'package:go_router/go_router.dart'; + +class VideoGalleryWithGradientContainer extends StatelessWidget { + final List videos; + + const VideoGalleryWithGradientContainer({ + required this.videos, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 16 / 9, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: const LinearGradient( + colors: [Colors.pinkAccent, Colors.blue], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: const EdgeInsets.all(4), + child: ClipRRect( + borderRadius: + BorderRadius.circular(20), // Inner radius (24 - 4 padding) + child: MultiVideoPlayer( + videos: videos, + autoPlay: false, + showControls: true, + ), + ), + ), + ); + } +} + +class ImageGallery extends StatefulWidget { + final int projectId; + final List images; + final int crossAxisCount; + const ImageGallery({ + required this.images, + required this.projectId, + this.crossAxisCount = 3, + Key? key, + }) : super(key: key); + + @override + State createState() => _ImageGalleryState(); +} + +class _ImageGalleryState extends State { + bool _showAllImages = false; + static const int _initialImageCount = 12; + + @override + Widget build(BuildContext context) { + final displayImages = _showAllImages + ? widget.images + : widget.images.take(_initialImageCount).toList(); + final hasMoreImages = widget.images.length > _initialImageCount; + + return Column( + children: [ + GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 1, + ), + itemCount: displayImages.length, + itemBuilder: (context, index) { + return HoverableImageContainer( + imageUrl: displayImages[index], + onTap: () { + final initialIndex = + widget.images.indexOf(displayImages[index]); + context.push( + '/project-details/${widget.projectId}/viewer/$initialIndex'); + }, + height: 120, + width: 120, + ); + }, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + ), + if (hasMoreImages && !_showAllImages) ...[ + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + _showAllImages = true; + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Show More', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ], + ); + } +} + +class ImageGalleryDialog extends StatefulWidget { + final List images; + final int initialIndex; + + const ImageGalleryDialog({ + required this.images, + required this.initialIndex, + Key? key, + }) : super(key: key); + + @override + State createState() => _ImageGalleryDialogState(); +} + +class _ImageGalleryDialogState extends State { + late PageController _controller; + late int _currentIndex; + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _currentIndex = widget.initialIndex; + _controller = PageController(initialPage: _currentIndex); + _focusNode = FocusNode(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _goTo(int newIndex) { + if (newIndex >= 0 && newIndex < widget.images.length) { + setState(() => _currentIndex = newIndex); + _controller.animateToPage( + newIndex, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + void _handleKeyPress(KeyEvent event) { + if (event is KeyDownEvent) { + switch (event.logicalKey) { + case LogicalKeyboardKey.arrowLeft: + if (_currentIndex > 0) _goTo(_currentIndex - 1); + break; + case LogicalKeyboardKey.arrowRight: + if (_currentIndex < widget.images.length - 1) { + _goTo(_currentIndex + 1); + } + break; + case LogicalKeyboardKey.space: + // Space can be used for navigation on web + if (_currentIndex < widget.images.length - 1) { + _goTo(_currentIndex + 1); + } + break; + case LogicalKeyboardKey.home: + _goTo(0); + break; + case LogicalKeyboardKey.end: + _goTo(widget.images.length - 1); + break; + } + } + } + + // Get responsive spacing based on screen width + double _getHorizontalSpacing(double width) { + if (width < 360) return 4.0; // Very small phones + if (width < 420) return 8.0; // Small phones + if (width < 768) return 16.0; // Regular phones + return 24.0; // Tablets and larger + } + + double _getTopSpacing(double width) { + if (width < 360) return 16.0; // Account for status bar + if (width < 420) return 24.0; + if (width < 768) return 32.0; + return 40.0; + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final width = size.width; + final horizontalSpacing = _getHorizontalSpacing(width); + final topSpacing = _getTopSpacing(width); + + return KeyboardListener( + focusNode: _focusNode, + autofocus: true, + onKeyEvent: _handleKeyPress, + child: Dialog( + backgroundColor: Colors.black, + insetPadding: EdgeInsets.zero, + child: SizedBox.expand( + child: Stack( + alignment: Alignment.center, + children: [ + // Main image viewer with safe padding + Positioned.fill( + child: Padding( + padding: EdgeInsets.only( + top: topSpacing + 40, // Space for close button + bottom: 60, // Space for counter if needed + left: (horizontalSpacing * 2) + 44, // Space for nav buttons + right: (horizontalSpacing * 2) + 44, + ), + child: PageView.builder( + controller: _controller, + itemCount: widget.images.length, + onPageChanged: (i) => setState(() => _currentIndex = i), + itemBuilder: (context, index) { + return PhotoView( + imageProvider: NetworkImage(widget.images[index]), + backgroundDecoration: + const BoxDecoration(color: Colors.black), + minScale: PhotoViewComputedScale.contained, + maxScale: PhotoViewComputedScale.covered * 2.0, + initialScale: PhotoViewComputedScale.contained, + ); + }, + ), + ), + ), + + // Navigation buttons - positioned outside image area + if (_currentIndex > 0) + Positioned( + left: horizontalSpacing, + top: 0, + bottom: 0, + child: Center( + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(22), + border: Border.all(color: Colors.white24, width: 1), + ), + child: IconButton( + icon: const Icon( + Icons.arrow_back_ios_new, + color: Colors.white, + size: 20, + ), + onPressed: () => _goTo(_currentIndex - 1), + padding: EdgeInsets.zero, + ), + ), + ), + ), + + if (_currentIndex < widget.images.length - 1) + Positioned( + right: horizontalSpacing, + top: 0, + bottom: 0, + child: Center( + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(22), + border: Border.all(color: Colors.white24, width: 1), + ), + child: IconButton( + icon: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + size: 20, + ), + onPressed: () => _goTo(_currentIndex + 1), + padding: EdgeInsets.zero, + ), + ), + ), + ), + + // Close button + Positioned( + top: topSpacing, + right: horizontalSpacing, + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(22), + border: Border.all(color: Colors.white24, width: 1), + ), + child: IconButton( + icon: const Icon( + Icons.close, + color: Colors.white, + size: 20, + ), + onPressed: () => BrowserRoutingHelper.back(), + padding: EdgeInsets.zero, + ), + ), + ), + + // Image counter + if (widget.images.length > 1) + Positioned( + bottom: 15, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.white24, width: 1), + ), + child: Text( + '${_currentIndex + 1} / ${widget.images.length}', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/view/projects/components/projects_grid.dart b/lib/view/projects/components/projects_grid.dart index f24b87d..b83a1fb 100644 --- a/lib/view/projects/components/projects_grid.dart +++ b/lib/view/projects/components/projects_grid.dart @@ -1,46 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/model/projects_models_list.dart'; import 'package:flutter_portfolio/view/projects/components/project_info.dart'; import 'package:get/get.dart'; -import '../../../model/project_model.dart'; -import '../../../res/constants.dart'; + import '../../../view model/getx_controllers/projects_controller.dart'; + class ProjectGrid extends StatelessWidget { final int crossAxisCount; final double ratio; - ProjectGrid({super.key, this.crossAxisCount = 3, this.ratio=1.3}); + ProjectGrid({super.key, this.crossAxisCount = 3, this.ratio = 1.3}); final controller = Get.put(ProjectController()); @override Widget build(BuildContext context) { - return GridView.builder( + return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 30), - itemCount: projectList.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, childAspectRatio: ratio), - itemBuilder: (context, index) { - return Obx(() => AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.symmetric( - vertical: defaultPadding, horizontal: defaultPadding), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - gradient: const LinearGradient(colors: [ - Colors.pinkAccent, - Colors.blue, - ]), - boxShadow: [ - BoxShadow( - color: Colors.pink, - offset: Offset(-2, 0), - blurRadius: controller.hovers[index] ? 20 : 10, - ), - BoxShadow( - color: Colors.blue, - offset: Offset(2, 0), - blurRadius: controller.hovers[index] ? 20 : 10,), - ]), - child: ProjectStack(index: index) - )); - }, + sliver: SliverGrid.builder( + itemCount: projectList.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, childAspectRatio: ratio), + itemBuilder: (context, index) { + return ProjectStack(index: index); + }, + ), ); } -} \ No newline at end of file +} diff --git a/lib/view/projects/components/title_text.dart b/lib/view/projects/components/title_text.dart index 9ffd079..1ce31eb 100644 --- a/lib/view/projects/components/title_text.dart +++ b/lib/view/projects/components/title_text.dart @@ -1,45 +1,52 @@ - -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import '../../../view model/responsive.dart'; class TitleText extends StatelessWidget { - const TitleText({super.key, required this.prefix, required this.title}); + const TitleText({super.key, this.prefix = '', required this.title}); + final String prefix; final String title; + @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('$prefix ',style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Colors.white, - fontSize:!Responsive.isDesktop(context) ? Responsive.isLargeMobile(context)? 20: 30 : 50, - fontWeight: FontWeight.bold - ),), - kIsWeb && Responsive.isDesktop(context) ? ShaderMask( - shaderCallback: (bounds) { - return LinearGradient( - end: Alignment.centerRight, - begin: Alignment.centerLeft, - colors: [ - Colors.pink, - Colors.cyanAccent, - ]).createShader(bounds); - }, - child: Text(title,style: Theme.of(context).textTheme.titleMedium!.copyWith( + Text( + '$prefix ', + style: Theme.of(context).textTheme.titleMedium!.copyWith( color: Colors.white, - fontSize:!Responsive.isDesktop(context) ? Responsive.isLargeMobile(context)? 20: 30 : 50, - fontWeight: FontWeight.bold - ),), - ) : Text(title,style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Colors.white, - fontSize:!Responsive.isDesktop(context) ? Responsive.isLargeMobile(context)? 20: 30 : 50, - fontWeight: FontWeight.bold - ),), - + fontSize: !Responsive.isDesktop(context) + ? Responsive.isLargeMobile(context) + ? 20 + : 30 + : 50, + fontWeight: FontWeight.bold), + ), + ShaderMask( + shaderCallback: (bounds) { + return const LinearGradient( + end: Alignment.centerRight, + begin: Alignment.centerLeft, + colors: [ + Colors.pink, + Colors.blue, + ]).createShader(bounds); + }, + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Colors.white, + fontSize: !Responsive.isDesktop(context) + ? Responsive.isLargeMobile(context) + ? 20 + : 30 + : 50, + fontWeight: FontWeight.bold), + ), + ) ], ); } -} \ No newline at end of file +} diff --git a/lib/view/projects/project_view.dart b/lib/view/projects/project_view.dart deleted file mode 100644 index 715eba5..0000000 --- a/lib/view/projects/project_view.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_portfolio/res/constants.dart'; -import 'package:flutter_portfolio/view%20model/getx_controllers/projects_controller.dart'; -import 'package:flutter_portfolio/view%20model/responsive.dart'; -import 'package:flutter_portfolio/view/projects/components/title_text.dart'; -import 'package:get/get.dart'; -import 'components/projects_grid.dart'; -class ProjectsView extends StatelessWidget { - ProjectsView({super.key}); - final controller = Get.put(ProjectController()); - @override - Widget build(BuildContext context) { - return Scaffold( - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if(Responsive.isLargeMobile(context))const SizedBox( - height: defaultPadding, - ), - const TitleText(prefix: 'Latest', title: 'Projects'), - const SizedBox( - height: defaultPadding, - ), - Expanded( - child: Responsive( - desktop: ProjectGrid(crossAxisCount: 3,), - extraLargeScreen: ProjectGrid(crossAxisCount: 4,), - largeMobile: ProjectGrid(crossAxisCount: 1,ratio: 1.8), - mobile: ProjectGrid(crossAxisCount: 1,ratio: 1.5), - tablet: ProjectGrid(ratio: 1.4,crossAxisCount: 2,))) - ], - ), - ); - } -} - - - - - - - diff --git a/lib/view/splash/splash_view.dart b/lib/view/splash/splash_view.dart index ad3a019..b39fd33 100644 --- a/lib/view/splash/splash_view.dart +++ b/lib/view/splash/splash_view.dart @@ -1,28 +1,25 @@ - import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_portfolio/res/constants.dart'; -import 'package:flutter_portfolio/view/home/home.dart'; -import 'package:flutter_portfolio/view/intro/components/animated_texts_componenets.dart'; +import 'package:flutter_portfolio/view/intro/components/animated_image_container.dart'; import 'package:flutter_portfolio/view/splash/componenets/animated_loading_text.dart'; +import 'package:flutter_portfolio/routing/routs.dart'; +import 'package:go_router/go_router.dart'; class SplashView extends StatefulWidget { const SplashView({super.key}); - @override State createState() => _SplashViewState(); } class _SplashViewState extends State { - - @override void initState() { - // TODO: implement initState super.initState(); Timer(const Duration(seconds: 3), () { - Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const HomePage(),)); + if (mounted) { + context.go(RoutePaths.main); + } }); } @@ -35,8 +32,13 @@ class _SplashViewState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - AnimatedImageContainer(width: 100,height: 100,), - SizedBox(height: defaultPadding,), + AnimatedImageContainer( + width: 100, + height: 100, + ), + SizedBox( + height: defaultPadding, + ), AnimatedLoadingText(), ], ), diff --git a/lib/widgets/custom_project_grid_image.dart b/lib/widgets/custom_project_grid_image.dart new file mode 100644 index 0000000..e48084f --- /dev/null +++ b/lib/widgets/custom_project_grid_image.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_portfolio/res/constants.dart'; + +class CustomProjectGridImage extends StatelessWidget { + final String imageUrl; + final double height; + final double width; + final BoxFit fit; + final BorderRadius? borderRadius; + + const CustomProjectGridImage({ + super.key, + required this.imageUrl, + required this.height, + required this.width, + this.fit = BoxFit.cover, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + final radius = borderRadius; + final image = CachedNetworkImage( + imageUrl: imageUrl, + height: height, + width: width, + fit: fit, + placeholder: (context, url) => SizedBox( + height: height, + width: width, + child: Container( + color: bgColor, + ), + ), + errorWidget: (context, url, error) => SizedBox( + height: height, + width: width, + child: Container( + color: bgColor, + child: const Center( + child: Icon( + Icons.broken_image, + color: Colors.white38, + ), + ), + ), + ), + ); + + if (radius != null) { + return ClipRRect(borderRadius: radius, child: image); + } + return image; + } +} diff --git a/lib/widgets/image_with_placeholder.dart b/lib/widgets/image_with_placeholder.dart new file mode 100644 index 0000000..df6039e --- /dev/null +++ b/lib/widgets/image_with_placeholder.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portfolio/res/constants.dart'; + +class ImageWithBackgroundPlaceholder extends StatelessWidget { + final String imagePath; + final BoxFit fit; + final BorderRadius? borderRadius; + + const ImageWithBackgroundPlaceholder({ + super.key, + required this.imagePath, + this.fit = BoxFit.cover, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + final radius = borderRadius; + final image = Image.asset( + imagePath, + fit: fit, + frameBuilder: (context, child, frame, sync) { + if (frame == null) { + return Container( + color: bgColor, + ); + } + + return child; + }, + errorBuilder: (context, error, stackTrace) => SizedBox( + child: Container( + color: bgColor, + child: const Center( + child: Icon( + Icons.broken_image, + color: Colors.white38, + ), + ), + ), + ), + ); + + if (radius != null) { + return ClipRRect(borderRadius: radius, child: image); + } + return image; + } +} diff --git a/lib/widgets/menu_button.dart b/lib/widgets/menu_button.dart new file mode 100644 index 0000000..b36eda6 --- /dev/null +++ b/lib/widgets/menu_button.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import '../../../res/constants.dart'; + +class MenuButton extends StatefulWidget { + final VoidCallback? onTap; + const MenuButton({super.key, this.onTap}); + + @override + State createState() => _MenuButtonState(); +} + +class _MenuButtonState extends State { + bool _isHovered = false; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: widget.onTap, + borderRadius: BorderRadius.circular(20), + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: defaultPadding * 2.0, + width: defaultPadding * 2.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black, + boxShadow: [ + BoxShadow( + color: Colors.pinkAccent.withOpacity(.5), + offset: const Offset(1, 1), + blurRadius: + _isHovered ? defaultPadding / 1.5 : defaultPadding / 3, + ), + BoxShadow( + color: Colors.blue.withOpacity(.5), + offset: const Offset(-1, -1), + blurRadius: + _isHovered ? defaultPadding / 2 : defaultPadding / 4, + ), + ]), + child: Container( + height: defaultPadding * 2.0, + width: defaultPadding * 2.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black, + boxShadow: [ + BoxShadow( + color: Colors.pinkAccent.withOpacity(.5), + offset: const Offset(1, 1), + // blurRadius: _isHovered ? defaultPadding : defaultPadding / 2, + ), + BoxShadow( + color: Colors.blue.withOpacity(.5), + offset: const Offset(-1, -1), + // blurRadius: _isHovered ? defaultPadding : defaultPadding / 2, + ), + ]), + child: Center( + child: ShaderMask( + shaderCallback: (bounds) { + return LinearGradient( + colors: [Colors.pink, Colors.blue.shade900]) + .createShader(bounds); + }, + child: const Icon( + Icons.menu, + color: Colors.white, + size: defaultPadding * 1.2, + ), + )), + ), + ), + ), + ); + } +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..59149af --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_portfolio") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.flutter_portfolio") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..3792af4 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..5d07423 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + gtk + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..4099b2b --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "flutter_portfolio"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "flutter_portfolio"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..6ecd6a9 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import app_links +import package_info_plus +import path_provider_foundation +import shared_preferences_foundation +import sqflite_darwin +import url_launcher_macos +import video_player_avfoundation +import wakelock_plus + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8f01e05 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* flutter_portfolio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_portfolio.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutter_portfolio.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutter_portfolio.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPortfolio.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_portfolio.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_portfolio"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPortfolio.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_portfolio.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_portfolio"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPortfolio.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_portfolio.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_portfolio"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..26aefd0 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..c52b27f --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_portfolio + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPortfolio + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/pubspec.lock b/pubspec.lock index acef808..2f6a198 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,14 +1,38 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + app_links: + dependency: transitive + description: + name: app_links + sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" + url: "https://pub.dev" + source: hosted + version: "3.5.1" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.7.0" async: dependency: transitive description: @@ -25,6 +49,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" + url: "https://pub.dev" + source: hosted + version: "1.2.0" characters: dependency: transitive description: @@ -33,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + chewie: + dependency: "direct main" + description: + name: chewie + sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" + url: "https://pub.dev" + source: hosted + version: "1.7.5" clock: dependency: transitive description: @@ -45,10 +101,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.19.0" crypto: dependency: transitive description: @@ -57,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -65,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" fake_async: dependency: transitive description: @@ -81,11 +153,43 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + url: "https://pub.dev" + source: hosted + version: "5.2.1" flutter_lints: dependency: "direct dev" description: @@ -94,6 +198,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + flutter_native_splash: + dependency: "direct main" + description: + name: flutter_native_splash + sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" + url: "https://pub.dev" + source: hosted + version: "2.4.4" + flutter_screenutil: + dependency: "direct main" + description: + name: flutter_screenutil + sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" + url: "https://pub.dev" + source: hosted + version: "5.9.3" flutter_svg: dependency: "direct main" description: @@ -112,6 +232,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" + url: "https://pub.dev" + source: hosted + version: "8.2.14" font_awesome_flutter: dependency: "direct main" description: @@ -120,14 +248,30 @@ packages: url: "https://pub.dev" source: hosted version: "10.5.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: a49876ebae32a50eb62483c5c5ac80ed0d8da34f98ccc23986b03a8d28cee07c + url: "https://pub.dev" + source: hosted + version: "2.4.1" get: dependency: "direct main" description: name: get - sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 url: "https://pub.dev" source: hosted - version: "4.6.5" + version: "4.7.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "8b1f37dfaf6e958c6b872322db06f946509433bec3de753c3491a42ae9ec2b48" + url: "https://pub.dev" + source: hosted + version: "16.1.0" google_fonts: dependency: "direct main" description: @@ -136,6 +280,30 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: d6362dff9a54f8c1c372bb137c858b4024c16407324d34e6473e59623c9b9f50 + url: "https://pub.dev" + source: hosted + version: "2.11.1" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + html: + dependency: transitive + description: + name: html + sha256: "9475be233c437f0e3637af55e7702cbbe5c23a68bd56e8a5fa2d426297b7c6c8" + url: "https://pub.dev" + source: hosted + version: "0.15.5+1" http: dependency: transitive description: @@ -152,14 +320,54 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" js: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + url: "https://pub.dev" + source: hosted + version: "10.0.7" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + url: "https://pub.dev" + source: hosted + version: "3.0.8" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -168,38 +376,86 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "4.2.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -260,10 +516,10 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" photo_view: dependency: "direct main" description: @@ -284,47 +540,231 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: b74dc0f57b5dca5ce9f57a54b08110bf41d6fc8a0483c0fec10c79e9aa0fb2bb url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.4.1" + provider: + dependency: transitive + description: + name: provider + sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: e3089dac2121917cc0c72d42ab056fea0abbaf3c2229048fc50e64bafc731adf + url: "https://pub.dev" + source: hosted + version: "2.4.2" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "3d4571b3c5eb58ce52a419d86e655493d0bc3020672da79f72fa0c16ca3a8ec1" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + url: "https://pub.dev" + source: hosted + version: "2.5.4+6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + url: "https://pub.dev" + source: hosted + version: "2.4.1+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: "9f9ed283943313b23a1b27139bb18986e9b152a6d34530232c702c468d98e91a" + url: "https://pub.dev" + source: hosted + version: "2.3.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + supabase: + dependency: transitive + description: + name: supabase + sha256: c3ebddba69ddcf16d8b78e8c44c4538b0193d1cf944fde3b72eb5b279892a370 + url: "https://pub.dev" + source: hosted + version: "2.6.3" + supabase_flutter: + dependency: "direct main" + description: + name: supabase_flutter + sha256: "3b5b5b492e342f63f301605d0c66f6528add285b5744f53c9fd9abd5ffdbce5b" + url: "https://pub.dev" + source: hosted + version: "2.8.4" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" + source: hosted + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -337,10 +777,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.7.3" typed_data: dependency: transitive description: @@ -349,70 +789,86 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.1.12" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.0.38" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.5.1" vector_graphics: dependency: transitive description: @@ -445,6 +901,94 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14" + url: "https://pub.dev" + source: hosted + version: "2.9.5" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "7018dbcb395e2bca0b9a898e73989e67c0c4a5db269528e1b036ca38bcca0d0b" + url: "https://pub.dev" + source: hosted + version: "2.7.17" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "8a4e73a3faf2b13512978a43cf1cdda66feeeb900a0527f1fbfd7b19cf3458d3" + url: "https://pub.dev" + source: hosted + version: "2.6.7" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a + url: "https://pub.dev" + source: hosted + version: "6.2.1" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "881b375a934d8ebf868c7fb1423b2bfaa393a0a265fa3f733079a86536064a10" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + url: "https://pub.dev" + source: hosted + version: "14.3.0" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: "26ebc8b5e0037c15e2a1b661dcec8a475cb7205befcce8a33f545ae8c86b367c" + url: "https://pub.dev" + source: hosted + version: "1.1.6" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "582f2f7aecc7376332d961a0dd1efa9378ce117657e0ade55d9ff72699a55e82" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b + url: "https://pub.dev" + source: hosted + version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" win32: dependency: transitive description: @@ -465,10 +1009,26 @@ packages: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: "56155e9e0002cc51ea7112857bbcdc714d4c35e176d43e4d3ee233009ff410c9" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "2.0.3" sdks: - dart: ">=3.0.2 <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5338c12..19aecdf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,41 +21,42 @@ version: 1.0.0+1 environment: sdk: '>=3.0.2 <4.0.0' -# Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + chewie: ^1.7.4 + cupertino_icons: ^1.0.2 flutter: sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - google_fonts: ^5.1.0 + flutter_dotenv: ^5.2.1 + flutter_screenutil: ^5.9.3 flutter_svg: ^2.0.7 - get: ^4.6.5 - photo_view: ^0.14.0 - url_launcher: ^6.1.12 font_awesome_flutter: ^10.5.0 + get: ^4.7.2 + google_fonts: ^5.1.0 + photo_view: ^0.14.0 + supabase_flutter: ^2.8.4 + url_launcher: ^6.3.1 + video_player: ^2.8.1 + fluttertoast: ^8.2.6 + flutter_native_splash: ^2.4.4 + go_router: ^16.1.0 + cached_network_image: ^3.3.1 dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec - # The following section is specific to Flutter packages. flutter: @@ -69,13 +70,10 @@ flutter: - assets/images/ - assets/icons/ # - images/a_dot_ham.jpeg - # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware - # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages - # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a diff --git a/showcase.png b/showcase.png new file mode 100644 index 0000000..300f2b4 Binary files /dev/null and b/showcase.png differ diff --git a/web/index.html b/web/index.html index 319575b..7644d08 100644 --- a/web/index.html +++ b/web/index.html @@ -1,6 +1,4 @@ - - - + - + - + - flutter_portfolio + Eslam Hossam + + - - + - - - + + + + \ No newline at end of file diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..9f33c36 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(flutter_portfolio LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_portfolio") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..785a046 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..8f8ee4f --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + app_links + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..5d0e8b0 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "flutter_portfolio" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "flutter_portfolio" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_portfolio.exe" "\0" + VALUE "ProductName", "flutter_portfolio" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..e5ceccd --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"flutter_portfolio", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_