From 68bfbbb61b2900486b01cedd5a8af3700a4b8108 Mon Sep 17 00:00:00 2001 From: DVisTaskOne Date: Mon, 22 Jul 2024 23:26:02 +0300 Subject: [PATCH 1/4] Initial commit --- lib/main.dart | 13 +- lib/src/pages/photo_view.dart | 190 +++++++++++++++++++++++++ lib/src/widgets/photo_view_appbar.dart | 24 ++++ 3 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 lib/src/pages/photo_view.dart create mode 100644 lib/src/widgets/photo_view_appbar.dart diff --git a/lib/main.dart b/lib/main.dart index a725658..3fba613 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:surf_flutter_summer_school_24/src/pages/photo_view.dart'; void main() { runApp(const MainApp()); @@ -9,12 +10,12 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - home: Scaffold( - body: Center( - child: Text('Hello World!'), - ), - ), + return MaterialApp( + initialRoute: '/view_photo', + routes: { + '/view_photo': (context) => PhotoView(), + }, ); } } + diff --git a/lib/src/pages/photo_view.dart b/lib/src/pages/photo_view.dart new file mode 100644 index 0000000..5cfa5d5 --- /dev/null +++ b/lib/src/pages/photo_view.dart @@ -0,0 +1,190 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:surf_flutter_summer_school_24/src/widgets/photo_view_appbar.dart'; + +List images = [ + 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', + 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', + 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', + 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', + 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album' +]; + +class PhotoView extends StatefulWidget { + const PhotoView({super.key}); + + @override + State createState() => _PhotoViewState(); +} + + +class _PhotoViewState extends State with TickerProviderStateMixin { + late PageController _pageViewController; + late TabController _tabController; + int _currentPageIndex = 0; + + @override + void initState() { + super.initState(); + _pageViewController = PageController(); + _tabController = TabController(length: images.length, vsync: this); + } + + @override + void dispose() { + super.dispose(); + _pageViewController.dispose(); + _tabController.dispose(); + } + + @override + Widget build(BuildContext context) { + final int total = images.length; + return Scaffold( + appBar: PhotoViewAppbar(total: total, current: _currentPageIndex + 1), + body: Stack( + alignment: Alignment.bottomCenter, + children: [ + // Wrap PageView in a Stack to overlay previous/next image previews + Stack( + alignment: Alignment.center, + children: [ + PageView.builder( + controller: _pageViewController, + onPageChanged: _handlePageViewChanged, + itemCount: images.length, + itemBuilder: (context, index) { + return Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.all(16.0), // Add padding here + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: ClipRRect( + borderRadius: BorderRadius.circular(23), + child: Image.network( + images[_currentPageIndex], + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + ), + ), + + ); + }, + ), + ], + ), + PageIndicator( + tabController: _tabController, + currentPageIndex: _currentPageIndex, + onUpdateCurrentPageIndex: _updateCurrentPageIndex, + isOnDesktopAndWeb: _isOnDesktopAndWeb, + ), + ], + ) + ); + } + + void _handlePageViewChanged(int currentPageIndex) { + if (!_isOnDesktopAndWeb) { + setState(() { + _currentPageIndex = currentPageIndex; + }); + } else { + _tabController.index = currentPageIndex; + } + } + + void _updateCurrentPageIndex(int index) { + _tabController.index = index; + _pageViewController.animateToPage( + index, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + } + + bool get _isOnDesktopAndWeb { + if (kIsWeb) { + return true; + } + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + return true; + case TargetPlatform.android: + case TargetPlatform.iOS: + case TargetPlatform.fuchsia: + return false; + } + } +} + +class PageIndicator extends StatelessWidget { + const PageIndicator({ + super.key, + required this.tabController, + required this.currentPageIndex, + required this.onUpdateCurrentPageIndex, + required this.isOnDesktopAndWeb, + }); + + final int currentPageIndex; + final TabController tabController; + final void Function(int) onUpdateCurrentPageIndex; + final bool isOnDesktopAndWeb; + + @override + Widget build(BuildContext context) { + if (!isOnDesktopAndWeb) { + return const SizedBox.shrink(); + } + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + splashRadius: 16.0, + padding: EdgeInsets.zero, + onPressed: () { + if (currentPageIndex == 0) { + return; + } + onUpdateCurrentPageIndex(currentPageIndex - 1); + }, + icon: const Icon( + Icons.arrow_left_rounded, + size: 32.0, + ), + ), + TabPageSelector( + controller: tabController, + color: colorScheme.surface, + selectedColor: colorScheme.primary, + ), + IconButton( + splashRadius: 16.0, + padding: EdgeInsets.zero, + onPressed: () { + if (currentPageIndex == 2) { + return; + } + onUpdateCurrentPageIndex(currentPageIndex + 1); + }, + icon: const Icon( + Icons.arrow_right_rounded, + size: 32.0, + ), + ), + ], + ), + ); + } +} + diff --git a/lib/src/widgets/photo_view_appbar.dart b/lib/src/widgets/photo_view_appbar.dart new file mode 100644 index 0000000..b25a9d6 --- /dev/null +++ b/lib/src/widgets/photo_view_appbar.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class PhotoViewAppbar extends StatelessWidget implements PreferredSizeWidget { + final int total; + final int current; + const PhotoViewAppbar({super.key, required this.total, required this.current}); + + @override + Widget build(BuildContext context) { + String ct = current.toString() + "/" + total.toString(); + return AppBar( + leading: IconButton( + onPressed: () {}, + icon: const Icon(Icons.arrow_back_rounded) + ), + actions: [ + Text(ct) + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} From 256002fff9dce2a2428a23a29188ccc6dda1b822 Mon Sep 17 00:00:00 2001 From: DVisTaskOne Date: Wed, 24 Jul 2024 10:06:18 +0300 Subject: [PATCH 2/4] Second commit --- lib/main.dart | 21 -- lib/src/assets/fonts/Magnolia.ttf | Bin 0 -> 165644 bytes .../feature/theme/data/theme_repository.dart | 22 ++ lib/src/feature/theme/di/theme_inherited.dart | 27 ++ .../theme/domain/theme_controller.dart | 30 ++ lib/src/feature/theme/ui/theme_builder.dart | 37 +++ lib/src/main.dart | 50 ++++ lib/src/pages/gallery.dart | 90 ++++++ lib/src/pages/photo_view.dart | 113 +------- lib/src/storage/theme/theme_storage.dart | 40 +++ lib/src/uikit/colors/color_palette.dart | 65 +++++ lib/src/uikit/colors/color_scheme.dart | 272 ++++++++++++++++++ lib/src/uikit/theme/theme_data.dart | 88 ++++++ lib/src/uikit/typography/typography.dart | 7 + lib/src/widgets/gallery_appbar.dart | 31 ++ lib/src/widgets/gallery_bottom_sheet.dart | 67 +++++ lib/src/widgets/photo_gallery_container.dart | 19 ++ lib/src/widgets/photo_view_appbar.dart | 15 +- lib/src/widgets/photo_view_container.dart | 29 ++ lib/src/widgets/shimmer_loading.dart | 47 +++ pubspec.lock | 135 ++++++++- pubspec.yaml | 10 + 22 files changed, 1089 insertions(+), 126 deletions(-) delete mode 100644 lib/main.dart create mode 100644 lib/src/assets/fonts/Magnolia.ttf create mode 100644 lib/src/feature/theme/data/theme_repository.dart create mode 100644 lib/src/feature/theme/di/theme_inherited.dart create mode 100644 lib/src/feature/theme/domain/theme_controller.dart create mode 100644 lib/src/feature/theme/ui/theme_builder.dart create mode 100644 lib/src/main.dart create mode 100644 lib/src/pages/gallery.dart create mode 100644 lib/src/storage/theme/theme_storage.dart create mode 100644 lib/src/uikit/colors/color_palette.dart create mode 100644 lib/src/uikit/colors/color_scheme.dart create mode 100644 lib/src/uikit/theme/theme_data.dart create mode 100644 lib/src/uikit/typography/typography.dart create mode 100644 lib/src/widgets/gallery_appbar.dart create mode 100644 lib/src/widgets/gallery_bottom_sheet.dart create mode 100644 lib/src/widgets/photo_gallery_container.dart create mode 100644 lib/src/widgets/photo_view_container.dart create mode 100644 lib/src/widgets/shimmer_loading.dart diff --git a/lib/main.dart b/lib/main.dart deleted file mode 100644 index 3fba613..0000000 --- a/lib/main.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:surf_flutter_summer_school_24/src/pages/photo_view.dart'; - -void main() { - runApp(const MainApp()); -} - -class MainApp extends StatelessWidget { - const MainApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - initialRoute: '/view_photo', - routes: { - '/view_photo': (context) => PhotoView(), - }, - ); - } -} - diff --git a/lib/src/assets/fonts/Magnolia.ttf b/lib/src/assets/fonts/Magnolia.ttf new file mode 100644 index 0000000000000000000000000000000000000000..76d5581a1b192e485307c1b4f14cff179700e781 GIT binary patch literal 165644 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 DU?+A3 literal 0 HcmV?d00001 diff --git a/lib/src/feature/theme/data/theme_repository.dart b/lib/src/feature/theme/data/theme_repository.dart new file mode 100644 index 0000000..f66096b --- /dev/null +++ b/lib/src/feature/theme/data/theme_repository.dart @@ -0,0 +1,22 @@ +import 'package:surf_flutter_summer_school_24/src/storage/theme/theme_storage.dart'; +import 'package:flutter/material.dart'; + +class ThemeRepository { + final ThemeStorage _themeStorage; + + ThemeRepository({ + required ThemeStorage themeStorage, + }) : _themeStorage = themeStorage; + + Future setThemeMode( + ThemeMode newThemeMode, + ) async { + await _themeStorage.saveThemeMode( + mode: newThemeMode, + ); + } + + ThemeMode? getThemeMode() { + return _themeStorage.getThemeMode(); + } +} \ No newline at end of file diff --git a/lib/src/feature/theme/di/theme_inherited.dart b/lib/src/feature/theme/di/theme_inherited.dart new file mode 100644 index 0000000..1bd3b65 --- /dev/null +++ b/lib/src/feature/theme/di/theme_inherited.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import '../domain/theme_controller.dart'; + +class ThemeInherited extends InheritedWidget { + const ThemeInherited({ + super.key, + required this.themeController, + required super.child, + }); + + final ThemeController themeController; + + static ThemeController? maybeOf(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType() + ?.themeController; + } + + static ThemeController of(BuildContext context) { + final ThemeController? result = maybeOf(context); + assert(result != null, 'No MyThemeInherited found in context'); + return result!; + } + + @override + bool updateShouldNotify(ThemeInherited oldWidget) => false; +} \ No newline at end of file diff --git a/lib/src/feature/theme/domain/theme_controller.dart b/lib/src/feature/theme/domain/theme_controller.dart new file mode 100644 index 0000000..91b15af --- /dev/null +++ b/lib/src/feature/theme/domain/theme_controller.dart @@ -0,0 +1,30 @@ +import 'package:surf_flutter_summer_school_24/src/feature/theme/data/theme_repository.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class ThemeController { + final ThemeRepository _themeRepository; + + ThemeController({ + required ThemeRepository themeRepository, + }) : _themeRepository = themeRepository; + + late final ValueNotifier _themeMode = ValueNotifier( + _themeRepository.getThemeMode() ?? ThemeMode.system, + ); + + ValueListenable get themeMode => _themeMode; + + Future setThemeMode(ThemeMode newThemeMode) async { + if (newThemeMode == _themeMode.value) return; + await _themeRepository.setThemeMode(newThemeMode); + _themeMode.value = newThemeMode; + } + + Future switchThemeMode() async { + final newThemeMode = + _themeMode.value != ThemeMode.light ? ThemeMode.light : ThemeMode.dark; + await _themeRepository.setThemeMode(newThemeMode); + _themeMode.value = newThemeMode; + } +} \ No newline at end of file diff --git a/lib/src/feature/theme/ui/theme_builder.dart b/lib/src/feature/theme/ui/theme_builder.dart new file mode 100644 index 0000000..4adf152 --- /dev/null +++ b/lib/src/feature/theme/ui/theme_builder.dart @@ -0,0 +1,37 @@ +import 'package:surf_flutter_summer_school_24/src/feature/theme/di/theme_inherited.dart'; +import 'package:flutter/material.dart'; + +typedef ThemeWidgetBuilder = Widget Function( + BuildContext context, + ThemeMode themeMode, + ); + +class ThemeBuilder extends StatefulWidget { + const ThemeBuilder({ + required this.builder, + super.key, + }); + + final ThemeWidgetBuilder builder; + + @override + State createState() => _ThemeBuilderState(); +} + +class _ThemeBuilderState extends State { + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: ThemeInherited.of(context).themeMode, + builder: ( + builderContext, + themeMode, + _, + ) => + widget.builder( + builderContext, + themeMode, + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/main.dart b/lib/src/main.dart new file mode 100644 index 0000000..3fa117a --- /dev/null +++ b/lib/src/main.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:surf_flutter_summer_school_24/src/feature/theme/data/theme_repository.dart'; +import 'package:surf_flutter_summer_school_24/src/feature/theme/di/theme_inherited.dart'; +import 'package:surf_flutter_summer_school_24/src/feature/theme/domain/theme_controller.dart'; +import 'package:surf_flutter_summer_school_24/src/feature/theme/ui/theme_builder.dart'; +import 'package:surf_flutter_summer_school_24/src/pages/gallery.dart'; +import 'package:surf_flutter_summer_school_24/src/storage/theme/theme_storage.dart'; +import 'package:surf_flutter_summer_school_24/src/uikit/theme/theme_data.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + final prefs = await SharedPreferences.getInstance(); + final themeStorage = ThemeStorage( + prefs: prefs, + ); + final themeRepository = ThemeRepository( + themeStorage: themeStorage, + ); + final themeController = ThemeController( + themeRepository: themeRepository, + ); + runApp(MainApp( + themeController: themeController, + )); +} + +class MainApp extends StatelessWidget { + final ThemeController themeController; + + const MainApp({super.key, required this.themeController,}); + + @override + Widget build(BuildContext context) { + return ThemeInherited( + themeController: themeController, + child: ThemeBuilder( + builder: (_, themeMode) { + return MaterialApp( + theme: AppThemeData.lightTheme, + darkTheme: AppThemeData.darkTheme, + themeMode: themeMode, + home: Gallery() + ); + }, + ) + ); + } +} + diff --git a/lib/src/pages/gallery.dart b/lib/src/pages/gallery.dart new file mode 100644 index 0000000..81ba5c4 --- /dev/null +++ b/lib/src/pages/gallery.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_summer_school_24/src/pages/photo_view.dart'; +import 'package:surf_flutter_summer_school_24/src/widgets/gallery_appbar.dart'; +import 'package:surf_flutter_summer_school_24/src/widgets/photo_gallery_container.dart'; + +List images = [ + 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', + 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', + 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', + 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', + 'https://sun9-55.userapi.com/impg/mDIc6RPZmIznA9Uip0tKXeep7gQg5ot0XUUaVw/OyDLqBzPL0A.jpg?size=1024x1280&quality=96&sign=a38727bf7d11a4b1811446972e048f9e&type=album', + 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', + 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', + 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', + 'https://sun9-38.userapi.com/impg/mtqQmWCnxwMDRjqVGEiczyfZ0CqSu7G7e1BpMg/JOfRS5xYG_w.jpg?size=884x884&quality=96&sign=d972f22e5d4b8d82c82323b81655e86e&type=album', + 'https://sun9-25.userapi.com/impg/U1xodQh60uHV1H75doPQtI9lHR-ps4y7pQ7KTQ/vsRSDAx2Ma8.jpg?size=639x641&quality=96&sign=975c4c8ee9bd5e4c3976f211ef239c98&type=album', + 'https://sun9-24.userapi.com/impg/UVP9-Kst1oeGJvfFz6cOgCFRgMacoQrqYeqdZA/s4SRxgafGxk.jpg?size=637x634&quality=96&sign=80155bf77dc4385ccf467379490b1da4&type=album', + 'https://sun9-30.userapi.com/impg/MO4GiY_8B_f3ep-8NusBQWAZOy-QJVTHWyZ2Cw/EJRz0qfPmJE.jpg?size=564x564&quality=96&sign=ef87156b2be6159ab306e12152df3d29&type=album', + 'https://sun9-40.userapi.com/impf/c846017/v846017039/c4d2a/_QCZsc09OYE.jpg?size=540x530&quality=96&sign=733f462520a192a9bbf5b716f00c2a2f&type=album', + 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', + 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', + 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', + 'https://sun9-55.userapi.com/impg/mDIc6RPZmIznA9Uip0tKXeep7gQg5ot0XUUaVw/OyDLqBzPL0A.jpg?size=1024x1280&quality=96&sign=a38727bf7d11a4b1811446972e048f9e&type=album', + 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', + 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', + 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', + 'https://sun9-38.userapi.com/impg/mtqQmWCnxwMDRjqVGEiczyfZ0CqSu7G7e1BpMg/JOfRS5xYG_w.jpg?size=884x884&quality=96&sign=d972f22e5d4b8d82c82323b81655e86e&type=album', + 'https://sun9-25.userapi.com/impg/U1xodQh60uHV1H75doPQtI9lHR-ps4y7pQ7KTQ/vsRSDAx2Ma8.jpg?size=639x641&quality=96&sign=975c4c8ee9bd5e4c3976f211ef239c98&type=album', + 'https://sun9-24.userapi.com/impg/UVP9-Kst1oeGJvfFz6cOgCFRgMacoQrqYeqdZA/s4SRxgafGxk.jpg?size=637x634&quality=96&sign=80155bf77dc4385ccf467379490b1da4&type=album', + 'https://sun9-30.userapi.com/impg/MO4GiY_8B_f3ep-8NusBQWAZOy-QJVTHWyZ2Cw/EJRz0qfPmJE.jpg?size=564x564&quality=96&sign=ef87156b2be6159ab306e12152df3d29&type=album', + 'https://sun9-40.userapi.com/impf/c846017/v846017039/c4d2a/_QCZsc09OYE.jpg?size=540x530&quality=96&sign=733f462520a192a9bbf5b716f00c2a2f&type=album', +]; + +class Gallery extends StatefulWidget { + const Gallery({super.key}); + + @override + State createState() => _GalleryState(); +} + +class _GalleryState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: GalleryAppbar(), + body: GridView.builder( + primary: false, + padding: const EdgeInsets.all(20), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 3, + mainAxisSpacing: 5, + ), + itemCount: images.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PhotoView(images: images, initialIndex: index), + ), + ); + }, + child: FutureBuilder( + future: loadImage(images[index]), + builder: (context, snapshot) { + if ((snapshot.connectionState == ConnectionState.waiting)) { + return PhotoGalleryContainer( + urlImage: images[index], + isLoading: true, + ); + } else { + return PhotoGalleryContainer( + urlImage: images[index], + isLoading: false, + ); + } + }, + ), + ); + }, + )); + } + + Future loadImage(String urlImage) async { + await Future.delayed(Duration(seconds: 1)); + return; + } +} diff --git a/lib/src/pages/photo_view.dart b/lib/src/pages/photo_view.dart index 5cfa5d5..7993383 100644 --- a/lib/src/pages/photo_view.dart +++ b/lib/src/pages/photo_view.dart @@ -1,17 +1,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:surf_flutter_summer_school_24/src/widgets/photo_view_appbar.dart'; - -List images = [ - 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', - 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', - 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', - 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', - 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album' -]; +import 'package:surf_flutter_summer_school_24/src/widgets/photo_view_container.dart'; class PhotoView extends StatefulWidget { - const PhotoView({super.key}); + final List images; + final int initialIndex; + + const PhotoView({super.key, required this.images, required this.initialIndex}); @override State createState() => _PhotoViewState(); @@ -26,8 +22,9 @@ class _PhotoViewState extends State with TickerProviderStateMixin { @override void initState() { super.initState(); - _pageViewController = PageController(); - _tabController = TabController(length: images.length, vsync: this); + _currentPageIndex = widget.initialIndex; + _pageViewController = PageController(initialPage: widget.initialIndex, viewportFraction: 0.8); + _tabController = TabController(length: widget.images.length, vsync: this, initialIndex: widget.initialIndex); } @override @@ -39,49 +36,25 @@ class _PhotoViewState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final int total = images.length; + final int total = widget.images.length; return Scaffold( appBar: PhotoViewAppbar(total: total, current: _currentPageIndex + 1), body: Stack( alignment: Alignment.bottomCenter, children: [ - // Wrap PageView in a Stack to overlay previous/next image previews Stack( alignment: Alignment.center, children: [ PageView.builder( controller: _pageViewController, onPageChanged: _handlePageViewChanged, - itemCount: images.length, + itemCount: widget.images.length, itemBuilder: (context, index) { - return Align( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.all(16.0), // Add padding here - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: ClipRRect( - borderRadius: BorderRadius.circular(23), - child: Image.network( - images[_currentPageIndex], - fit: BoxFit.cover, - alignment: Alignment.center, - ), - ), - ), - ), - - ); + return PhotoViewContainer(urlImage: widget.images[index]); }, ), ], ), - PageIndicator( - tabController: _tabController, - currentPageIndex: _currentPageIndex, - onUpdateCurrentPageIndex: _updateCurrentPageIndex, - isOnDesktopAndWeb: _isOnDesktopAndWeb, - ), ], ) ); @@ -123,68 +96,4 @@ class _PhotoViewState extends State with TickerProviderStateMixin { } } -class PageIndicator extends StatelessWidget { - const PageIndicator({ - super.key, - required this.tabController, - required this.currentPageIndex, - required this.onUpdateCurrentPageIndex, - required this.isOnDesktopAndWeb, - }); - - final int currentPageIndex; - final TabController tabController; - final void Function(int) onUpdateCurrentPageIndex; - final bool isOnDesktopAndWeb; - - @override - Widget build(BuildContext context) { - if (!isOnDesktopAndWeb) { - return const SizedBox.shrink(); - } - final ColorScheme colorScheme = Theme.of(context).colorScheme; - - return Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - splashRadius: 16.0, - padding: EdgeInsets.zero, - onPressed: () { - if (currentPageIndex == 0) { - return; - } - onUpdateCurrentPageIndex(currentPageIndex - 1); - }, - icon: const Icon( - Icons.arrow_left_rounded, - size: 32.0, - ), - ), - TabPageSelector( - controller: tabController, - color: colorScheme.surface, - selectedColor: colorScheme.primary, - ), - IconButton( - splashRadius: 16.0, - padding: EdgeInsets.zero, - onPressed: () { - if (currentPageIndex == 2) { - return; - } - onUpdateCurrentPageIndex(currentPageIndex + 1); - }, - icon: const Icon( - Icons.arrow_right_rounded, - size: 32.0, - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/storage/theme/theme_storage.dart b/lib/src/storage/theme/theme_storage.dart new file mode 100644 index 0000000..23a101a --- /dev/null +++ b/lib/src/storage/theme/theme_storage.dart @@ -0,0 +1,40 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ThemeStorage { + final SharedPreferences _prefs; + + ThemeStorage({ + required SharedPreferences prefs, + }) : _prefs = prefs; + + ThemeMode? getThemeMode() { + final storedName = _prefs.getString( + ThemeStorageKeys.mode.key, + ); + + if (storedName?.isEmpty ?? true) return null; + + return ThemeMode.values.firstWhereOrNull( + (themeMode) => themeMode.name == storedName, + ); + } + + Future saveThemeMode({ + required ThemeMode mode, + }) { + return _prefs.setString( + ThemeStorageKeys.mode.key, + mode.name, + ); + } +} + +enum ThemeStorageKeys { + mode('theme_mode'); + + final String key; + + const ThemeStorageKeys(this.key); +} \ No newline at end of file diff --git a/lib/src/uikit/colors/color_palette.dart b/lib/src/uikit/colors/color_palette.dart new file mode 100644 index 0000000..d372f9f --- /dev/null +++ b/lib/src/uikit/colors/color_palette.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +abstract class LightColorPalette { + static const purple = Color(0xFF9824F2); + + static const greenYellow = Color(0xFFBEFF3D); + + static const darkScarlet = Color(0xFF4D052A); + + static const folly = Color(0xFFFF004D); + + static const cultured = Color(0xFFF6F6F6); + + static const chineseBlack = Color(0xFF171717); + + static const white = Colors.white; + + static const black = Colors.black; + + static const vividRaspberry = Color(0xFFFF176B); + + static const lightSilver = Color(0xFFD9D9D9); + + static const lightGreen = Color(0xFFB5CCAE); + + static const darkGreen = Color(0xFF84A58F); + + static const lightViolet = Color(0xFF74305B); + + static const violet = Color(0xFF4A194E); + + static const appleGreen = Color(0xFF83C000); + + static const platinum = Color(0xFFE7E4E0); +} + +abstract class DarkColorPalette { + static const hanPurple = Color(0xFF6D38FF); + + static const inchworm = Color(0xFFC6FF57); + + static const maroon = Color(0xFF7B0008); + + static const brinkPink = Color(0xFFFF607D); + + static const raisinBlack = Color(0xFF222222); + + static const lightSilver = Color(0xFFD6D6D6); + + static const cyclamen = Color(0xFFFF79A8); + + static const etonBlue = Color(0xFF9CD29C); + + static const russianGreen = Color(0xFF628B6E); + + static const plum = Color(0xFF9E478B); + + static const brownChocolate = Color(0xFF561E43); + + static const vividLimeGreen = Color(0xFF9ECF00); + + static const white = Colors.white; + + static const black = Colors.black; +} \ No newline at end of file diff --git a/lib/src/uikit/colors/color_scheme.dart b/lib/src/uikit/colors/color_scheme.dart new file mode 100644 index 0000000..a528938 --- /dev/null +++ b/lib/src/uikit/colors/color_scheme.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_summer_school_24/src/uikit/colors/color_palette.dart'; + +const _skeletonOpacity = 0.06; + +@immutable +class AppColorScheme extends ThemeExtension { + final Color primary; + + final Color onPrimary; + + final Color secondary; + + final Color onSecondary; + + final Color surface; + + final Color surfaceSecondary; + + final Color onSurface; + + final Color background; + + final Color backgroundSecondary; + + final Color backgroundTertiary; + + final Color tetradicBackground; + + final Color onBackground; + + final Color onBackgroundSecondary; + + final Color danger; + + final Color dangerSecondary; + + final Color onDanger; + + final Color textField; + + final Color textFieldLabel; + + final Color textFieldHelper; + + final Color frameTextFieldSecondary; + + final Color inactive; + + final Color positive; + + final Color onPositive; + + final Color skeletonPrimary; + + final Color skeletonOnPrimary; + + final Color skeletonSecondary; + + final Color skeletonTertiary; + + final Color shimmer; + + AppColorScheme.light() + : primary = LightColorPalette.black, + onPrimary = LightColorPalette.white, + secondary = LightColorPalette.greenYellow, + onSecondary = LightColorPalette.chineseBlack, + surface = LightColorPalette.white, + surfaceSecondary = LightColorPalette.cultured, + onSurface = LightColorPalette.chineseBlack, + background = LightColorPalette.cultured, + backgroundSecondary = LightColorPalette.darkScarlet, + backgroundTertiary = LightColorPalette.cultured, + onBackground = LightColorPalette.chineseBlack, + onBackgroundSecondary = LightColorPalette.white, + danger = LightColorPalette.folly, + dangerSecondary = LightColorPalette.vividRaspberry, + onDanger = LightColorPalette.white, + textField = LightColorPalette.chineseBlack, + textFieldLabel = LightColorPalette.black, + textFieldHelper = LightColorPalette.black, + frameTextFieldSecondary = LightColorPalette.chineseBlack, + inactive = LightColorPalette.black, + positive = LightColorPalette.greenYellow, + onPositive = LightColorPalette.chineseBlack, + skeletonPrimary = LightColorPalette.black.withOpacity(_skeletonOpacity), + skeletonOnPrimary = LightColorPalette.white, + skeletonSecondary = LightColorPalette.cultured, + skeletonTertiary = LightColorPalette.lightSilver, + tetradicBackground = LightColorPalette.lightGreen, + shimmer = LightColorPalette.platinum; + + AppColorScheme.dark() + : primary = DarkColorPalette.white, + onPrimary = DarkColorPalette.black, + secondary = DarkColorPalette.inchworm, + onSecondary = DarkColorPalette.black, + surface = DarkColorPalette.raisinBlack, + surfaceSecondary = DarkColorPalette.raisinBlack, + onSurface = DarkColorPalette.white, + background = DarkColorPalette.raisinBlack, + backgroundSecondary = DarkColorPalette.maroon, + backgroundTertiary = DarkColorPalette.raisinBlack, + onBackground = DarkColorPalette.white, + onBackgroundSecondary = DarkColorPalette.white, + danger = DarkColorPalette.brinkPink, + dangerSecondary = DarkColorPalette.cyclamen, + onDanger = DarkColorPalette.white, + textField = DarkColorPalette.lightSilver, + textFieldLabel = DarkColorPalette.white, + textFieldHelper = DarkColorPalette.black, + frameTextFieldSecondary = DarkColorPalette.lightSilver, + inactive = DarkColorPalette.black, + positive = DarkColorPalette.inchworm, + onPositive = DarkColorPalette.black, + skeletonPrimary = DarkColorPalette.black.withOpacity(_skeletonOpacity), + skeletonOnPrimary = DarkColorPalette.white, + skeletonSecondary = DarkColorPalette.raisinBlack, + skeletonTertiary = DarkColorPalette.lightSilver, + tetradicBackground = DarkColorPalette.etonBlue, + shimmer = LightColorPalette.platinum; + + const AppColorScheme._({ + required this.primary, + required this.onPrimary, + required this.secondary, + required this.onSecondary, + required this.surface, + required this.surfaceSecondary, + required this.onSurface, + required this.background, + required this.backgroundSecondary, + required this.backgroundTertiary, + required this.onBackground, + required this.onBackgroundSecondary, + required this.danger, + required this.dangerSecondary, + required this.onDanger, + required this.textField, + required this.textFieldLabel, + required this.textFieldHelper, + required this.frameTextFieldSecondary, + required this.inactive, + required this.positive, + required this.onPositive, + required this.skeletonPrimary, + required this.skeletonOnPrimary, + required this.skeletonSecondary, + required this.skeletonTertiary, + required this.tetradicBackground, + required this.shimmer, + }); + + @override + ThemeExtension copyWith({ + Color? primary, + Color? onPrimary, + Color? secondary, + Color? onSecondary, + Color? surface, + Color? surfaceSecondary, + Color? onSurface, + Color? background, + Color? backgroundSecondary, + Color? backgroundTertiary, + Color? onBackground, + Color? onBackgroundSecondary, + Color? danger, + Color? dangerSecondary, + Color? onDanger, + Color? textField, + Color? textFieldLabel, + Color? textFieldHelper, + Color? frameTextFieldSecondary, + Color? inactive, + Color? positive, + Color? onPositive, + Color? skeletonPrimary, + Color? skeletonOnPrimary, + Color? skeletonSecondary, + Color? skeletonTertiary, + Color? tetradicBackground, + Color? shimmer, + }) { + return AppColorScheme._( + primary: primary ?? this.primary, + onPrimary: onPrimary ?? this.onPrimary, + secondary: secondary ?? this.secondary, + onSecondary: onSecondary ?? this.onSecondary, + surface: surface ?? this.surface, + surfaceSecondary: surfaceSecondary ?? this.surfaceSecondary, + onSurface: onSurface ?? this.onSurface, + background: background ?? this.background, + backgroundSecondary: backgroundSecondary ?? this.backgroundSecondary, + backgroundTertiary: backgroundTertiary ?? this.backgroundTertiary, + onBackground: onBackground ?? this.onBackground, + onBackgroundSecondary: + onBackgroundSecondary ?? this.onBackgroundSecondary, + danger: danger ?? this.danger, + dangerSecondary: dangerSecondary ?? this.dangerSecondary, + onDanger: onDanger ?? this.onDanger, + textField: textField ?? this.textField, + textFieldLabel: textFieldLabel ?? this.textFieldLabel, + textFieldHelper: textFieldHelper ?? this.textFieldHelper, + frameTextFieldSecondary: + frameTextFieldSecondary ?? this.frameTextFieldSecondary, + inactive: inactive ?? this.inactive, + positive: positive ?? this.positive, + onPositive: onPositive ?? this.onPositive, + skeletonPrimary: skeletonPrimary ?? this.skeletonPrimary, + skeletonOnPrimary: skeletonOnPrimary ?? this.skeletonOnPrimary, + skeletonSecondary: skeletonSecondary ?? this.skeletonSecondary, + skeletonTertiary: skeletonTertiary ?? this.skeletonTertiary, + tetradicBackground: tetradicBackground ?? this.tetradicBackground, + shimmer: shimmer ?? this.shimmer, + ); + } + + @override + ThemeExtension lerp( + ThemeExtension? other, + double t, + ) { + if (other is! AppColorScheme) { + return this; + } + + return AppColorScheme._( + primary: Color.lerp(primary, other.primary, t)!, + onPrimary: Color.lerp(onPrimary, other.onPrimary, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + onSecondary: Color.lerp(onSecondary, other.onSecondary, t)!, + surface: Color.lerp(surface, other.surface, t)!, + surfaceSecondary: + Color.lerp(surfaceSecondary, other.surfaceSecondary, t)!, + onSurface: Color.lerp(onSurface, other.onSurface, t)!, + background: Color.lerp(background, other.background, t)!, + backgroundSecondary: + Color.lerp(backgroundSecondary, other.backgroundSecondary, t)!, + backgroundTertiary: + Color.lerp(backgroundTertiary, other.backgroundTertiary, t)!, + onBackground: Color.lerp(onBackground, other.onBackground, t)!, + onBackgroundSecondary: + Color.lerp(onBackgroundSecondary, other.onBackgroundSecondary, t)!, + danger: Color.lerp(danger, other.danger, t)!, + dangerSecondary: Color.lerp(dangerSecondary, other.dangerSecondary, t)!, + onDanger: Color.lerp(onDanger, other.onDanger, t)!, + textField: Color.lerp(textField, other.textField, t)!, + textFieldLabel: Color.lerp(textFieldLabel, other.textFieldLabel, t)!, + textFieldHelper: Color.lerp(textFieldHelper, other.textFieldHelper, t)!, + frameTextFieldSecondary: Color.lerp( + frameTextFieldSecondary, other.frameTextFieldSecondary, t)!, + inactive: Color.lerp(inactive, other.inactive, t)!, + positive: Color.lerp(positive, other.positive, t)!, + onPositive: Color.lerp(onPositive, other.onPositive, t)!, + skeletonPrimary: Color.lerp(skeletonPrimary, other.skeletonPrimary, t)!, + skeletonOnPrimary: + Color.lerp(skeletonOnPrimary, other.skeletonOnPrimary, t)!, + skeletonSecondary: + Color.lerp(skeletonSecondary, other.skeletonSecondary, t)!, + skeletonTertiary: + Color.lerp(skeletonTertiary, other.skeletonTertiary, t)!, + tetradicBackground: + Color.lerp(tetradicBackground, other.tetradicBackground, t)!, + shimmer: Color.lerp(shimmer, other.shimmer, t)!, + ); + } + + static AppColorScheme of(BuildContext context) => + Theme.of(context).extension()!; +} \ No newline at end of file diff --git a/lib/src/uikit/theme/theme_data.dart b/lib/src/uikit/theme/theme_data.dart new file mode 100644 index 0000000..51fbabc --- /dev/null +++ b/lib/src/uikit/theme/theme_data.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_summer_school_24/src/uikit/colors/color_scheme.dart'; +import 'package:surf_flutter_summer_school_24/src/uikit/typography/typography.dart'; + +abstract class AppThemeData { + static final lightTheme = ThemeData( + extensions: [_lightColorScheme], + brightness: Brightness.light, + textTheme: const TextTheme( + titleLarge: headline1, + ), + colorScheme: ColorScheme( + brightness: Brightness.light, + primary: _lightColorScheme.primary, + onPrimary: _lightColorScheme.onPrimary, + secondary: _lightColorScheme.secondary, + onSecondary: _lightColorScheme.onSecondary, + error: _lightColorScheme.danger, + onError: _lightColorScheme.onDanger, + background: _lightColorScheme.background, + onBackground: _lightColorScheme.onBackground, + surface: _lightColorScheme.surface, + onSurface: _lightColorScheme.onSurface, + ), + scaffoldBackgroundColor: _lightColorScheme.background, + appBarTheme: AppBarTheme( + color: _lightColorScheme.background, + iconTheme: IconThemeData( + color: _lightColorScheme.primary, + ), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: _lightColorScheme.background, + selectedItemColor: _lightColorScheme.primary, + unselectedItemColor: _lightColorScheme.onBackground, + ), + snackBarTheme: SnackBarThemeData( + backgroundColor: _lightColorScheme.primary, + contentTextStyle: TextStyle( + color: _lightColorScheme.onPrimary, + ), + ), + ); + + static final darkTheme = ThemeData( + extensions: [_darkColorScheme], + brightness: Brightness.dark, + textTheme: const TextTheme( + titleLarge: headline1, + ), + colorScheme: ColorScheme( + brightness: Brightness.dark, + primary: _darkColorScheme.primary, + onPrimary: _darkColorScheme.onPrimary, + secondary: _darkColorScheme.secondary, + onSecondary: _darkColorScheme.onSecondary, + error: _darkColorScheme.danger, + onError: _darkColorScheme.onDanger, + background: _darkColorScheme.background, + onBackground: _darkColorScheme.onBackground, + surface: _darkColorScheme.surface, + onSurface: _darkColorScheme.onSurface, + ), + scaffoldBackgroundColor: _darkColorScheme.background, + appBarTheme: AppBarTheme( + color: _darkColorScheme.background, + iconTheme: IconThemeData( + color: _darkColorScheme.primary, + ), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: _darkColorScheme.background, + selectedItemColor: _darkColorScheme.primary, + unselectedItemColor: _darkColorScheme.onBackground, + ), + snackBarTheme: SnackBarThemeData( + backgroundColor: _darkColorScheme.primary, + contentTextStyle: TextStyle( + color: _darkColorScheme.onPrimary, + ), + ), + ); + + static final _lightColorScheme = AppColorScheme.light(); + static final _darkColorScheme = AppColorScheme.dark(); + + +} \ No newline at end of file diff --git a/lib/src/uikit/typography/typography.dart b/lib/src/uikit/typography/typography.dart new file mode 100644 index 0000000..833d3b7 --- /dev/null +++ b/lib/src/uikit/typography/typography.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +const TextStyle headline1 = TextStyle( + fontFamily: 'Magnolia', + fontSize: 32, + fontWeight: FontWeight.normal, +); diff --git a/lib/src/widgets/gallery_appbar.dart b/lib/src/widgets/gallery_appbar.dart new file mode 100644 index 0000000..c3db484 --- /dev/null +++ b/lib/src/widgets/gallery_appbar.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_summer_school_24/src/widgets/gallery_bottom_sheet.dart'; + +class GalleryAppbar extends StatelessWidget implements PreferredSizeWidget { + const GalleryAppbar({super.key}); + + @override + Widget build(BuildContext context) { + String title = "Постограм"; + return AppBar( + centerTitle: true, + title: Text(title), + actions: [ + IconButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return const GalleryBottomSheet(); + } + ); + }, + icon: const Icon(Icons.more_vert) + ) + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} \ No newline at end of file diff --git a/lib/src/widgets/gallery_bottom_sheet.dart b/lib/src/widgets/gallery_bottom_sheet.dart new file mode 100644 index 0000000..e4f2239 --- /dev/null +++ b/lib/src/widgets/gallery_bottom_sheet.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import '../feature/theme/di/theme_inherited.dart'; + +class GalleryBottomSheet extends StatelessWidget { + const GalleryBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 160, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Row( + children: [ + Icon( + ThemeInherited.of(context).themeMode.value == ThemeMode.light + ? Icons.wb_sunny_outlined + : Icons.brightness_2_outlined, + ), + const SizedBox(width: 10), + const Text('Тема'), + ], + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only(right: 25.0), + child: Text( + ThemeInherited.of(context).themeMode.value == ThemeMode.light + ? 'Светлая' + : 'Темная', + ), + ), + ], + ), + onPressed: () { + ThemeInherited.of(context).switchThemeMode(); + }, + ), + ElevatedButton( + child: const Padding( + padding: EdgeInsets.only(left: 25.0), + child: Row( + children: [ + Icon(Icons.cloud_upload_outlined), + SizedBox(width: 10), + Text('Загрузить фото...'), + ], + ), + ), + onPressed: () {}, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/widgets/photo_gallery_container.dart b/lib/src/widgets/photo_gallery_container.dart new file mode 100644 index 0000000..b367862 --- /dev/null +++ b/lib/src/widgets/photo_gallery_container.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_summer_school_24/src/widgets/shimmer_loading.dart'; + +class PhotoGalleryContainer extends StatelessWidget { + final String urlImage; + final bool isLoading; + + const PhotoGalleryContainer({super.key, required this.urlImage, required this.isLoading}); + + @override + Widget build(BuildContext context) { + return ShimmerLoading( + isLoading: isLoading, + child: Container( + child: Image.network(urlImage, fit: BoxFit.cover), + ), + ); + } +} diff --git a/lib/src/widgets/photo_view_appbar.dart b/lib/src/widgets/photo_view_appbar.dart index b25a9d6..e524e92 100644 --- a/lib/src/widgets/photo_view_appbar.dart +++ b/lib/src/widgets/photo_view_appbar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:surf_flutter_summer_school_24/src/pages/gallery.dart'; class PhotoViewAppbar extends StatelessWidget implements PreferredSizeWidget { final int total; @@ -10,11 +11,21 @@ class PhotoViewAppbar extends StatelessWidget implements PreferredSizeWidget { String ct = current.toString() + "/" + total.toString(); return AppBar( leading: IconButton( - onPressed: () {}, + onPressed: () { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => Gallery(), + ), (Route route) => false, + ); + }, icon: const Icon(Icons.arrow_back_rounded) ), actions: [ - Text(ct) + Padding( + padding: EdgeInsets.fromLTRB(0, 0, 10, 0), + child: Text(ct), + ) ], ); } diff --git a/lib/src/widgets/photo_view_container.dart b/lib/src/widgets/photo_view_container.dart new file mode 100644 index 0000000..f7aeb3d --- /dev/null +++ b/lib/src/widgets/photo_view_container.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class PhotoViewContainer extends StatelessWidget { + final String urlImage; + + const PhotoViewContainer({super.key, required this.urlImage}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: ClipRRect( + borderRadius: BorderRadius.circular(23), + child: Image.network( + urlImage, + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + ), + ), + ); + } +} + diff --git a/lib/src/widgets/shimmer_loading.dart b/lib/src/widgets/shimmer_loading.dart new file mode 100644 index 0000000..07e7855 --- /dev/null +++ b/lib/src/widgets/shimmer_loading.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +const _shimmerGradient = LinearGradient( + colors: [ + Color(0xFFEBEBF4), + Color(0xFFF4F4F4), + ], + stops: [ + 0.0, + 1.0, + ], + begin: Alignment(0.0, 0.0), + end: Alignment(1.0, 0.0), + tileMode: TileMode.clamp, +); + +class ShimmerLoading extends StatefulWidget { + const ShimmerLoading({ + super.key, + required this.isLoading, + required this.child, + }); + + final bool isLoading; + final Widget child; + + @override + State createState() => _ShimmerLoadingState(); +} + +class _ShimmerLoadingState extends State { + @override + Widget build(BuildContext context) { + if (!widget.isLoading) { + return widget.child; + } + + return ShaderMask( + blendMode: BlendMode.srcATop, + shaderCallback: (bounds) { + return _shimmerGradient.createShader(bounds); + }, + child: widget.child, + ); + } +} + diff --git a/pubspec.lock b/pubspec.lock index 9883fc6..c081e39 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" flutter: dependency: "direct main" description: flutter @@ -67,6 +83,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: @@ -131,6 +152,102 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + shared_preferences: + dependency: "direct main" + 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: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + 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: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + 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 @@ -200,6 +317,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.1" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" sdks: dart: ">=3.4.3 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 13e222f..670c5d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: flutter: sdk: flutter + shared_preferences: ^2.2.3 dev_dependencies: flutter_test: @@ -17,3 +18,12 @@ dev_dependencies: flutter: uses-material-design: true + + assets: + + fonts: + - family: Magnolia + fonts: + - asset: lib/src/assets/fonts/Magnolia.ttf + + From 0fac613bc6e0bf28ab27286ad1e32ffb360a8367 Mon Sep 17 00:00:00 2001 From: DVisTaskOne Date: Wed, 24 Jul 2024 22:58:04 +0300 Subject: [PATCH 3/4] ... + Equatable + Elementary --- README.md | 10 ++ {lib/src/assets/fonts => fonts}/Magnolia.ttf | Bin .../photos/data/i_photo_repository.dart | 5 + .../photos/data/mock_photo_repository.dart | 130 ++++++++++++++++++ lib/src/main.dart | 4 +- lib/src/models/photo_entity.dart | 20 +++ lib/src/pages/gallery.dart | 90 ------------ lib/src/pages/gallery/gallery_model.dart | 16 +++ lib/src/pages/gallery/gallery_widget.dart | 49 +++++++ .../pages/gallery/gallery_widget_model.dart | 28 ++++ .../gallery}/widgets/gallery_appbar.dart | 2 +- .../widgets/gallery_bottom_sheet.dart | 7 +- .../widgets/photo_gallery_container.dart | 40 ++++++ .../pages/{ => photo_view}/photo_view.dart | 49 +------ .../widgets/photo_view_appbar.dart | 15 +- .../widgets/photo_view_container.dart | 0 lib/src/uikit/colors/color_palette.dart | 8 ++ lib/src/uikit/colors/color_scheme.dart | 11 +- lib/src/uikit/theme/theme_data.dart | 4 + lib/src/widgets/photo_gallery_container.dart | 19 --- lib/src/widgets/shimmer_loading.dart | 47 ------- pubspec.lock | 24 ++++ pubspec.yaml | 5 +- 23 files changed, 366 insertions(+), 217 deletions(-) rename {lib/src/assets/fonts => fonts}/Magnolia.ttf (100%) create mode 100644 lib/src/feature/photos/data/i_photo_repository.dart create mode 100644 lib/src/feature/photos/data/mock_photo_repository.dart create mode 100644 lib/src/models/photo_entity.dart delete mode 100644 lib/src/pages/gallery.dart create mode 100644 lib/src/pages/gallery/gallery_model.dart create mode 100644 lib/src/pages/gallery/gallery_widget.dart create mode 100644 lib/src/pages/gallery/gallery_widget_model.dart rename lib/src/{ => pages/gallery}/widgets/gallery_appbar.dart (88%) rename lib/src/{ => pages/gallery}/widgets/gallery_bottom_sheet.dart (91%) create mode 100644 lib/src/pages/gallery/widgets/photo_gallery_container.dart rename lib/src/pages/{ => photo_view}/photo_view.dart (53%) rename lib/src/{ => pages/photo_view}/widgets/photo_view_appbar.dart (65%) rename lib/src/{ => pages/photo_view}/widgets/photo_view_container.dart (100%) delete mode 100644 lib/src/widgets/photo_gallery_container.dart delete mode 100644 lib/src/widgets/shimmer_loading.dart diff --git a/README.md b/README.md index a45aff8..5751ec4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ # surf_flutter_summer_school_24 Шаблонный репозиторий для Surf Flutter Summer School '24 +![Screenshot_20240724_223550.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_223550.png) +![Screenshot_20240724_223810.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_223810.png) +![Screenshot_20240724_223903.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_223903.png) +![Screenshot_20240724_223949.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_223949.png) +![Screenshot_20240724_224015.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224015.png) +![Screenshot_20240724_224044.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224044.png) +![Screenshot_20240724_224117.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224117.png) +![Screenshot_20240724_224314.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224314.png) +![Screenshot_20240724_224210.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224210.png) +![Screenshot_20240724_224235.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224235.png) \ No newline at end of file diff --git a/lib/src/assets/fonts/Magnolia.ttf b/fonts/Magnolia.ttf similarity index 100% rename from lib/src/assets/fonts/Magnolia.ttf rename to fonts/Magnolia.ttf diff --git a/lib/src/feature/photos/data/i_photo_repository.dart b/lib/src/feature/photos/data/i_photo_repository.dart new file mode 100644 index 0000000..ff4a94b --- /dev/null +++ b/lib/src/feature/photos/data/i_photo_repository.dart @@ -0,0 +1,5 @@ +import '../../../models/photo_entity.dart'; + +abstract interface class IPhotoRepository { + Future> getPhotos(); +} \ No newline at end of file diff --git a/lib/src/feature/photos/data/mock_photo_repository.dart b/lib/src/feature/photos/data/mock_photo_repository.dart new file mode 100644 index 0000000..d4bb31f --- /dev/null +++ b/lib/src/feature/photos/data/mock_photo_repository.dart @@ -0,0 +1,130 @@ +import 'package:surf_flutter_summer_school_24/src/feature/photos/data/i_photo_repository.dart'; +import 'package:surf_flutter_summer_school_24/src/models/photo_entity.dart'; + + +class MockPhotoRepository implements IPhotoRepository { + @override + Future> getPhotos() async{ + return [ + const PhotoEntity( + id: '1', + url: 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album' + ), + const PhotoEntity( + id: '2', + url: 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album' + ), + const PhotoEntity( + id: '3', + url: 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album' + ), + const PhotoEntity( + id: '4', + url: 'https://sun9-55.userapi.com/impg/mDIc6RPZmIznA9Uip0tKXeep7gQg5ot0XUUaVw/OyDLqBzPL0A.jpg?size=1024x1280&quality=96&sign=a38727bf7d11a4b1811446972e048f9e&type=album' + ), + const PhotoEntity( + id: '5', + url: 'https://sun9-38.userapi.com/impg/mtqQmWCnxwMDRjqVGEiczyfZ0CqSu7G7e1BpMg/JOfRS5xYG_w.jpg?size=884x884&quality=96&sign=d972f22e5d4b8d82c82323b81655e86e&type=album' + ), + const PhotoEntity( + id: '6', + url: 'https://sun9-25.userapi.com/impg/U1xodQh60uHV1H75doPQtI9lHR-ps4y7pQ7KTQ/vsRSDAx2Ma8.jpg?size=639x641&quality=96&sign=975c4c8ee9bd5e4c3976f211ef239c98&type=album' + ), + const PhotoEntity( + id: '7', + url: 'https://sun9-24.userapi.com/impg/UVP9-Kst1oeGJvfFz6cOgCFRgMacoQrqYeqdZA/s4SRxgafGxk.jpg?size=637x634&quality=96&sign=80155bf77dc4385ccf467379490b1da4&type=album' + ), + const PhotoEntity( + id: '8', + url: 'https://sun9-30.userapi.com/impg/MO4GiY_8B_f3ep-8NusBQWAZOy-QJVTHWyZ2Cw/EJRz0qfPmJE.jpg?size=564x564&quality=96&sign=ef87156b2be6159ab306e12152df3d29&type=album' + ), + const PhotoEntity( + id: '9', + url: 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album' + ), + const PhotoEntity( + id: '10', + url: 'https://sun9-40.userapi.com/impf/c846017/v846017039/c4d2a/_QCZsc09OYE.jpg?size=540x530&quality=96&sign=733f462520a192a9bbf5b716f00c2a2f&type=album' + ), + const PhotoEntity( + id: '11', + url: 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album' + ), + const PhotoEntity( + id: '12', + url: 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album' + ), + const PhotoEntity( + id: '13', + url: 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album' + ), + const PhotoEntity( + id: '14', + url: 'https://sun9-55.userapi.com/impg/mDIc6RPZmIznA9Uip0tKXeep7gQg5ot0XUUaVw/OyDLqBzPL0A.jpg?size=1024x1280&quality=96&sign=a38727bf7d11a4b1811446972e048f9e&type=album' + ), + const PhotoEntity( + id: '15', + url: 'https://sun9-38.userapi.com/impg/mtqQmWCnxwMDRjqVGEiczyfZ0CqSu7G7e1BpMg/JOfRS5xYG_w.jpg?size=884x884&quality=96&sign=d972f22e5d4b8d82c82323b81655e86e&type=album' + ), + const PhotoEntity( + id: '16', + url: 'https://sun9-25.userapi.com/impg/U1xodQh60uHV1H75doPQtI9lHR-ps4y7pQ7KTQ/vsRSDAx2Ma8.jpg?size=639x641&quality=96&sign=975c4c8ee9bd5e4c3976f211ef239c98&type=album' + ), + const PhotoEntity( + id: '17', + url: 'https://sun9-24.userapi.com/impg/UVP9-Kst1oeGJvfFz6cOgCFRgMacoQrqYeqdZA/s4SRxgafGxk.jpg?size=637x634&quality=96&sign=80155bf77dc4385ccf467379490b1da4&type=album' + ), + const PhotoEntity( + id: '18', + url: 'https://sun9-30.userapi.com/impg/MO4GiY_8B_f3ep-8NusBQWAZOy-QJVTHWyZ2Cw/EJRz0qfPmJE.jpg?size=564x564&quality=96&sign=ef87156b2be6159ab306e12152df3d29&type=album' + ), + const PhotoEntity( + id: '19', + url: 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album' + ), + const PhotoEntity( + id: '20', + url: 'https://sun9-40.userapi.com/impf/c846017/v846017039/c4d2a/_QCZsc09OYE.jpg?size=540x530&quality=96&sign=733f462520a192a9bbf5b716f00c2a2f&type=album' + ),const PhotoEntity( + id: '21', + url: 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album' + ), + const PhotoEntity( + id: '22', + url: 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album' + ), + const PhotoEntity( + id: '23', + url: 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album' + ), + const PhotoEntity( + id: '24', + url: 'https://sun9-55.userapi.com/impg/mDIc6RPZmIznA9Uip0tKXeep7gQg5ot0XUUaVw/OyDLqBzPL0A.jpg?size=1024x1280&quality=96&sign=a38727bf7d11a4b1811446972e048f9e&type=album' + ), + const PhotoEntity( + id: '25', + url: 'https://sun9-38.userapi.com/impg/mtqQmWCnxwMDRjqVGEiczyfZ0CqSu7G7e1BpMg/JOfRS5xYG_w.jpg?size=884x884&quality=96&sign=d972f22e5d4b8d82c82323b81655e86e&type=album' + ), + const PhotoEntity( + id: '26', + url: 'https://sun9-25.userapi.com/impg/U1xodQh60uHV1H75doPQtI9lHR-ps4y7pQ7KTQ/vsRSDAx2Ma8.jpg?size=639x641&quality=96&sign=975c4c8ee9bd5e4c3976f211ef239c98&type=album' + ), + const PhotoEntity( + id: '27', + url: 'https://sun9-24.userapi.com/impg/UVP9-Kst1oeGJvfFz6cOgCFRgMacoQrqYeqdZA/s4SRxgafGxk.jpg?size=637x634&quality=96&sign=80155bf77dc4385ccf467379490b1da4&type=album' + ), + const PhotoEntity( + id: '28', + url: 'https://sun9-30.userapi.com/impg/MO4GiY_8B_f3ep-8NusBQWAZOy-QJVTHWyZ2Cw/EJRz0qfPmJE.jpg?size=564x564&quality=96&sign=ef87156b2be6159ab306e12152df3d29&type=album' + ), + const PhotoEntity( + id: '29', + url: 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album' + ), + const PhotoEntity( + id: '30', + url: 'https://sun9-40.userapi.com/impf/c846017/v846017039/c4d2a/_QCZsc09OYE.jpg?size=540x530&quality=96&sign=733f462520a192a9bbf5b716f00c2a2f&type=album' + ), + ]; + } +} \ No newline at end of file diff --git a/lib/src/main.dart b/lib/src/main.dart index 3fa117a..c6c74bf 100644 --- a/lib/src/main.dart +++ b/lib/src/main.dart @@ -4,7 +4,7 @@ import 'package:surf_flutter_summer_school_24/src/feature/theme/data/theme_repos import 'package:surf_flutter_summer_school_24/src/feature/theme/di/theme_inherited.dart'; import 'package:surf_flutter_summer_school_24/src/feature/theme/domain/theme_controller.dart'; import 'package:surf_flutter_summer_school_24/src/feature/theme/ui/theme_builder.dart'; -import 'package:surf_flutter_summer_school_24/src/pages/gallery.dart'; +import 'package:surf_flutter_summer_school_24/src/pages/gallery/gallery_widget.dart'; import 'package:surf_flutter_summer_school_24/src/storage/theme/theme_storage.dart'; import 'package:surf_flutter_summer_school_24/src/uikit/theme/theme_data.dart'; @@ -40,7 +40,7 @@ class MainApp extends StatelessWidget { theme: AppThemeData.lightTheme, darkTheme: AppThemeData.darkTheme, themeMode: themeMode, - home: Gallery() + home: const GalleryWidget() ); }, ) diff --git a/lib/src/models/photo_entity.dart b/lib/src/models/photo_entity.dart new file mode 100644 index 0000000..0dd062f --- /dev/null +++ b/lib/src/models/photo_entity.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; + +final class PhotoEntity extends Equatable { + final String id; + final String url; + final DateTime? createAt; + + const PhotoEntity({ + required this.id, + required this.url, + this.createAt, + }); + + @override + List get props => [ + id, + url, + createAt, + ]; +} \ No newline at end of file diff --git a/lib/src/pages/gallery.dart b/lib/src/pages/gallery.dart deleted file mode 100644 index 81ba5c4..0000000 --- a/lib/src/pages/gallery.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:surf_flutter_summer_school_24/src/pages/photo_view.dart'; -import 'package:surf_flutter_summer_school_24/src/widgets/gallery_appbar.dart'; -import 'package:surf_flutter_summer_school_24/src/widgets/photo_gallery_container.dart'; - -List images = [ - 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', - 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', - 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', - 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', - 'https://sun9-55.userapi.com/impg/mDIc6RPZmIznA9Uip0tKXeep7gQg5ot0XUUaVw/OyDLqBzPL0A.jpg?size=1024x1280&quality=96&sign=a38727bf7d11a4b1811446972e048f9e&type=album', - 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', - 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', - 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', - 'https://sun9-38.userapi.com/impg/mtqQmWCnxwMDRjqVGEiczyfZ0CqSu7G7e1BpMg/JOfRS5xYG_w.jpg?size=884x884&quality=96&sign=d972f22e5d4b8d82c82323b81655e86e&type=album', - 'https://sun9-25.userapi.com/impg/U1xodQh60uHV1H75doPQtI9lHR-ps4y7pQ7KTQ/vsRSDAx2Ma8.jpg?size=639x641&quality=96&sign=975c4c8ee9bd5e4c3976f211ef239c98&type=album', - 'https://sun9-24.userapi.com/impg/UVP9-Kst1oeGJvfFz6cOgCFRgMacoQrqYeqdZA/s4SRxgafGxk.jpg?size=637x634&quality=96&sign=80155bf77dc4385ccf467379490b1da4&type=album', - 'https://sun9-30.userapi.com/impg/MO4GiY_8B_f3ep-8NusBQWAZOy-QJVTHWyZ2Cw/EJRz0qfPmJE.jpg?size=564x564&quality=96&sign=ef87156b2be6159ab306e12152df3d29&type=album', - 'https://sun9-40.userapi.com/impf/c846017/v846017039/c4d2a/_QCZsc09OYE.jpg?size=540x530&quality=96&sign=733f462520a192a9bbf5b716f00c2a2f&type=album', - 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', - 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', - 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', - 'https://sun9-55.userapi.com/impg/mDIc6RPZmIznA9Uip0tKXeep7gQg5ot0XUUaVw/OyDLqBzPL0A.jpg?size=1024x1280&quality=96&sign=a38727bf7d11a4b1811446972e048f9e&type=album', - 'https://sun9-66.userapi.com/impg/c855428/v855428659/1df047/63VkypHBeZo.jpg?size=604x377&quality=96&sign=84e35c2c415bcf6603d8c81269bcfec4&type=album', - 'https://sun9-67.userapi.com/impg/tX4OvfRLP0qjy8pX-D7WGIrPp6tU9Kk5OQErBw/O7cfSBKIhps.jpg?size=1199x1199&quality=95&sign=032b69843ac686317904906820bf4020&type=album', - 'https://sun9-70.userapi.com/impg/5y5vwIq2-aaxiyscdm9XWy3iOw-KDQUPbWoidg/-J_O_1Pnx9Q.jpg?size=1024x1280&quality=96&sign=285af90845367d97da75d3dbceabbea0&type=album', - 'https://sun9-38.userapi.com/impg/mtqQmWCnxwMDRjqVGEiczyfZ0CqSu7G7e1BpMg/JOfRS5xYG_w.jpg?size=884x884&quality=96&sign=d972f22e5d4b8d82c82323b81655e86e&type=album', - 'https://sun9-25.userapi.com/impg/U1xodQh60uHV1H75doPQtI9lHR-ps4y7pQ7KTQ/vsRSDAx2Ma8.jpg?size=639x641&quality=96&sign=975c4c8ee9bd5e4c3976f211ef239c98&type=album', - 'https://sun9-24.userapi.com/impg/UVP9-Kst1oeGJvfFz6cOgCFRgMacoQrqYeqdZA/s4SRxgafGxk.jpg?size=637x634&quality=96&sign=80155bf77dc4385ccf467379490b1da4&type=album', - 'https://sun9-30.userapi.com/impg/MO4GiY_8B_f3ep-8NusBQWAZOy-QJVTHWyZ2Cw/EJRz0qfPmJE.jpg?size=564x564&quality=96&sign=ef87156b2be6159ab306e12152df3d29&type=album', - 'https://sun9-40.userapi.com/impf/c846017/v846017039/c4d2a/_QCZsc09OYE.jpg?size=540x530&quality=96&sign=733f462520a192a9bbf5b716f00c2a2f&type=album', -]; - -class Gallery extends StatefulWidget { - const Gallery({super.key}); - - @override - State createState() => _GalleryState(); -} - -class _GalleryState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: GalleryAppbar(), - body: GridView.builder( - primary: false, - padding: const EdgeInsets.all(20), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 3, - mainAxisSpacing: 5, - ), - itemCount: images.length, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PhotoView(images: images, initialIndex: index), - ), - ); - }, - child: FutureBuilder( - future: loadImage(images[index]), - builder: (context, snapshot) { - if ((snapshot.connectionState == ConnectionState.waiting)) { - return PhotoGalleryContainer( - urlImage: images[index], - isLoading: true, - ); - } else { - return PhotoGalleryContainer( - urlImage: images[index], - isLoading: false, - ); - } - }, - ), - ); - }, - )); - } - - Future loadImage(String urlImage) async { - await Future.delayed(Duration(seconds: 1)); - return; - } -} diff --git a/lib/src/pages/gallery/gallery_model.dart b/lib/src/pages/gallery/gallery_model.dart new file mode 100644 index 0000000..0d08a2c --- /dev/null +++ b/lib/src/pages/gallery/gallery_model.dart @@ -0,0 +1,16 @@ +import 'package:elementary/elementary.dart'; +import 'package:flutter/material.dart'; +import '../../feature/photos/data/i_photo_repository.dart'; +import '../../models/photo_entity.dart'; + +class GalleryModel extends ElementaryModel { + final IPhotoRepository photoRepository; + final ValueNotifier> images = ValueNotifier([]); // Используем ValueNotifier + + GalleryModel(this.photoRepository); + + Future fetchPhotos() async { + final photos = await photoRepository.getPhotos(); + images.value = photos; // Обновляем ValueNotifier + } +} \ No newline at end of file diff --git a/lib/src/pages/gallery/gallery_widget.dart b/lib/src/pages/gallery/gallery_widget.dart new file mode 100644 index 0000000..939283e --- /dev/null +++ b/lib/src/pages/gallery/gallery_widget.dart @@ -0,0 +1,49 @@ +import 'package:elementary/elementary.dart'; +import 'package:flutter/material.dart'; + +import '../../feature/photos/data/mock_photo_repository.dart'; +import '../../models/photo_entity.dart'; +import 'widgets/gallery_appbar.dart'; +import 'widgets/photo_gallery_container.dart'; +import 'gallery_model.dart'; +import 'gallery_widget_model.dart'; + +class GalleryWidget extends ElementaryWidget { + const GalleryWidget({ + Key? key, + WidgetModelFactory wm = createGalleryWidgetModel, + }) + : super(wm, key: key); + + static GalleryWidgetModel createGalleryWidgetModel(BuildContext context) => + GalleryWidgetModel(GalleryModel(MockPhotoRepository())); + + @override + Widget build(GalleryWidgetModel wm) { + return Scaffold( + appBar: const GalleryAppbar(), + body: ValueListenableBuilder< + List>( // Используем ValueListenableBuilder + valueListenable: wm.model.images, + builder: (context, images, child) { + return GridView.builder( + primary: false, + padding: const EdgeInsets.all(20), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 3, + mainAxisSpacing: 5, + ), + itemCount: images.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () => wm.onPhotoTap(index), + child: PhotoGalleryContainer(urlImage: images[index].url), + ); + }, + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/pages/gallery/gallery_widget_model.dart b/lib/src/pages/gallery/gallery_widget_model.dart new file mode 100644 index 0000000..eab271b --- /dev/null +++ b/lib/src/pages/gallery/gallery_widget_model.dart @@ -0,0 +1,28 @@ +import 'package:elementary/elementary.dart'; +import 'package:flutter/material.dart'; + +import '../../feature/photos/data/mock_photo_repository.dart'; +import '../photo_view/photo_view.dart'; +import 'gallery_model.dart'; +import 'gallery_widget.dart'; + +class GalleryWidgetModel extends WidgetModel { + GalleryWidgetModel(GalleryModel model) : super(model); + + @override + void initWidgetModel() { + super.initWidgetModel(); + model.fetchPhotos(); + } + + void onPhotoTap(int index) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => PhotoView( + images: model.images.value, // Получаем значение из ValueNotifier + initialIndex: index, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/widgets/gallery_appbar.dart b/lib/src/pages/gallery/widgets/gallery_appbar.dart similarity index 88% rename from lib/src/widgets/gallery_appbar.dart rename to lib/src/pages/gallery/widgets/gallery_appbar.dart index c3db484..acc76eb 100644 --- a/lib/src/widgets/gallery_appbar.dart +++ b/lib/src/pages/gallery/widgets/gallery_appbar.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:surf_flutter_summer_school_24/src/widgets/gallery_bottom_sheet.dart'; +import 'package:surf_flutter_summer_school_24/src/pages/gallery/widgets/gallery_bottom_sheet.dart'; class GalleryAppbar extends StatelessWidget implements PreferredSizeWidget { const GalleryAppbar({super.key}); diff --git a/lib/src/widgets/gallery_bottom_sheet.dart b/lib/src/pages/gallery/widgets/gallery_bottom_sheet.dart similarity index 91% rename from lib/src/widgets/gallery_bottom_sheet.dart rename to lib/src/pages/gallery/widgets/gallery_bottom_sheet.dart index e4f2239..9081ba4 100644 --- a/lib/src/widgets/gallery_bottom_sheet.dart +++ b/lib/src/pages/gallery/widgets/gallery_bottom_sheet.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; - -import '../feature/theme/di/theme_inherited.dart'; +import '../../../feature/theme/di/theme_inherited.dart'; class GalleryBottomSheet extends StatelessWidget { const GalleryBottomSheet({super.key}); @override Widget build(BuildContext context) { - return Container( + return SizedBox( height: 160, child: Center( child: Column( @@ -37,7 +36,7 @@ class GalleryBottomSheet extends StatelessWidget { child: Text( ThemeInherited.of(context).themeMode.value == ThemeMode.light ? 'Светлая' - : 'Темная', + : 'Темная', style: TextStyle(color: Theme.of(context).colorScheme.secondary) ), ), ], diff --git a/lib/src/pages/gallery/widgets/photo_gallery_container.dart b/lib/src/pages/gallery/widgets/photo_gallery_container.dart new file mode 100644 index 0000000..d21b405 --- /dev/null +++ b/lib/src/pages/gallery/widgets/photo_gallery_container.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + + +class PhotoGalleryContainer extends StatefulWidget { + final String urlImage; + + const PhotoGalleryContainer({super.key, required this.urlImage}); + + @override + State createState() => _PhotoGalleryContainerState(); +} + +class _PhotoGalleryContainerState extends State { + + + @override + Widget build(BuildContext context) { + return Image.network( + widget.urlImage, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Shimmer.fromColors( + baseColor: Theme.of(context).colorScheme.errorContainer, + highlightColor: Theme.of(context).colorScheme.onErrorContainer, + period: const Duration(milliseconds: 600), + direction: ShimmerDirection.ltr, + child: Container( + color: Colors.grey, + width: double.infinity, + height: double.infinity, + ), + ); + }, + ); + } +} diff --git a/lib/src/pages/photo_view.dart b/lib/src/pages/photo_view/photo_view.dart similarity index 53% rename from lib/src/pages/photo_view.dart rename to lib/src/pages/photo_view/photo_view.dart index 7993383..fc6a599 100644 --- a/lib/src/pages/photo_view.dart +++ b/lib/src/pages/photo_view/photo_view.dart @@ -1,10 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:surf_flutter_summer_school_24/src/widgets/photo_view_appbar.dart'; -import 'package:surf_flutter_summer_school_24/src/widgets/photo_view_container.dart'; +import 'package:surf_flutter_summer_school_24/src/pages/photo_view/widgets/photo_view_appbar.dart'; +import 'package:surf_flutter_summer_school_24/src/pages/photo_view/widgets/photo_view_container.dart'; + +import '../../models/photo_entity.dart'; class PhotoView extends StatefulWidget { - final List images; + final List images; final int initialIndex; const PhotoView({super.key, required this.images, required this.initialIndex}); @@ -16,7 +18,6 @@ class PhotoView extends StatefulWidget { class _PhotoViewState extends State with TickerProviderStateMixin { late PageController _pageViewController; - late TabController _tabController; int _currentPageIndex = 0; @override @@ -24,14 +25,12 @@ class _PhotoViewState extends State with TickerProviderStateMixin { super.initState(); _currentPageIndex = widget.initialIndex; _pageViewController = PageController(initialPage: widget.initialIndex, viewportFraction: 0.8); - _tabController = TabController(length: widget.images.length, vsync: this, initialIndex: widget.initialIndex); } @override void dispose() { super.dispose(); _pageViewController.dispose(); - _tabController.dispose(); } @override @@ -47,10 +46,9 @@ class _PhotoViewState extends State with TickerProviderStateMixin { children: [ PageView.builder( controller: _pageViewController, - onPageChanged: _handlePageViewChanged, itemCount: widget.images.length, itemBuilder: (context, index) { - return PhotoViewContainer(urlImage: widget.images[index]); + return PhotoViewContainer(urlImage: widget.images[index].url); }, ), ], @@ -59,41 +57,6 @@ class _PhotoViewState extends State with TickerProviderStateMixin { ) ); } - - void _handlePageViewChanged(int currentPageIndex) { - if (!_isOnDesktopAndWeb) { - setState(() { - _currentPageIndex = currentPageIndex; - }); - } else { - _tabController.index = currentPageIndex; - } - } - - void _updateCurrentPageIndex(int index) { - _tabController.index = index; - _pageViewController.animateToPage( - index, - duration: const Duration(milliseconds: 400), - curve: Curves.easeInOut, - ); - } - - bool get _isOnDesktopAndWeb { - if (kIsWeb) { - return true; - } - switch (defaultTargetPlatform) { - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - return true; - case TargetPlatform.android: - case TargetPlatform.iOS: - case TargetPlatform.fuchsia: - return false; - } - } } diff --git a/lib/src/widgets/photo_view_appbar.dart b/lib/src/pages/photo_view/widgets/photo_view_appbar.dart similarity index 65% rename from lib/src/widgets/photo_view_appbar.dart rename to lib/src/pages/photo_view/widgets/photo_view_appbar.dart index e524e92..4cec851 100644 --- a/lib/src/widgets/photo_view_appbar.dart +++ b/lib/src/pages/photo_view/widgets/photo_view_appbar.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:surf_flutter_summer_school_24/src/pages/gallery.dart'; + +import '../../gallery/gallery_widget.dart'; class PhotoViewAppbar extends StatelessWidget implements PreferredSizeWidget { final int total; @@ -8,14 +9,13 @@ class PhotoViewAppbar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - String ct = current.toString() + "/" + total.toString(); return AppBar( leading: IconButton( onPressed: () { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - builder: (context) => Gallery(), + builder: (context) => const GalleryWidget(), ), (Route route) => false, ); }, @@ -23,8 +23,13 @@ class PhotoViewAppbar extends StatelessWidget implements PreferredSizeWidget { ), actions: [ Padding( - padding: EdgeInsets.fromLTRB(0, 0, 10, 0), - child: Text(ct), + padding: const EdgeInsets.fromLTRB(0, 0, 10, 0), + child: Row( + children: [ + Text(current.toString()), + Text("/$total", style: TextStyle(color: Theme.of(context).colorScheme.secondary)) + ], + ), ) ], ); diff --git a/lib/src/widgets/photo_view_container.dart b/lib/src/pages/photo_view/widgets/photo_view_container.dart similarity index 100% rename from lib/src/widgets/photo_view_container.dart rename to lib/src/pages/photo_view/widgets/photo_view_container.dart diff --git a/lib/src/uikit/colors/color_palette.dart b/lib/src/uikit/colors/color_palette.dart index d372f9f..afc67bd 100644 --- a/lib/src/uikit/colors/color_palette.dart +++ b/lib/src/uikit/colors/color_palette.dart @@ -21,6 +21,8 @@ abstract class LightColorPalette { static const lightSilver = Color(0xFFD9D9D9); + static const silver = Color(0xFFB2B2B2); + static const lightGreen = Color(0xFFB5CCAE); static const darkGreen = Color(0xFF84A58F); @@ -32,6 +34,10 @@ abstract class LightColorPalette { static const appleGreen = Color(0xFF83C000); static const platinum = Color(0xFFE7E4E0); + + static const raisinBlack = Color(0xFF232323); + + static const darkSilver = Color(0xFF626262); } abstract class DarkColorPalette { @@ -47,6 +53,8 @@ abstract class DarkColorPalette { static const lightSilver = Color(0xFFD6D6D6); + static const darkSilver = Color(0xFF626262); + static const cyclamen = Color(0xFFFF79A8); static const etonBlue = Color(0xFF9CD29C); diff --git a/lib/src/uikit/colors/color_scheme.dart b/lib/src/uikit/colors/color_scheme.dart index a528938..c55e437 100644 --- a/lib/src/uikit/colors/color_scheme.dart +++ b/lib/src/uikit/colors/color_scheme.dart @@ -61,11 +61,12 @@ class AppColorScheme extends ThemeExtension { final Color shimmer; + AppColorScheme.light() : primary = LightColorPalette.black, onPrimary = LightColorPalette.white, - secondary = LightColorPalette.greenYellow, - onSecondary = LightColorPalette.chineseBlack, + secondary = LightColorPalette.darkSilver, + onSecondary = LightColorPalette.platinum, surface = LightColorPalette.white, surfaceSecondary = LightColorPalette.cultured, onSurface = LightColorPalette.chineseBlack, @@ -89,12 +90,12 @@ class AppColorScheme extends ThemeExtension { skeletonSecondary = LightColorPalette.cultured, skeletonTertiary = LightColorPalette.lightSilver, tetradicBackground = LightColorPalette.lightGreen, - shimmer = LightColorPalette.platinum; + shimmer = LightColorPalette.silver; AppColorScheme.dark() : primary = DarkColorPalette.white, onPrimary = DarkColorPalette.black, - secondary = DarkColorPalette.inchworm, + secondary = DarkColorPalette.darkSilver, onSecondary = DarkColorPalette.black, surface = DarkColorPalette.raisinBlack, surfaceSecondary = DarkColorPalette.raisinBlack, @@ -119,7 +120,7 @@ class AppColorScheme extends ThemeExtension { skeletonSecondary = DarkColorPalette.raisinBlack, skeletonTertiary = DarkColorPalette.lightSilver, tetradicBackground = DarkColorPalette.etonBlue, - shimmer = LightColorPalette.platinum; + shimmer =DarkColorPalette.raisinBlack; const AppColorScheme._({ required this.primary, diff --git a/lib/src/uikit/theme/theme_data.dart b/lib/src/uikit/theme/theme_data.dart index 51fbabc..59b1868 100644 --- a/lib/src/uikit/theme/theme_data.dart +++ b/lib/src/uikit/theme/theme_data.dart @@ -21,6 +21,8 @@ abstract class AppThemeData { onBackground: _lightColorScheme.onBackground, surface: _lightColorScheme.surface, onSurface: _lightColorScheme.onSurface, + errorContainer: _lightColorScheme.shimmer, + onErrorContainer: _lightColorScheme.onSecondary ), scaffoldBackgroundColor: _lightColorScheme.background, appBarTheme: AppBarTheme( @@ -60,6 +62,8 @@ abstract class AppThemeData { onBackground: _darkColorScheme.onBackground, surface: _darkColorScheme.surface, onSurface: _darkColorScheme.onSurface, + errorContainer: _darkColorScheme.shimmer, + onErrorContainer: _darkColorScheme.secondary ), scaffoldBackgroundColor: _darkColorScheme.background, appBarTheme: AppBarTheme( diff --git a/lib/src/widgets/photo_gallery_container.dart b/lib/src/widgets/photo_gallery_container.dart deleted file mode 100644 index b367862..0000000 --- a/lib/src/widgets/photo_gallery_container.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:surf_flutter_summer_school_24/src/widgets/shimmer_loading.dart'; - -class PhotoGalleryContainer extends StatelessWidget { - final String urlImage; - final bool isLoading; - - const PhotoGalleryContainer({super.key, required this.urlImage, required this.isLoading}); - - @override - Widget build(BuildContext context) { - return ShimmerLoading( - isLoading: isLoading, - child: Container( - child: Image.network(urlImage, fit: BoxFit.cover), - ), - ); - } -} diff --git a/lib/src/widgets/shimmer_loading.dart b/lib/src/widgets/shimmer_loading.dart deleted file mode 100644 index 07e7855..0000000 --- a/lib/src/widgets/shimmer_loading.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; - -const _shimmerGradient = LinearGradient( - colors: [ - Color(0xFFEBEBF4), - Color(0xFFF4F4F4), - ], - stops: [ - 0.0, - 1.0, - ], - begin: Alignment(0.0, 0.0), - end: Alignment(1.0, 0.0), - tileMode: TileMode.clamp, -); - -class ShimmerLoading extends StatefulWidget { - const ShimmerLoading({ - super.key, - required this.isLoading, - required this.child, - }); - - final bool isLoading; - final Widget child; - - @override - State createState() => _ShimmerLoadingState(); -} - -class _ShimmerLoadingState extends State { - @override - Widget build(BuildContext context) { - if (!widget.isLoading) { - return widget.child; - } - - return ShaderMask( - blendMode: BlendMode.srcATop, - shaderCallback: (bounds) { - return _shimmerGradient.createShader(bounds); - }, - child: widget.child, - ); - } -} - diff --git a/pubspec.lock b/pubspec.lock index c081e39..326d59e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + elementary: + dependency: "direct main" + description: + name: elementary + sha256: "9b8385fac1c2102918a512d7a80ce0babe61af960f3c86902a671b09131d958c" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -248,6 +264,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 670c5d4..b50800b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,9 @@ dependencies: flutter: sdk: flutter shared_preferences: ^2.2.3 + shimmer: ^3.0.0 + equatable: ^2.0.5 + elementary: ^3.0.0 dev_dependencies: flutter_test: @@ -24,6 +27,6 @@ flutter: fonts: - family: Magnolia fonts: - - asset: lib/src/assets/fonts/Magnolia.ttf + - asset: fonts/Magnolia.ttf From 3c923078bc58ef89d630a1946888cd9754cfc083 Mon Sep 17 00:00:00 2001 From: Daniel Visotskiy Date: Wed, 24 Jul 2024 23:12:02 +0300 Subject: [PATCH 4/4] Update README.md --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5751ec4..384ddae 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # surf_flutter_summer_school_24 Шаблонный репозиторий для Surf Flutter Summer School '24 -![Screenshot_20240724_223550.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_223550.png) -![Screenshot_20240724_223810.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_223810.png) -![Screenshot_20240724_223903.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_223903.png) -![Screenshot_20240724_223949.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_223949.png) -![Screenshot_20240724_224015.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224015.png) -![Screenshot_20240724_224044.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224044.png) -![Screenshot_20240724_224117.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224117.png) -![Screenshot_20240724_224314.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224314.png) -![Screenshot_20240724_224210.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224210.png) -![Screenshot_20240724_224235.png](..%2F..%2F..%2F..%2F%C8%E7%EE%E1%F0%E0%E6%E5%ED%E8%FF%2FScreenshot_20240724_224235.png) \ No newline at end of file +![Screenshot_20240724_223550](https://github.com/user-attachments/assets/fa94c98e-a9d6-411a-9a06-b9aaa0d099ff) +![Screenshot_20240724_223810](https://github.com/user-attachments/assets/5a15f69d-4c85-420f-91f8-6defd15e5310) +![Screenshot_20240724_223903](https://github.com/user-attachments/assets/609be02a-7446-43c5-b17c-c344bc3f26ec) +![Screenshot_20240724_223949](https://github.com/user-attachments/assets/da60db58-081d-4d58-b473-c19fa2904b64) +![Screenshot_20240724_224015](https://github.com/user-attachments/assets/46655c2a-39c3-4d94-8a27-1cad28752049) +![Screenshot_20240724_224044](https://github.com/user-attachments/assets/49cc926d-6b77-404a-8b93-fcb94aeb4898) +![Screenshot_20240724_224117](https://github.com/user-attachments/assets/d52cc536-1ffa-4585-8b55-bca0daeaa60c) +![Screenshot_20240724_224314](https://github.com/user-attachments/assets/6f00962d-2a56-4c8c-b696-20c0c2901f2a) +![Screenshot_20240724_224235](https://github.com/user-attachments/assets/e71b2ff2-3bb9-4f86-a861-e9bd69bfec5c) +![Screenshot_20240724_224210](https://github.com/user-attachments/assets/4c673ea4-a26d-4f1b-af6d-24fa7c085487)