Browse Source

add: reward feature

pull/52/head
AmirrezaChegini 2 days ago
parent
commit
da15c875d4
  1. BIN
      assets/images/gift.png
  2. BIN
      assets/images/gift_background.png
  3. BIN
      assets/images/gift_disable.png
  4. 10
      assets/svg/icon_play_video.svg
  5. 7
      lib/common_ui/resources/my_assets.dart
  6. 2
      lib/core/constants/my_api.dart
  7. 8
      lib/core/routers/my_routes.dart
  8. 381
      lib/core/widgets/dialog/reward_dialog.dart
  9. 76
      lib/core/widgets/video/my_video_player.dart
  10. 12
      lib/features/download/data/datasource/download_datasource.dart
  11. 2
      lib/features/guider/data/datasource/guider_datasource.dart
  12. 2
      lib/features/home/presentation/bloc/home_bloc.dart
  13. 8
      lib/features/level/data/datasource/level_datasource.dart
  14. 25
      lib/features/level/data/model/node_model.dart
  15. 21
      lib/features/level/data/model/prize_model.dart
  16. 6
      lib/features/level/data/repository_impl/level_repository_impl.dart
  17. 30
      lib/features/level/domain/entity/node_entity.dart
  18. 25
      lib/features/level/domain/entity/prize_entity.dart
  19. 6
      lib/features/level/domain/entity/total_data_entity.dart
  20. 4
      lib/features/level/domain/entity/total_data_entity.g.dart
  21. 6
      lib/features/level/domain/repository/level_repository.dart
  22. 6
      lib/features/level/domain/usecases/get_levels_usecase.dart
  23. 29
      lib/features/level/presentation/bloc/level_bloc.dart
  24. 2
      lib/features/level/presentation/bloc/level_event.dart
  25. 17
      lib/features/level/presentation/ui/level_page.dart
  26. 121
      lib/features/level/presentation/ui/widgets/level_widget.dart
  27. 161
      lib/features/level/presentation/ui/widgets/node_widget.dart
  28. 19
      lib/features/question/data/datasource/question_datasource.dart
  29. 5
      lib/init_bindings.dart
  30. 3
      lib/l10n/app_ar.arb
  31. 3
      lib/l10n/app_de.arb
  32. 3
      lib/l10n/app_en.arb
  33. 3
      lib/l10n/app_fr.arb
  34. 6
      lib/l10n/app_localizations.dart
  35. 3
      lib/l10n/app_localizations_ar.dart
  36. 3
      lib/l10n/app_localizations_de.dart
  37. 3
      lib/l10n/app_localizations_en.dart
  38. 3
      lib/l10n/app_localizations_fr.dart
  39. 3
      lib/l10n/app_localizations_ru.dart
  40. 3
      lib/l10n/app_localizations_tr.dart
  41. 3
      lib/l10n/app_ru.arb
  42. 3
      lib/l10n/app_tr.arb
  43. 176
      pubspec.lock
  44. 1
      pubspec.yaml

BIN
assets/images/gift.png

After

Width: 71  |  Height: 75  |  Size: 4.3 KiB

BIN
assets/images/gift_background.png

After

Width: 101  |  Height: 105  |  Size: 12 KiB

BIN
assets/images/gift_disable.png

After

Width: 95  |  Height: 100  |  Size: 5.3 KiB

10
assets/svg/icon_play_video.svg

@ -0,0 +1,10 @@
<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="22.1758" y="19.1177" width="22.1765" height="24.4706" fill="white"/>
<path d="M30.5879 6.61768C36.9451 6.61771 43.0418 9.14309 47.5371 13.6382C52.0325 18.1335 54.5586 24.231 54.5586 30.5884C54.5586 36.9457 52.0324 43.0423 47.5371 47.5376C43.0418 52.0329 36.9452 54.5591 30.5879 54.5591C24.2305 54.5591 18.1331 52.033 13.6377 47.5376C9.1426 43.0423 6.61722 36.9456 6.61719 30.5884C6.61719 24.231 9.14234 18.1335 13.6377 13.6382C18.1331 9.14283 24.2305 6.61768 30.5879 6.61768ZM27.7012 20.9155C27.0578 20.8844 26.4176 21.0286 25.8496 21.3325C25.2817 21.6365 24.8066 22.0886 24.4756 22.6411C24.1446 23.1937 23.9697 23.8261 23.9697 24.4702V36.7056C23.9696 37.3497 24.1447 37.9821 24.4756 38.5347C24.8066 39.0873 25.2817 39.5403 25.8496 39.8442C26.4176 40.1481 27.0578 40.2924 27.7012 40.2612C28.3447 40.23 28.9679 40.0241 29.5039 39.6665L38.6807 33.5493C39.168 33.2243 39.5674 32.7836 39.8438 32.2671C40.1201 31.7506 40.2646 31.1741 40.2646 30.5884C40.2646 30.0026 40.1202 29.4252 39.8438 28.9087C39.5674 28.3924 39.1679 27.9523 38.6807 27.6274L29.5039 21.5093C28.968 21.1518 28.3446 20.9468 27.7012 20.9155Z" fill="#EB511A" stroke="url(#paint0_linear_42_2463)"/>
<defs>
<linearGradient id="paint0_linear_42_2463" x1="30.5878" y1="6.11768" x2="30.5878" y2="55.0589" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCB32"/>
<stop offset="1" stop-color="#FFA105"/>
</linearGradient>
</defs>
</svg>

7
lib/common_ui/resources/my_assets.dart

@ -36,6 +36,9 @@ class MyAssets {
static const String satellite = 'assets/images/satellite.png'; static const String satellite = 'assets/images/satellite.png';
static const String planetFinal = 'assets/images/planet_final.png'; static const String planetFinal = 'assets/images/planet_final.png';
static const String behindDiamond = 'assets/images/behind_diamond.png'; static const String behindDiamond = 'assets/images/behind_diamond.png';
static const String gift = 'assets/images/gift.png';
static const String giftDisable = 'assets/images/gift_disable.png';
static const String giftBackground = 'assets/images/gift_background.png';
/// SVG /// SVG
static const String closeBtn = 'assets/svg/close_btn.svg'; static const String closeBtn = 'assets/svg/close_btn.svg';
@ -69,6 +72,7 @@ class MyAssets {
static const String diamondContainer = 'assets/svg/diamond_container.svg'; static const String diamondContainer = 'assets/svg/diamond_container.svg';
static const String iconPlay = 'assets/svg/icon_play.svg'; static const String iconPlay = 'assets/svg/icon_play.svg';
static const String iconNotif = 'assets/svg/icon_notif.svg'; static const String iconNotif = 'assets/svg/icon_notif.svg';
static const String iconPlayVideo = 'assets/svg/icon_play_video.svg';
static final List<String> images = [ static final List<String> images = [
backgroundHome, backgroundHome,
@ -103,5 +107,8 @@ class MyAssets {
satellite, satellite,
planetFinal, planetFinal,
behindDiamond, behindDiamond,
gift,
giftDisable,
giftBackground,
]; ];
} }

2
lib/core/constants/my_api.dart

@ -10,7 +10,7 @@ class MyApi {
static const String baseUrl = 'https://hadihoda.newhorizonco.uk/api'; static const String baseUrl = 'https://hadihoda.newhorizonco.uk/api';
static const String levels = '/quiz/optimized/v2/levels/';
static const String levels = '/quiz/optimized/v3/levels/';
static const String images = '/quiz/optimized/download-all-files/images/'; static const String images = '/quiz/optimized/download-all-files/images/';
static const String audios = '/quiz/optimized/v2/download-all-files/audio/'; static const String audios = '/quiz/optimized/v2/download-all-files/audio/';
} }

8
lib/core/routers/my_routes.dart

@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart';
import 'package:hadi_hoda_flutter/core/middlewares/my_middlewares.dart'; import 'package:hadi_hoda_flutter/core/middlewares/my_middlewares.dart';
import 'package:hadi_hoda_flutter/core/utils/my_context.dart'; import 'package:hadi_hoda_flutter/core/utils/my_context.dart';
import 'package:hadi_hoda_flutter/core/widgets/video/my_video_player.dart';
import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_bloc.dart'; import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_bloc.dart';
import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_event.dart'; import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_event.dart';
import 'package:hadi_hoda_flutter/features/download/presentation/ui/download_page.dart'; import 'package:hadi_hoda_flutter/features/download/presentation/ui/download_page.dart';
@ -42,6 +43,7 @@ class Routes {
static const String questionPage = '/question_page'; static const String questionPage = '/question_page';
static const String guiderPage = '/guider_page'; static const String guiderPage = '/guider_page';
static const String levelPage = '/level_page'; static const String levelPage = '/level_page';
static const String videoPage = '/video_page';
} }
final GoRouter appPages = _appPages(); final GoRouter appPages = _appPages();
@ -147,5 +149,11 @@ GoRouter _appPages() => GoRouter(
child: const QuestionPage(), child: const QuestionPage(),
), ),
), ),
GoRoute(
name: Routes.videoPage,
path: Routes.videoPage,
builder: (context, state) =>
MyVideoPlayer(videoURL: state.extra as String),
),
], ],
); );

381
lib/core/widgets/dialog/reward_dialog.dart

@ -0,0 +1,381 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_animations.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart';
import 'package:hadi_hoda_flutter/core/routers/my_routes.dart';
import 'package:hadi_hoda_flutter/core/utils/my_localization.dart';
import 'package:hadi_hoda_flutter/core/utils/screen_size.dart';
import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart';
import 'package:hadi_hoda_flutter/core/widgets/dialog/styles/dialog_background.dart';
import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/prize_entity.dart';
import 'package:lottie/lottie.dart';
Future<void> showRewardDialog({
required BuildContext context,
required PrizeEntity prize,
}) async {
await showDialog(
context: context,
builder: (context) => RewardDialog(prize: prize),
barrierColor: MyColors.purple.withValues(alpha: 0.82),
useSafeArea: false,
);
}
class RewardDialog extends StatelessWidget {
const RewardDialog({super.key, required this.prize});
final PrizeEntity prize;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MyColors.transparent,
body: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Center(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal:
setSize(context: context, mobile: 18, tablet: 120) ?? 0,
),
child: Stack(
alignment: Alignment.center,
children: [
Lottie.asset(
MyAnimations.confetti,
height: context.heightScreen,
fit: BoxFit.cover,
),
Stack(
clipBehavior: Clip.none,
children: [
DialogBackground(
child: Column(
spacing: 34,
children: [
Text(
context.translate.reward,
style: MYTextStyle.titr0.copyWith(
color: Color(0XFF322386),
fontSize: 22,
),
),
Text(
prize.title ?? '',
style: MYTextStyle.titr0.copyWith(
color: Color(0XFF322386),
fontSize: 22,
),
),
CustomPaint(
painter: _CustomShapePainter(),
child: ClipPath(
clipper: _CustomShapeClipper(),
child: GestureDetector(
onTap: () {
context.pushNamed(
Routes.videoPage,
extra: prize.animationURL as String,
);
},
child: Stack(
alignment: Alignment.center,
children: [
Image.network(
prize.imageURL ?? '',
fit: BoxFit.cover,
),
MyImage(
image: MyAssets.iconPlayVideo,
),
],
),
),
),
),
],
),
),
PositionedDirectional(
end: setSize(context: context, mobile: 30, tablet: 40),
top: -12,
child: GestureDetector(
onTap: context.pop,
behavior: HitTestBehavior.opaque,
child: MyImage(
image: MyAssets.closeBtn,
size: setSize(context: context, mobile: 40, tablet: 60),
),
),
),
],
),
],
),
),
),
),
);
}
}
class _CustomShapePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Paint strokePaint = Paint()
..color = Color(0XFFF2F7FF)
..style = PaintingStyle.stroke
..strokeWidth = 4;
final Path path = _CustomShapeClipper().getClip(size);
canvas.drawPath(path, strokePaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return this != oldDelegate;
}
}
class _CustomShapeClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
// Original SVG dimensions to calculate the scaling factors.
final double originalWidth = 193.0;
final double originalHeight = 189.0;
// Scaling factors to make the path responsive.
final double scaleX = size.width / originalWidth;
final double scaleY = size.height / originalHeight;
// The path is defined using the scaled coordinates from the SVG.
final Path path = Path()
..moveTo(148.483 * scaleX, 4.10254 * scaleY)
..cubicTo(
131.624 * scaleX,
1.93333 * scaleY,
111.221 * scaleX,
1.00169 * scaleY,
91.2451 * scaleX,
1.2666 * scaleY,
)
..cubicTo(
71.2667 * scaleX,
1.53156 * scaleY,
51.7626 * scaleX,
2.99274 * scaleY,
36.6973 * scaleX,
5.59668 * scaleY,
)
..cubicTo(
29.1597 * scaleX,
6.8995 * scaleY,
22.7796 * scaleX,
8.48114 * scaleY,
18.0205 * scaleX,
10.3203 * scaleY,
)
..cubicTo(
15.641 * scaleX,
11.2399 * scaleY,
13.7026 * scaleX,
12.2101 * scaleY,
12.2383 * scaleX,
13.2188 * scaleY,
)
..cubicTo(
10.7653 * scaleX,
14.2333 * scaleY,
9.84633 * scaleX,
15.2359 * scaleY,
9.3916 * scaleX,
16.1904 * scaleY,
)
..cubicTo(
8.252 * scaleX,
18.5828 * scaleY,
7.18153 * scaleX,
22.466 * scaleY,
6.2207 * scaleX,
27.5654 * scaleY,
)
..cubicTo(
5.26481 * scaleX,
32.6387 * scaleY,
4.43215 * scaleX,
38.8273 * scaleY,
3.73535 * scaleX,
45.7744 * scaleY,
)
..cubicTo(
2.34189 * scaleX,
59.6675 * scaleY,
1.49647 * scaleX,
76.5363 * scaleY,
1.27832 * scaleX,
93.4678 * scaleY,
)
..cubicTo(
1.06017 * scaleX,
110.4 * scaleY,
1.47057 * scaleX,
127.372 * scaleY,
2.58301 * scaleX,
141.473 * scaleY,
)
..cubicTo(
3.13928 * scaleX,
148.524 * scaleY,
3.86921 * scaleX,
154.841 * scaleY,
4.78125 * scaleX,
160.068 * scaleY,
)
..cubicTo(
5.69748 * scaleX,
165.32 * scaleY,
6.78334 * scaleX,
169.385 * scaleY,
8.01367 * scaleX,
171.984 * scaleY,
)
..cubicTo(
8.53417 * scaleX,
173.084 * scaleY,
9.59654 * scaleX,
174.216 * scaleY,
11.2891 * scaleX,
175.343 * scaleY,
)
..cubicTo(
12.9722 * scaleX,
176.463 * scaleY,
15.1988 * scaleX,
177.524 * scaleY,
17.9219 * scaleX,
178.515 * scaleY,
)
..cubicTo(
23.3679 * scaleX,
180.496 * scaleY,
30.6491 * scaleX,
182.138 * scaleY,
39.1807 * scaleX,
183.437 * scaleY,
)
..cubicTo(
56.2336 * scaleX,
186.032 * scaleY,
78.0934 * scaleX,
187.222 * scaleY,
99.8242 * scaleX,
187.064 * scaleY,
)
..cubicTo(
121.556 * scaleX,
186.906 * scaleY,
143.101 * scaleX,
185.4 * scaleY,
159.525 * scaleX,
182.622 * scaleY,
)
..cubicTo(
167.745 * scaleX,
181.232 * scaleY,
174.627 * scaleX,
179.531 * scaleY,
179.594 * scaleX,
177.548 * scaleY,
)
..cubicTo(
182.079 * scaleX,
176.556 * scaleY,
184.034 * scaleX,
175.512 * scaleY,
185.429 * scaleX,
174.437 * scaleY,
)
..cubicTo(
186.83 * scaleX,
173.355 * scaleY,
187.568 * scaleX,
172.319 * scaleY,
187.812 * scaleX,
171.361 * scaleY,
)
..lineTo(
187.812 * scaleX,
171.361 * scaleY,
) // In SVG, this was H (horizontal line), equivalent to lineTo in Flutter
..cubicTo(
189.156 * scaleX,
166.074 * scaleY,
190.148 * scaleX,
155.525 * scaleY,
190.773 * scaleX,
142.157 * scaleY,
)
..cubicTo(
191.396 * scaleX,
128.832 * scaleY,
191.651 * scaleX,
112.822 * scaleY,
191.552 * scaleX,
96.6875 * scaleY,
)
..cubicTo(
191.453 * scaleX,
80.5539 * scaleY,
191.001 * scaleX,
64.3091 * scaleY,
190.213 * scaleX,
50.5156 * scaleY,
)
..cubicTo(
189.423 * scaleX,
36.6928 * scaleY,
188.299 * scaleX,
25.4153 * scaleY,
186.876 * scaleX,
19.167 * scaleY,
)
..cubicTo(
186.404 * scaleX,
17.0929 * scaleY,
185.566 * scaleX,
15.3424 * scaleY,
184.087 * scaleX,
14.1582 * scaleY,
)
..cubicTo(
181.343 * scaleX,
11.9613 * scaleY,
176.72 * scaleX,
9.98089 * scaleY,
170.561 * scaleX,
8.27539 * scaleY,
)
..cubicTo(
164.434 * scaleX,
6.579 * scaleY,
156.914 * scaleX,
5.18731 * scaleY,
148.483 * scaleX,
4.10254 * scaleY,
)
..close(); // Closes the path to form a complete shape.
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return this != oldClipper;
}
}

76
lib/core/widgets/video/my_video_player.dart

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart';
import 'package:hadi_hoda_flutter/core/constants/my_constants.dart';
import 'package:hadi_hoda_flutter/core/services/audio_service.dart';
import 'package:hadi_hoda_flutter/core/utils/my_device.dart';
import 'package:hadi_hoda_flutter/init_bindings.dart';
import 'package:pod_player/pod_player.dart';
class MyVideoPlayer extends StatefulWidget {
const MyVideoPlayer({super.key, required this.videoURL});
final String? videoURL;
@override
State<MyVideoPlayer> createState() => _MyVideoPlayerState();
}
class _MyVideoPlayerState extends State<MyVideoPlayer> {
late final PodPlayerController _controller;
final AudioService _mainAudioService = locator(
instanceName: MyConstants.mainAudioService,
);
final AudioService _effectAudioService = locator(
instanceName: MyConstants.effectAudioService,
);
@override
void initState() {
super.initState();
_mainAudioService.stop();
_effectAudioService.stop();
_controller = PodPlayerController(
podPlayerConfig: PodPlayerConfig(
autoPlay: false,
isLooping: false,
wakelockEnabled: true,
),
playVideoFrom: PlayVideoFrom.network(widget.videoURL ?? ''),
)..initialise();
}
@override
void dispose() {
_controller.dispose();
_mainAudioService.play();
_effectAudioService.play();
MyDevice.setPortrait();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MyColors.black,
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: MyColors.transparent,
foregroundColor: MyColors.white,
),
body: PodVideoPlayer(
controller: _controller,
matchVideoAspectRatioToFrame: true,
matchFrameAspectRatioToVideo: true,
videoAspectRatio: _controller.videoPlayerValue?.aspectRatio ?? 16 / 9,
podProgressBarConfig: PodProgressBarConfig(),
onToggleFullScreen: (isFullScreen) async {
if (isFullScreen) {
await MyDevice.setAllOrientations();
} else {
await MyDevice.setPortrait();
}
},
),
);
}
}

12
lib/features/download/data/datasource/download_datasource.dart

@ -10,8 +10,8 @@ import 'package:hadi_hoda_flutter/core/response/base_response.dart';
import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; import 'package:hadi_hoda_flutter/core/utils/local_storage.dart';
import 'package:hadi_hoda_flutter/core/utils/storage_path.dart'; import 'package:hadi_hoda_flutter/core/utils/storage_path.dart';
import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart';
import 'package:hadi_hoda_flutter/features/level/data/model/level_model.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/data/model/node_model.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -146,11 +146,11 @@ class DownloadDatasourceImpl implements IDownloadDatasource {
path: MyApi.levels, path: MyApi.levels,
queryParameters: {'lang': selectedLanguage}, queryParameters: {'lang': selectedLanguage},
); );
final List<LevelEntity> levels = BaseResponse.getDataList<LevelEntity>(
response?['result'],
(json) => LevelModel.fromJson(json),
final List<NodeEntity> levels = BaseResponse.getDataList<NodeEntity>(
response?['path'],
(json) => NodeModel.fromJson(json),
); );
await data.add(TotalDataEntity(code: selectedLanguage, levels: levels));
await data.add(TotalDataEntity(code: selectedLanguage, nodes: levels));
} }
} }

2
lib/features/guider/data/datasource/guider_datasource.dart

@ -24,7 +24,7 @@ class GuiderDatasourceImpl implements IGuiderDatasource {
(e) => e.code == selectedLanguage, (e) => e.code == selectedLanguage,
orElse: () => TotalDataEntity(), orElse: () => TotalDataEntity(),
); );
final LevelEntity? findLevel = findData.levels?.first;
final LevelEntity? findLevel = findData.nodes?.first.level;
return findLevel ?? LevelEntity(); return findLevel ?? LevelEntity();
} catch (e) { } catch (e) {
throw MyException(errorMessage: '$e'); throw MyException(errorMessage: '$e');

2
lib/features/home/presentation/bloc/home_bloc.dart

@ -42,7 +42,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
(e) => e.code == selectedLanguage, (e) => e.code == selectedLanguage,
orElse: () => TotalDataEntity(), orElse: () => TotalDataEntity(),
); );
if (findData.levels?.isNotEmpty ?? false) {
if (findData.nodes?.isNotEmpty ?? false) {
context.goNamed(Routes.levelPage); context.goNamed(Routes.levelPage);
} else { } else {
context.goNamed(Routes.downloadPage); context.goNamed(Routes.downloadPage);

8
lib/features/level/data/datasource/level_datasource.dart

@ -2,12 +2,12 @@ import 'package:hadi_hoda_flutter/core/constants/my_constants.dart';
import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart';
import 'package:hadi_hoda_flutter/core/params/level_params.dart'; import 'package:hadi_hoda_flutter/core/params/level_params.dart';
import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; import 'package:hadi_hoda_flutter/core/utils/local_storage.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
abstract class ILevelDatasource { abstract class ILevelDatasource {
Future<List<LevelEntity>> getLevels({required LevelParams params});
Future<List<NodeEntity>> getLevels({required LevelParams params});
} }
/// Local /// Local
@ -15,7 +15,7 @@ class LocalLevelDatasourceImpl implements ILevelDatasource {
const LocalLevelDatasourceImpl(); const LocalLevelDatasourceImpl();
@override @override
Future<List<LevelEntity>> getLevels({required LevelParams params}) async {
Future<List<NodeEntity>> getLevels({required LevelParams params}) async {
try { try {
final String selectedLanguage = LocalStorage.readData( final String selectedLanguage = LocalStorage.readData(
key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage;
@ -24,7 +24,7 @@ class LocalLevelDatasourceImpl implements ILevelDatasource {
(e) => e.code == selectedLanguage, (e) => e.code == selectedLanguage,
orElse: () => TotalDataEntity(), orElse: () => TotalDataEntity(),
); );
return findData.levels ?? [];
return findData.nodes ?? [];
} catch (_) { } catch (_) {
throw MyException(errorMessage: 'Operation Failed'); throw MyException(errorMessage: 'Operation Failed');
} }

25
lib/features/level/data/model/node_model.dart

@ -0,0 +1,25 @@
import 'package:hadi_hoda_flutter/features/level/data/model/level_model.dart';
import 'package:hadi_hoda_flutter/features/level/data/model/prize_model.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
class NodeModel extends NodeEntity {
NodeModel({super.nodeType, super.level, super.prize});
factory NodeModel.fromJson(Map<String, dynamic> json) {
return NodeModel(
nodeType: json['node_type'] == null
? null
: NodeType.fromJson[json['node_type']],
level: json['node_type'] == null
? null
: json['node_type'] == 'level'
? LevelModel.fromJson(json['data'])
: null,
prize: json['node_type'] == null
? null
: json['node_type'] == 'prize'
? PrizeModel.fromJson(json['data'])
: null,
);
}
}

21
lib/features/level/data/model/prize_model.dart

@ -0,0 +1,21 @@
import 'package:hadi_hoda_flutter/features/level/domain/entity/prize_entity.dart';
class PrizeModel extends PrizeEntity {
PrizeModel({
super.id,
super.afterLevel,
super.title,
super.imageURL,
super.animationURL,
});
factory PrizeModel.fromJson(Map<String, dynamic> json) {
return PrizeModel(
id: json['id'],
afterLevel: json['after_level'],
title: json['title'],
imageURL: json['image_url'],
animationURL: json['animation_url'],
);
}
}

6
lib/features/level/data/repository_impl/level_repository_impl.dart

@ -3,7 +3,7 @@ import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart';
import 'package:hadi_hoda_flutter/core/params/level_params.dart'; import 'package:hadi_hoda_flutter/core/params/level_params.dart';
import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; import 'package:hadi_hoda_flutter/core/utils/data_state.dart';
import 'package:hadi_hoda_flutter/features/level/data/datasource/level_datasource.dart'; import 'package:hadi_hoda_flutter/features/level/data/datasource/level_datasource.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/repository/level_repository.dart'; import 'package:hadi_hoda_flutter/features/level/domain/repository/level_repository.dart';
class LevelRepositoryImpl implements ILevelRepository { class LevelRepositoryImpl implements ILevelRepository {
@ -12,11 +12,11 @@ class LevelRepositoryImpl implements ILevelRepository {
const LevelRepositoryImpl(this.datasource); const LevelRepositoryImpl(this.datasource);
@override @override
Future<DataState<List<LevelEntity>, MyException>> getLevels({
Future<DataState<List<NodeEntity>, MyException>> getLevels({
required LevelParams params, required LevelParams params,
}) async { }) async {
try { try {
final List<LevelEntity> response = await datasource.getLevels(
final List<NodeEntity> response = await datasource.getLevels(
params: params, params: params,
); );
return DataState.success(response); return DataState.success(response);

30
lib/features/level/domain/entity/node_entity.dart

@ -0,0 +1,30 @@
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/prize_entity.dart';
import 'package:hive/hive.dart';
part 'node_entity.g.dart';
@HiveType(typeId: 8)
enum NodeType {
@HiveField(0)
level,
@HiveField(1)
prize;
static Map<String, NodeType> get fromJson => {
'level': NodeType.level,
'prize': NodeType.prize,
};
}
@HiveType(typeId: 7)
class NodeEntity extends HiveObject {
@HiveField(0)
NodeType? nodeType;
@HiveField(1)
LevelEntity? level;
@HiveField(2)
PrizeEntity? prize;
NodeEntity({this.nodeType, this.level, this.prize});
}

25
lib/features/level/domain/entity/prize_entity.dart

@ -0,0 +1,25 @@
import 'package:hive/hive.dart';
part 'prize_entity.g.dart';
@HiveType(typeId: 6)
class PrizeEntity extends HiveObject {
@HiveField(0)
int? id;
@HiveField(1)
int? afterLevel;
@HiveField(2)
String? title;
@HiveField(3)
String? imageURL;
@HiveField(4)
String? animationURL;
PrizeEntity({
this.id,
this.afterLevel,
this.title,
this.imageURL,
this.animationURL,
});
}

6
lib/features/level/domain/entity/total_data_entity.dart

@ -1,4 +1,4 @@
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
part 'total_data_entity.g.dart'; part 'total_data_entity.g.dart';
@ -8,10 +8,10 @@ class TotalDataEntity extends HiveObject{
@HiveField(0) @HiveField(0)
String? code; String? code;
@HiveField(1) @HiveField(1)
List<LevelEntity>? levels;
List<NodeEntity>? nodes;
TotalDataEntity({ TotalDataEntity({
this.code, this.code,
this.levels,
this.nodes,
}); });
} }

4
lib/features/level/domain/entity/total_data_entity.g.dart

@ -18,7 +18,7 @@ class TotalDataEntityAdapter extends TypeAdapter<TotalDataEntity> {
}; };
return TotalDataEntity( return TotalDataEntity(
code: fields[0] as String?, code: fields[0] as String?,
levels: (fields[1] as List?)?.cast<LevelEntity>(),
nodes: (fields[1] as List?)?.cast<NodeEntity>(),
); );
} }
@ -29,7 +29,7 @@ class TotalDataEntityAdapter extends TypeAdapter<TotalDataEntity> {
..writeByte(0) ..writeByte(0)
..write(obj.code) ..write(obj.code)
..writeByte(1) ..writeByte(1)
..write(obj.levels);
..write(obj.nodes);
} }
@override @override

6
lib/features/level/domain/repository/level_repository.dart

@ -1,8 +1,10 @@
import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart';
import 'package:hadi_hoda_flutter/core/params/level_params.dart'; import 'package:hadi_hoda_flutter/core/params/level_params.dart';
import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; import 'package:hadi_hoda_flutter/core/utils/data_state.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
abstract class ILevelRepository { abstract class ILevelRepository {
Future<DataState<List<LevelEntity>, MyException>> getLevels({required LevelParams params});
Future<DataState<List<NodeEntity>, MyException>> getLevels({
required LevelParams params,
});
} }

6
lib/features/level/domain/usecases/get_levels_usecase.dart

@ -2,16 +2,16 @@ import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart';
import 'package:hadi_hoda_flutter/core/params/level_params.dart'; import 'package:hadi_hoda_flutter/core/params/level_params.dart';
import 'package:hadi_hoda_flutter/core/usecase/usecase.dart'; import 'package:hadi_hoda_flutter/core/usecase/usecase.dart';
import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; import 'package:hadi_hoda_flutter/core/utils/data_state.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/repository/level_repository.dart'; import 'package:hadi_hoda_flutter/features/level/domain/repository/level_repository.dart';
class GetLevelsUseCase implements UseCase<List<LevelEntity>, LevelParams> {
class GetLevelsUseCase implements UseCase<List<NodeEntity>, LevelParams> {
final ILevelRepository repository; final ILevelRepository repository;
const GetLevelsUseCase(this.repository); const GetLevelsUseCase(this.repository);
@override @override
Future<DataState<List<LevelEntity>, MyException>> call(LevelParams params) {
Future<DataState<List<NodeEntity>, MyException>> call(LevelParams params) {
return repository.getLevels(params: params); return repository.getLevels(params: params);
} }
} }

29
lib/features/level/presentation/bloc/level_bloc.dart

@ -13,12 +13,15 @@ import 'package:hadi_hoda_flutter/core/utils/local_storage.dart';
import 'package:hadi_hoda_flutter/core/utils/my_context.dart'; import 'package:hadi_hoda_flutter/core/utils/my_context.dart';
import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; import 'package:hadi_hoda_flutter/core/utils/screen_size.dart';
import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart';
import 'package:hadi_hoda_flutter/core/widgets/dialog/reward_dialog.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_location.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_location.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/prize_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/usecases/get_levels_usecase.dart'; import 'package:hadi_hoda_flutter/features/level/domain/usecases/get_levels_usecase.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_event.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_event.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_state.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_state.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/level_widget.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/node_widget.dart';
class LevelBloc extends Bloc<LevelEvent, LevelState> { class LevelBloc extends Bloc<LevelEvent, LevelState> {
/// ------------constructor------------ /// ------------constructor------------
@ -174,7 +177,7 @@ class LevelBloc extends Bloc<LevelEvent, LevelState> {
]; ];
final List<LevelEntity> levelList = [];
final List<NodeEntity> nodeList = [];
late final Stream<double> volumeStream; late final Stream<double> volumeStream;
@ -221,6 +224,14 @@ class LevelBloc extends Bloc<LevelEvent, LevelState> {
} }
} }
bool getReward(int index) {
final int currentLevel = int.parse(
LocalStorage.readData(key: MyConstants.currentLevel) ?? '1',
);
return currentLevel > index;
}
Future<void> changeMute() async { Future<void> changeMute() async {
await Future.wait([ await Future.wait([
_mainAudioService.changeMute(), _mainAudioService.changeMute(),
@ -235,7 +246,15 @@ class LevelBloc extends Bloc<LevelEvent, LevelState> {
return currentLevel - 1; return currentLevel - 1;
} }
/// ------------Api Calls------------
void showReward({
required BuildContext context,
required PrizeEntity prize,
}) {
showRewardDialog(context: context, prize: prize);
}
/// ------------Event Calls------------
FutureOr<void> _getLevelListEvent(GetLevelListEvent event, FutureOr<void> _getLevelListEvent(GetLevelListEvent event,
Emitter<LevelState> emit) async { Emitter<LevelState> emit) async {
final int currentLevel = int.parse( final int currentLevel = int.parse(
@ -244,11 +263,11 @@ class LevelBloc extends Bloc<LevelEvent, LevelState> {
await _getLeveslUseCase(LevelParams()).then((value) { await _getLeveslUseCase(LevelParams()).then((value) {
value.fold( value.fold(
(data) async { (data) async {
levelList.addAll(data);
nodeList.addAll(data);
try { try {
emit(state.copyWith( emit(state.copyWith(
getLevelStatus: const BaseComplete(''), getLevelStatus: const BaseComplete(''),
chooseLevel: data.singleWhere((e) => e.order == currentLevel),
chooseLevel: data.singleWhere((e) => e.level?.order == currentLevel).level,
)); ));
} catch (e) { } catch (e) {
emit(state.copyWith( emit(state.copyWith(

2
lib/features/level/presentation/bloc/level_event.dart

@ -1,5 +1,5 @@
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/level_widget.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/node_widget.dart';
sealed class LevelEvent { sealed class LevelEvent {
const LevelEvent(); const LevelEvent();

17
lib/features/level/presentation/ui/level_page.dart

@ -16,7 +16,7 @@ import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_event.d
import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_state.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_state.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/diamond_level.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/diamond_level.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/level_path.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/level_path.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/level_widget.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/node_widget.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/play_button.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/play_button.dart';
class LevelPage extends StatelessWidget { class LevelPage extends StatelessWidget {
@ -208,7 +208,7 @@ class LevelPage extends StatelessWidget {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
...List.generate( ...List.generate(
context.read<LevelBloc>().levelList.length,
context.read<LevelBloc>().nodeList.length,
(index) => Positioned( (index) => Positioned(
top: context.read<LevelBloc>().locationList[index].top, top: context.read<LevelBloc>().locationList[index].top,
bottom: context.read<LevelBloc>().locationList[index].bottom, bottom: context.read<LevelBloc>().locationList[index].bottom,
@ -217,10 +217,17 @@ class LevelPage extends StatelessWidget {
child: BlocBuilder<LevelBloc, LevelState>( child: BlocBuilder<LevelBloc, LevelState>(
buildWhen: (previous, current) => buildWhen: (previous, current) =>
previous.chooseLevel?.id != current.chooseLevel?.id, previous.chooseLevel?.id != current.chooseLevel?.id,
builder: (context, state) => LevelWidget(
builder: (context, state) => NodeWidget(
chooseLevel: state.chooseLevel, chooseLevel: state.chooseLevel,
level: context.read<LevelBloc>().levelList[index],
type: context.read<LevelBloc>().getLevelType(index + 1),
node: context.read<LevelBloc>().nodeList[index],
type: context.read<LevelBloc>().getLevelType,
getReward: context.read<LevelBloc>().getReward,
onRewardPressed: (prize) {
context.read<LevelBloc>().showReward(
context: context,
prize: prize,
);
},
onTap: (LevelEntity level, LevelType type) { onTap: (LevelEntity level, LevelType type) {
context.read<LevelBloc>().add( context.read<LevelBloc>().add(
ChooseLevelEvent(level, type), ChooseLevelEvent(level, type),

121
lib/features/level/presentation/ui/widgets/level_widget.dart

@ -1,121 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart';
import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart';
import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
enum LevelType {
unFinished,
finished,
current;
static Map<LevelType, String> get image => {
LevelType.unFinished: MyAssets.level,
LevelType.finished: MyAssets.finishedLevel,
LevelType.current: MyAssets.currentLevel,
};
static Map<LevelType, Color> get textShadowColor => {
LevelType.unFinished: Color(0XFF5B5B5B),
LevelType.finished: Color(0XFF096D7B),
LevelType.current: Color(0XFF91500D),
};
static Map<LevelType, Color> get textColor => {
LevelType.unFinished: Color(0XFFEDEDED),
LevelType.finished: Color(0XFFFFF2D0),
LevelType.current: Color(0XFFFFF2D0),
};
}
class LevelWidget extends StatelessWidget {
const LevelWidget({
super.key,
required this.level,
required this.type,
required this.chooseLevel,
this.onTap,
});
final LevelType type;
final LevelEntity level;
final LevelEntity? chooseLevel;
final Function(LevelEntity level, LevelType type)? onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => onTap?.call(level, type),
child: Stack(
alignment: Alignment.topCenter,
clipBehavior: Clip.none,
children: [
MyImage(
image: LevelType.image[type] ?? MyAssets.level,
fit: BoxFit.cover,
size: setSize(context: context, tablet: 70, mobile: 44),
),
ShaderMask(
blendMode: BlendMode.modulate,
shaderCallback: (bounds) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0XFFFFFFFF),
LevelType.textColor[type] ?? MyColors.white,
],
).createShader(bounds),
child: Text(
'${level.order ?? 0}',
maxLines: 1,
style: MYTextStyle.button1.copyWith(
fontSize: setSize(context: context, mobile: 24, tablet: 34),
shadows: [
BoxShadow(
color: LevelType.textShadowColor[type] ?? MyColors.white,
offset: Offset(0, 2.97),
)
],
),
),
),
if(level.id == chooseLevel?.id)
Positioned(
top: setSize(context: context, mobile: -20, tablet: -30),
child: MyImage(
image: MyAssets.location,
size: setSize(context: context,mobile: 26, tablet: 40),
),
),
if(type == LevelType.finished)
Positioned(
bottom: 0,
child: Container(
height: setSize(context: context, mobile: 17, tablet: 24),
width: setSize(context: context, mobile: 17, tablet: 24),
padding: EdgeInsets.all(3),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 1,
color: Color(0XFF3CFF3C),
),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0XFF48D336),
Color(0XFF2D7C23),
],
),
),
child: MyImage(image: MyAssets.doneRounded),
),
),
],
),
);
}
}

161
lib/features/level/presentation/ui/widgets/node_widget.dart

@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart';
import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart';
import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart';
import 'package:hadi_hoda_flutter/core/widgets/inkwell/my_inkwell.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/prize_entity.dart';
enum LevelType {
unFinished,
finished,
current;
static Map<LevelType, String> get image => {
LevelType.unFinished: MyAssets.level,
LevelType.finished: MyAssets.finishedLevel,
LevelType.current: MyAssets.currentLevel,
};
static Map<LevelType, Color> get textShadowColor => {
LevelType.unFinished: Color(0XFF5B5B5B),
LevelType.finished: Color(0XFF096D7B),
LevelType.current: Color(0XFF91500D),
};
static Map<LevelType, Color> get textColor => {
LevelType.unFinished: Color(0XFFEDEDED),
LevelType.finished: Color(0XFFFFF2D0),
LevelType.current: Color(0XFFFFF2D0),
};
}
class NodeWidget extends StatelessWidget {
const NodeWidget({
super.key,
required this.node,
required this.getReward,
required this.type,
required this.chooseLevel,
this.onTap,
this.onRewardPressed,
});
final LevelType Function(int index) type;
final bool Function(int index) getReward;
final NodeEntity node;
final LevelEntity? chooseLevel;
final Function(LevelEntity level, LevelType type)? onTap;
final void Function(PrizeEntity prize)? onRewardPressed;
@override
Widget build(BuildContext context) {
return Builder(
builder: (context) {
if (node.nodeType == NodeType.prize) {
return MyInkwell(
onTap: () {
if (getReward(node.prize?.afterLevel ?? 1)) {
onRewardPressed?.call(node.prize ?? PrizeEntity());
}
},
child: Stack(
alignment: Alignment.center,
children: [
if (getReward(node.prize?.afterLevel ?? 1)) ...{
MyImage(image: MyAssets.giftBackground, size: 70),
MyImage(image: MyAssets.gift, size: 50),
} else ...{
MyImage(image: MyAssets.giftDisable, size: 50),
},
],
),
);
} else {
return InkWell(
onTap: () => onTap?.call(
node.level ?? LevelEntity(),
type(node.level?.order ?? 1),
),
child: Stack(
alignment: Alignment.topCenter,
clipBehavior: Clip.none,
children: [
MyImage(
image:
LevelType.image[type(node.level?.order ?? 1)] ??
MyAssets.level,
fit: BoxFit.cover,
size: setSize(context: context, tablet: 70, mobile: 44),
),
ShaderMask(
blendMode: BlendMode.modulate,
shaderCallback: (bounds) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0XFFFFFFFF),
LevelType.textColor[type(node.level?.order ?? 1)] ??
MyColors.white,
],
).createShader(bounds),
child: Text(
'${node.level?.order ?? 0}',
maxLines: 1,
style: MYTextStyle.button1.copyWith(
fontSize: setSize(
context: context,
mobile: 24,
tablet: 34,
),
shadows: [
BoxShadow(
color:
LevelType.textShadowColor[type(
node.level?.order ?? 1,
)] ??
MyColors.white,
offset: Offset(0, 2.97),
),
],
),
),
),
if (node.level?.id == chooseLevel?.id)
Positioned(
top: setSize(context: context, mobile: -20, tablet: -30),
child: MyImage(
image: MyAssets.location,
size: setSize(context: context, mobile: 26, tablet: 40),
),
),
if (type(node.level?.order ?? 1) == LevelType.finished)
Positioned(
bottom: 0,
child: Container(
height: setSize(context: context, mobile: 17, tablet: 24),
width: setSize(context: context, mobile: 17, tablet: 24),
padding: EdgeInsets.all(3),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(width: 1, color: Color(0XFF3CFF3C)),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0XFF48D336), Color(0XFF2D7C23)],
),
),
child: MyImage(image: MyAssets.doneRounded),
),
),
],
),
);
}
},
);
}
}

19
lib/features/question/data/datasource/question_datasource.dart

@ -3,6 +3,7 @@ import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart';
import 'package:hadi_hoda_flutter/core/params/question_params.dart'; import 'package:hadi_hoda_flutter/core/params/question_params.dart';
import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; import 'package:hadi_hoda_flutter/core/utils/local_storage.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -25,11 +26,11 @@ class QuestionDatasourceImpl implements IQuestionDatasource {
(e) => e.code == selectedLanguage, (e) => e.code == selectedLanguage,
orElse: () => TotalDataEntity(), orElse: () => TotalDataEntity(),
); );
final LevelEntity? findLevel = findData.levels?.singleWhere(
(e) => e.id == params.id,
orElse: () => LevelEntity(),
final NodeEntity? findLevel = findData.nodes?.singleWhere(
(e) => e.level?.id == params.id,
orElse: () => NodeEntity(),
); );
return findLevel ?? LevelEntity();
return findLevel?.level ?? LevelEntity();
} catch (e) { } catch (e) {
throw MyException(errorMessage: '$e'); throw MyException(errorMessage: '$e');
} }
@ -47,14 +48,14 @@ class QuestionDatasourceImpl implements IQuestionDatasource {
(e) => e.code == selectedLanguage, (e) => e.code == selectedLanguage,
orElse: () => TotalDataEntity(), orElse: () => TotalDataEntity(),
); );
if(index > (findData.levels?.length ?? 0)){
if(index > (findData.nodes?.length ?? 0)){
throw MyException(); throw MyException();
} }
final LevelEntity? findLevel = findData.levels?.singleWhere(
(e) => e.order == index,
orElse: () => LevelEntity(),
final NodeEntity? findLevel = findData.nodes?.singleWhere(
(e) => e.level?.order == index,
orElse: () => NodeEntity(),
); );
return findLevel ?? LevelEntity();
return findLevel?.level ?? LevelEntity();
} catch (e) { } catch (e) {
throw MyException(errorMessage: '$e'); throw MyException(errorMessage: '$e');
} }

5
lib/init_bindings.dart

@ -20,6 +20,8 @@ import 'package:hadi_hoda_flutter/features/guider/domain/usecases/get_first_leve
import 'package:hadi_hoda_flutter/features/level/data/datasource/level_datasource.dart'; import 'package:hadi_hoda_flutter/features/level/data/datasource/level_datasource.dart';
import 'package:hadi_hoda_flutter/features/level/data/repository_impl/level_repository_impl.dart'; import 'package:hadi_hoda_flutter/features/level/data/repository_impl/level_repository_impl.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/prize_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/repository/level_repository.dart'; import 'package:hadi_hoda_flutter/features/level/domain/repository/level_repository.dart';
import 'package:hadi_hoda_flutter/features/level/domain/usecases/get_levels_usecase.dart'; import 'package:hadi_hoda_flutter/features/level/domain/usecases/get_levels_usecase.dart';
@ -92,7 +94,10 @@ Future<void> initDataBase() async {
..registerAdapter<AnswerEntity>(AnswerEntityAdapter()) ..registerAdapter<AnswerEntity>(AnswerEntityAdapter())
..registerAdapter<QuestionEntity>(QuestionEntityAdapter()) ..registerAdapter<QuestionEntity>(QuestionEntityAdapter())
..registerAdapter<HadithEntity>(HadithEntityAdapter()) ..registerAdapter<HadithEntity>(HadithEntityAdapter())
..registerAdapter<PrizeEntity>(PrizeEntityAdapter())
..registerAdapter<LevelEntity>(LevelEntityAdapter()) ..registerAdapter<LevelEntity>(LevelEntityAdapter())
..registerAdapter<NodeType>(NodeTypeAdapter())
..registerAdapter<NodeEntity>(NodeEntityAdapter())
..registerAdapter<TotalDataEntity>(TotalDataEntityAdapter()); ..registerAdapter<TotalDataEntity>(TotalDataEntityAdapter());
await Hive.openBox<TotalDataEntity>(MyConstants.levelBox); await Hive.openBox<TotalDataEntity>(MyConstants.levelBox);

3
lib/l10n/app_ar.arb

@ -35,5 +35,6 @@
"showcase_notif": "سيقوم الراوي بقراءة\nالإجابات والخيارات لك.", "showcase_notif": "سيقوم الراوي بقراءة\nالإجابات والخيارات لك.",
"showcase_stepper": "هنا سترى الأسئلة الخاصة\nبهذه المرحلة للوصول\nإلى الماسة.", "showcase_stepper": "هنا سترى الأسئلة الخاصة\nبهذه المرحلة للوصول\nإلى الماسة.",
"showcase_hadith": "اطّلع على المصادر والأحاديث\nلهذا السؤال", "showcase_hadith": "اطّلع على المصادر والأحاديث\nلهذا السؤال",
"showcase_guide": "هذا دليل سيساعدك\nفي رحلتك."
"showcase_guide": "هذا دليل سيساعدك\nفي رحلتك.",
"reward": "جائزة"
} }

3
lib/l10n/app_de.arb

@ -35,5 +35,6 @@
"showcase_notif": "Der Sprecher wird\ndir die Antwortmöglichkeiten\nvorlesen.", "showcase_notif": "Der Sprecher wird\ndir die Antwortmöglichkeiten\nvorlesen.",
"showcase_stepper": "Hier siehst du die\nFragen für diese\nStufe, um den\nDiamanten zu erreichen.", "showcase_stepper": "Hier siehst du die\nFragen für diese\nStufe, um den\nDiamanten zu erreichen.",
"showcase_hadith": "Quellen und\nHadithe zu dieser\nFrage ansehen.", "showcase_hadith": "Quellen und\nHadithe zu dieser\nFrage ansehen.",
"showcase_guide": "Dies ist eine Anleitung,\ndie dir hilft."
"showcase_guide": "Dies ist eine Anleitung,\ndie dir hilft.",
"reward": "belohnen"
} }

3
lib/l10n/app_en.arb

@ -35,5 +35,6 @@
"showcase_notif": "The announcer will\nread the answers to\nthe options to you.", "showcase_notif": "The announcer will\nread the answers to\nthe options to you.",
"showcase_stepper": "Here you will see the\nquestions for this\nstage to reach the\ndiamond.", "showcase_stepper": "Here you will see the\nquestions for this\nstage to reach the\ndiamond.",
"showcase_hadith": "View sources and\nhadiths for this\nquestion", "showcase_hadith": "View sources and\nhadiths for this\nquestion",
"showcase_guide": "This is a guide that will\nhelp you."
"showcase_guide": "This is a guide that will\nhelp you.",
"reward": "Reward"
} }

3
lib/l10n/app_fr.arb

@ -35,5 +35,6 @@
"showcase_notif": "Le narrateur lira\nles réponses des\noptions pour toi.", "showcase_notif": "Le narrateur lira\nles réponses des\noptions pour toi.",
"showcase_stepper": "Ici, tu verras les\nquestions de cette\nétape pour atteindre\nle diamant.", "showcase_stepper": "Ici, tu verras les\nquestions de cette\nétape pour atteindre\nle diamant.",
"showcase_hadith": "Consulte les sources et\nles hadiths pour cette\nquestion", "showcase_hadith": "Consulte les sources et\nles hadiths pour cette\nquestion",
"showcase_guide": "Ceci est un guide\nqui t’aidera."
"showcase_guide": "Ceci est un guide\nqui t’aidera.",
"reward": "récompense"
} }

6
lib/l10n/app_localizations.dart

@ -321,6 +321,12 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'This is a guide that will\nhelp you.'** /// **'This is a guide that will\nhelp you.'**
String get showcase_guide; String get showcase_guide;
/// No description provided for @reward.
///
/// In en, this message translates to:
/// **'Reward'**
String get reward;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

3
lib/l10n/app_localizations_ar.dart

@ -119,4 +119,7 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get showcase_guide => 'هذا دليل سيساعدك\nفي رحلتك.'; String get showcase_guide => 'هذا دليل سيساعدك\nفي رحلتك.';
@override
String get reward => 'جائزة';
} }

3
lib/l10n/app_localizations_de.dart

@ -126,4 +126,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get showcase_guide => 'Dies ist eine Anleitung,\ndie dir hilft.'; String get showcase_guide => 'Dies ist eine Anleitung,\ndie dir hilft.';
@override
String get reward => 'belohnen';
} }

3
lib/l10n/app_localizations_en.dart

@ -123,4 +123,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get showcase_guide => 'This is a guide that will\nhelp you.'; String get showcase_guide => 'This is a guide that will\nhelp you.';
@override
String get reward => 'Reward';
} }

3
lib/l10n/app_localizations_fr.dart

@ -124,4 +124,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get showcase_guide => 'Ceci est un guide\nqui t’aidera.'; String get showcase_guide => 'Ceci est un guide\nqui t’aidera.';
@override
String get reward => 'récompense';
} }

3
lib/l10n/app_localizations_ru.dart

@ -124,4 +124,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get showcase_guide => 'Это руководство,\nкоторое поможет тебе.'; String get showcase_guide => 'Это руководство,\nкоторое поможет тебе.';
@override
String get reward => 'награда';
} }

3
lib/l10n/app_localizations_tr.dart

@ -123,4 +123,7 @@ class AppLocalizationsTr extends AppLocalizations {
@override @override
String get showcase_guide => 'Bu, sana yardımcı olacak\nbir rehberdir.'; String get showcase_guide => 'Bu, sana yardımcı olacak\nbir rehberdir.';
@override
String get reward => 'ödül';
} }

3
lib/l10n/app_ru.arb

@ -35,5 +35,6 @@
"showcase_notif": "Диктор прочитает\nтебе все варианты\nответов.", "showcase_notif": "Диктор прочитает\nтебе все варианты\nответов.",
"showcase_stepper": "Здесь ты увидишь\nвопросы этого этапа,\nчтобы получить\nалмаз.", "showcase_stepper": "Здесь ты увидишь\nвопросы этого этапа,\nчтобы получить\nалмаз.",
"showcase_hadith": "Просмотри источники и\nхадисы по этому вопросу", "showcase_hadith": "Просмотри источники и\nхадисы по этому вопросу",
"showcase_guide": "Это руководство,\nкоторое поможет тебе."
"showcase_guide": "Это руководство,\nкоторое поможет тебе.",
"reward": "награда"
} }

3
lib/l10n/app_tr.arb

@ -35,5 +35,6 @@
"showcase_notif": "Anlatıcı, seçeneklerin\ncevaplarını sana\nokuyacak.", "showcase_notif": "Anlatıcı, seçeneklerin\ncevaplarını sana\nokuyacak.",
"showcase_stepper": "Burada, elmasa ulaşmak\niçin bu aşamadaki\nsoruları göreceksin.", "showcase_stepper": "Burada, elmasa ulaşmak\niçin bu aşamadaki\nsoruları göreceksin.",
"showcase_hadith": "Bu soruya ait\nkaynakları ve hadisleri\nincele", "showcase_hadith": "Bu soruya ait\nkaynakları ve hadisleri\nincele",
"showcase_guide": "Bu, sana yardımcı olacak\nbir rehberdir."
"showcase_guide": "Bu, sana yardımcı olacak\nbir rehberdir.",
"reward": "ödül"
} }

176
pubspec.lock

@ -145,6 +145,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@ -201,6 +209,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -209,6 +225,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.6" version: "2.3.6"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@ -317,6 +341,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -325,6 +357,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
get:
dependency: transitive
description:
name: get
sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a"
url: "https://pub.dev"
source: hosted
version: "4.7.3"
get_it: get_it:
dependency: "direct main" dependency: "direct main"
description: description:
@ -381,6 +421,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: transitive dependency: transitive
description: description:
@ -557,6 +605,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
package_info_plus:
dependency: transitive
description:
name: package_info_plus
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
url: "https://pub.dev"
source: hosted
version: "9.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -653,6 +717,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pod_player:
dependency: "direct main"
description:
name: pod_player
sha256: "38564f2e0d9b06fa9e4ad5023be2f6fa4cd9f95f70ace2f517a6d899b166a502"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -789,6 +861,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.1" version: "5.0.1"
simple_sparse_list:
dependency: transitive
description:
name: simple_sparse_list
sha256: aa648fd240fa39b49dcd11c19c266990006006de6699a412de485695910fbc1f
url: "https://pub.dev"
source: hosted
version: "0.1.4"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -898,6 +978,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
unicode:
dependency: transitive
description:
name: unicode
sha256: "0d99edbd2e74726bed2e4989713c8bec02e5581628e334d8c88c0271593fb402"
url: "https://pub.dev"
source: hosted
version: "1.1.8"
universal_html:
dependency: transitive
description:
name: universal_html
sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4
url: "https://pub.dev"
source: hosted
version: "2.3.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
version: "2.3.1"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
@ -938,6 +1042,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
video_player:
dependency: transitive
description:
name: video_player
sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: d74b66f283afff135d5be0ceccca2ca74dff7df1e9b1eaca6bd4699875d3ae60
url: "https://pub.dev"
source: hosted
version: "2.8.22"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: e4d33b79a064498c6eb3a6a492b6a5012573d4943c28d566caf1a6c0840fe78d
url: "https://pub.dev"
source: hosted
version: "2.8.8"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
url: "https://pub.dev"
source: hosted
version: "6.6.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -946,6 +1090,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.0.2"
wakelock_plus:
dependency: transitive
description:
name: wakelock_plus
sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
wakelock_plus_platform_interface:
dependency: transitive
description:
name: wakelock_plus_platform_interface
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -978,6 +1138,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -1002,6 +1170,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
youtube_explode_dart:
dependency: transitive
description:
name: youtube_explode_dart
sha256: "947ba05e0c4f050743e480e7bca3575ff6427d86cc898c1a69f5e1d188cdc9e0"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
sdks: sdks:
dart: ">=3.9.2 <4.0.0" dart: ">=3.9.2 <4.0.0"
flutter: ">=3.35.0" flutter: ">=3.35.0"

1
pubspec.yaml

@ -28,6 +28,7 @@ dependencies:
lottie: ^3.3.2 lottie: ^3.3.2
path_drawing: ^1.0.1 path_drawing: ^1.0.1
path_provider: ^2.1.5 path_provider: ^2.1.5
pod_player: ^0.2.2
pretty_dio_logger: ^1.4.0 pretty_dio_logger: ^1.4.0
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
showcaseview: ^5.0.1 showcaseview: ^5.0.1

Loading…
Cancel
Save