From b1cd0e5e237ac6ba11d3e9963f58f71c71e7f983 Mon Sep 17 00:00:00 2001 From: Oleg Date: Tue, 3 Sep 2024 21:15:34 +0300 Subject: [PATCH] add news page --- .gitignore | 4 +- .vscode/launch.json | 34 +++ ios/Podfile.lock | 51 ++++ ios/Runner.xcodeproj/project.pbxproj | 6 +- lib/common/model/dependencies.dart | 2 + lib/common/routing/routes.dart | 10 +- .../data/initialize_dependencies.dart | 8 + .../schedule/widget/schedule_drawer.dart | 13 + lib/feature/tutorials/bloc/tutorial_bloc.dart | 253 ++++++++++++++++++ .../data/tutorial_network_data_provider.dart | 27 ++ .../tutorials/data/tutorial_repository.dart | 25 ++ lib/feature/tutorials/model/tutorial.dart | 52 ++++ .../widget/home_widget_tutorial.dart | 39 +++ .../tutorials/widget/tutorials_page.dart | 222 +++++++++++++++ lib/l10n/app_en.arb | 6 +- lib/l10n/app_ru.arb | 6 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 148 +++++++++- pubspec.yaml | 3 +- 19 files changed, 901 insertions(+), 10 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 lib/feature/tutorials/bloc/tutorial_bloc.dart create mode 100644 lib/feature/tutorials/data/tutorial_network_data_provider.dart create mode 100644 lib/feature/tutorials/data/tutorial_repository.dart create mode 100644 lib/feature/tutorials/model/tutorial.dart create mode 100644 lib/feature/tutorials/widget/home_widget_tutorial.dart create mode 100644 lib/feature/tutorials/widget/tutorials_page.dart diff --git a/.gitignore b/.gitignore index 78431ed..1867a97 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ app.*.map.json /android/app/release # Test coverage report -coverage \ No newline at end of file +coverage + +env.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..87700dc --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "uneconly", + "request": "launch", + "type": "dart", + "args": [ + "--dart-define-from-file=env.json" + ] + }, + { + "name": "uneconly (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile", + "args": [ + "--dart-define-from-file=env.json" + ] + }, + { + "name": "uneconly (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release", + "args": [ + "--dart-define-from-file=env.json" + ] + } + ] +} \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3139cae..6eeb78a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -64,6 +64,20 @@ PODS: - AppMetricaCore (= 5.7.0) - AppMetricaCoreUtils (= 5.7.0) - AppMetricaLog (= 5.7.0) + - div_expressions_resolver (0.4.3): + - DivKit (< 31.0, >= 29.0) + - Flutter + - DivKit (30.15.0): + - DivKit_LayoutKit (= 30.15.0) + - DivKit_Serialization (= 30.15.0) + - VGSL (~> 6.0) + - DivKit_LayoutKit (30.15.0): + - DivKit_LayoutKitInterface (= 30.15.0) + - VGSL (~> 6.0) + - DivKit_LayoutKitInterface (30.15.0): + - VGSL (~> 6.0) + - DivKit_Serialization (30.15.0): + - VGSL (~> 6.0) - Flutter (1.0.0) - home_widget (0.0.1): - Flutter @@ -78,6 +92,9 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS - sqlite3 (3.46.1): - sqlite3/common (= 3.46.1) - sqlite3/common (3.46.1) @@ -98,14 +115,26 @@ PODS: - sqlite3/rtree - url_launcher_ios (0.0.1): - Flutter + - VGSL (6.4.1): + - VGSLFundamentals (= 6.4.1) + - VGSLNetworking (= 6.4.1) + - VGSLUI (= 6.4.1) + - VGSLFundamentals (6.4.1) + - VGSLNetworking (6.4.1): + - VGSLFundamentals (= 6.4.1) + - VGSLUI (= 6.4.1) + - VGSLUI (6.4.1): + - VGSLFundamentals (= 6.4.1) DEPENDENCIES: - appmetrica_plugin (from `.symlinks/plugins/appmetrica_plugin/ios`) + - div_expressions_resolver (from `.symlinks/plugins/div_expressions_resolver/ios`) - Flutter (from `Flutter`) - home_widget (from `.symlinks/plugins/home_widget/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -127,12 +156,22 @@ SPEC REPOS: - AppMetricaProtobufUtils - AppMetricaStorageUtils - AppMetricaWebKit + - DivKit + - DivKit_LayoutKit + - DivKit_LayoutKitInterface + - DivKit_Serialization - KSCrash - sqlite3 + - VGSL + - VGSLFundamentals + - VGSLNetworking + - VGSLUI EXTERNAL SOURCES: appmetrica_plugin: :path: ".symlinks/plugins/appmetrica_plugin/ios" + div_expressions_resolver: + :path: ".symlinks/plugins/div_expressions_resolver/ios" Flutter: :path: Flutter home_widget: @@ -143,6 +182,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" sqlite3_flutter_libs: :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" url_launcher_ios: @@ -166,15 +207,25 @@ SPEC CHECKSUMS: AppMetricaProtobufUtils: 69272f30e19e30d814b7f05cbd1130e888764c8d AppMetricaStorageUtils: d72c866868dce22626441349bcd45eeec43b9e86 AppMetricaWebKit: bf5a05e7ed13857807522639c8984a1c192e88ef + div_expressions_resolver: 4de71072592939bc93c771766e252a21ee07cfd2 + DivKit: b410363a61bfbd42dd75765f78ec868eedc3007a + DivKit_LayoutKit: a7e0441fbfa9ef9aa406b9a492b2b086fc57e5ba + DivKit_LayoutKitInterface: 86f4d1823b42dfeabbd10f1d8cc4be92cee0f8ed + DivKit_Serialization: 8f5d3b3b05f03cb5c3e7f68eb0eb683891089a0f Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 KSCrash: 158a0998f08ae7d4e54ef8a2da62d6e08b46d03a path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqlite3: 19d8c26842078b45fa2deed63c4bbbe0c0e786ce sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + VGSL: f933d558164acfa8df87e1365fa47b069871761c + VGSLFundamentals: a957918fb54f377e1f75b4e46923a8f4b8db9b3b + VGSLNetworking: 79a991a7695839a2c80715c7bc5a06327a1a0259 + VGSLUI: 71495727b634be4042a805eb52ce2e0297d02812 PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c0e5d20..43d45b2 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -473,7 +473,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -670,7 +670,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -719,7 +719,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/common/model/dependencies.dart b/lib/common/model/dependencies.dart index 76978cb..b3eb5ae 100644 --- a/lib/common/model/dependencies.dart +++ b/lib/common/model/dependencies.dart @@ -5,6 +5,7 @@ import 'package:uneconly/common/database/database.dart'; import 'package:uneconly/common/logging/logging_repository.dart'; import 'package:uneconly/feature/initialization/widget/inherited_dependencies.dart'; import 'package:uneconly/feature/settings/data/settings_repository.dart'; +import 'package:uneconly/feature/tutorials/data/tutorial_repository.dart'; class Dependencies { Dependencies(); @@ -14,6 +15,7 @@ class Dependencies { late final ISettingsRepository settingsRepository; late final MyDatabase database; late final Dio dio; + late final ITutorialRepository tutorialRepository; factory Dependencies.of(BuildContext context) => InheritedDependencies.of(context); diff --git a/lib/common/routing/routes.dart b/lib/common/routing/routes.dart index 71d03ff..48ef994 100644 --- a/lib/common/routing/routes.dart +++ b/lib/common/routing/routes.dart @@ -6,13 +6,17 @@ import 'package:uneconly/feature/schedule/widget/home_page.dart'; import 'package:uneconly/feature/schedule/widget/schedule_page.dart'; import 'package:uneconly/feature/select/widget/select_page.dart'; import 'package:uneconly/feature/settings/widget/settings_page.dart'; +import 'package:uneconly/feature/tutorials/widget/home_widget_tutorial.dart'; +import 'package:uneconly/feature/tutorials/widget/tutorials_page.dart'; enum Routes with OctopusRoute { loading('loading', title: 'Loading'), schedule('schedule', title: 'Schedule'), select('select', title: 'Select'), settings('settings', title: 'Settings'), - home('home', title: 'Home'); + home('home', title: 'Home'), + tutorials('tutorials', title: 'Tutorials'), + homeWidgetTutorial('homeWidgetTutorial', title: 'Home Widget Tutorial'); const Routes(this.name, {this.title}); @@ -49,6 +53,10 @@ enum Routes with OctopusRoute { return const SettingsPage(); case Routes.home: return const HomePage(); + case Routes.tutorials: + return const TutorialsPage(); + case Routes.homeWidgetTutorial: + return const HomeWidgetTutorial(); } } } diff --git a/lib/feature/initialization/data/initialize_dependencies.dart b/lib/feature/initialization/data/initialize_dependencies.dart index c1af0fa..967b129 100644 --- a/lib/feature/initialization/data/initialize_dependencies.dart +++ b/lib/feature/initialization/data/initialize_dependencies.dart @@ -10,6 +10,8 @@ import 'package:uneconly/constants.dart'; import 'package:uneconly/feature/initialization/data/platform/platform_initialization.dart'; import 'package:uneconly/feature/settings/data/settings_local_data_provider.dart'; import 'package:uneconly/feature/settings/data/settings_repository.dart'; +import 'package:uneconly/feature/tutorials/data/tutorial_network_data_provider.dart'; +import 'package:uneconly/feature/tutorials/data/tutorial_repository.dart'; /// Initializes the app and returns a [Dependencies] object Future $initializeDependencies({ @@ -74,6 +76,12 @@ Map _getInitializationSteps({ baseUrl: serverAddress, ), ), + 'Initialize tutorial repository': (dependencies) async => + dependencies.tutorialRepository = TutorialRepository( + networkDataProvider: TutorialNetworkDataProvider( + dio: dependencies.dio, + ), + ), 'Log app initialized': (_) {}, }; diff --git a/lib/feature/schedule/widget/schedule_drawer.dart b/lib/feature/schedule/widget/schedule_drawer.dart index 829a7cc..037f5b0 100644 --- a/lib/feature/schedule/widget/schedule_drawer.dart +++ b/lib/feature/schedule/widget/schedule_drawer.dart @@ -77,6 +77,19 @@ class ScheduleDrawer extends StatelessWidget { ); }, ), + // ListTile for news + ListTile( + title: Text( + AppLocalizations.of(context)!.news, + ), + onTap: () { + Octopus.of( + context, + ).push( + Routes.tutorials, + ); + }, + ), // ListTile to view schedule of another group ListTile( title: Text( diff --git a/lib/feature/tutorials/bloc/tutorial_bloc.dart b/lib/feature/tutorials/bloc/tutorial_bloc.dart new file mode 100644 index 0000000..1bc6e8c --- /dev/null +++ b/lib/feature/tutorials/bloc/tutorial_bloc.dart @@ -0,0 +1,253 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart' as bloc_concurrency; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:l/l.dart'; +import 'package:uneconly/feature/tutorials/data/tutorial_repository.dart'; +import 'package:uneconly/feature/tutorials/model/tutorial.dart'; + +part 'tutorial_bloc.freezed.dart'; + +/* Tutorial Events */ + +@freezed +class TutorialEvent with _$TutorialEvent { + const TutorialEvent._(); + + @Implements() + @With<_ProcessingStateEmitter>() + @With<_SuccessfulStateEmitter>() + @With<_ErrorStateEmitter>() + @With<_IdleStateEmitter>() + const factory TutorialEvent.create() = CreateTutorialEvent; + + @Implements() + @With<_ProcessingStateEmitter>() + @With<_SuccessfulStateEmitter>() + @With<_ErrorStateEmitter>() + @With<_IdleStateEmitter>() + const factory TutorialEvent.read() = ReadTutorialEvent; + + @Implements() + @With<_ProcessingStateEmitter>() + @With<_SuccessfulStateEmitter>() + @With<_ErrorStateEmitter>() + @With<_IdleStateEmitter>() + const factory TutorialEvent.update() = UpdateTutorialEvent; + + @Implements() + @With<_ProcessingStateEmitter>() + @With<_SuccessfulStateEmitter>() + @With<_ErrorStateEmitter>() + @With<_IdleStateEmitter>() + const factory TutorialEvent.delete() = DeleteTutorialEvent; +} + +/* Tutorial States */ + +@freezed +class TutorialState with _$TutorialState { + const TutorialState._(); + + /// Is in idle state + bool get idling => !isProcessing; + + /// Is in progress state + bool get isProcessing => maybeMap( + orElse: () => true, + idle: (_) => false, + ); + + /// If an error has occurred + bool get hasError => maybeMap(orElse: () => false, error: (_) => true); + + /// Idling state + const factory TutorialState.idle({ + required final TutorialEntity data, + @Default('Idle') final String message, + }) = IdleTutorialState; + + /// Processing + const factory TutorialState.processing({ + required final TutorialEntity data, + @Default('Processing') final String message, + }) = ProcessingTutorialState; + + /// Successful + const factory TutorialState.successful({ + required final TutorialEntity data, + @Default('Successful') final String message, + }) = SuccessfulTutorialState; + + /// An error has occurred + const factory TutorialState.error({ + required final TutorialEntity data, + @Default('An error has occurred') final String message, + }) = ErrorTutorialState; +} + +/// Buisiness Logic Component TutorialBLoC +class TutorialBLoC extends Bloc + implements EventSink { + TutorialBLoC({ + required final ITutorialRepository repository, + final TutorialState? initialState, + }) : _repository = repository, + super( + initialState ?? + TutorialState.idle( + data: TutorialEntity( + news: {}, + ), + message: 'Initial idle state', + ), + ) { + on( + (event, emit) => event.map>( + create: (event) => _create(event, emit), + read: (event) => _read(event, emit), + update: (event) => _update(event, emit), + delete: (event) => _delete(event, emit), + ), + transformer: bloc_concurrency.sequential(), + //transformer: bloc_concurrency.restartable(), + //transformer: bloc_concurrency.droppable(), + //transformer: bloc_concurrency.concurrent(), + ); + } + + final ITutorialRepository _repository; + + /// Create event handler + Future _create( + CreateTutorialEvent event, Emitter emit) async { + // try { + // emit(event.inProgress(state: state)); + // //final newData = await _repository.(); + // emit(event.successful(state: state, newData: newData)); + // } on Object catch (err, stackTrace) { + // l.e('An error occurred in the TutorialBLoC: $err', stackTrace); + // emit(event.error(state: state, message: 'An error occurred')); + // rethrow; + // } finally { + // emit(event.idle(state: state)); + // } + } + + /// Read event handler + Future _read( + ReadTutorialEvent event, Emitter emit) async { + try { + emit(event.inProgress(state: state)); + final newData = await _repository.fetchNews().timeout( + const Duration(seconds: 5), + onTimeout: () => throw TimeoutException('Timeout'), + ); + emit( + event.successful( + state: state, + newData: state.data.copyWith(news: newData), + ), + ); + } on Object catch (err, stackTrace) { + l.e('An error occurred in the TutorialBLoC: $err', stackTrace); + emit(event.error(state: state, message: 'An error occurred')); + rethrow; + } finally { + emit(event.idle(state: state)); + } + } + + /// Update event handler + Future _update( + UpdateTutorialEvent event, Emitter emit) async { + // try { + // emit(event.inProgress(state: state)); + // final newData = await _repository.(); + // emit(event.successful(state: state, newData: newData)); + // } on Object catch (err, stackTrace) { + // l.e('An error occurred in the TutorialBLoC: $err', stackTrace); + // emit(event.error(state: state, message: 'An error occurred')); + // rethrow; + // } finally { + // emit(event.idle(state: state)); + // } + } + + /// Delete event handler + Future _delete( + DeleteTutorialEvent event, Emitter emit) async { + // try { + // emit(event.inProgress(state: state)); + // //final newData = await _repository.(); + // emit(event.successful(state: state, newData: newData)); + // } on Object catch (err, stackTrace) { + // l.e('An error occurred in the TutorialBLoC: $err', stackTrace); + // emit(event.error(state: state, message: 'An error occurred')); + // rethrow; + // } finally { + // emit(event.idle(state: state)); + // } + } +} + +/* Interfaces for events TutorialEvent */ + +abstract class ITutorialEvent {} + +/* Mixins for events TutorialEvent */ + +/// Creating state "Processing" +mixin _ProcessingStateEmitter on TutorialEvent { + /// Creating state "Processing" + TutorialState inProgress({ + required final TutorialState state, + final String? message, + }) => + TutorialState.processing( + data: state.data, + message: message ?? 'Processing', + ); +} + +/// Creating state "Successful" +mixin _SuccessfulStateEmitter on TutorialEvent { + /// Creating state "Successful" + TutorialState successful({ + required final TutorialState state, + final TutorialEntity? newData, + final String? message, + }) => + TutorialState.successful( + data: newData ?? state.data, + message: message ?? 'Successful', + ); +} + +/// Creating state "Error" +mixin _ErrorStateEmitter on TutorialEvent { + /// An error occurred + TutorialState error({ + required final TutorialState state, + final String? message, + }) => + TutorialState.error( + data: state.data, + message: message ?? 'An error has occurred', + ); +} + +/// Creating state "Idle" +mixin _IdleStateEmitter on TutorialEvent { + /// Creating state "Successful" + /// Idle before getting an event + TutorialState idle({ + required final TutorialState state, + final String? message, + }) => + TutorialState.idle( + data: state.data, + message: message ?? 'Idle', + ); +} diff --git a/lib/feature/tutorials/data/tutorial_network_data_provider.dart b/lib/feature/tutorials/data/tutorial_network_data_provider.dart new file mode 100644 index 0000000..54fc3cf --- /dev/null +++ b/lib/feature/tutorials/data/tutorial_network_data_provider.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart'; +import 'package:l/l.dart'; + +abstract class ITutorialNetworkDataProvider { + Future> fetchNews(); +} + +class TutorialNetworkDataProvider implements ITutorialNetworkDataProvider { + TutorialNetworkDataProvider({ + required final Dio dio, + }) : _dio = dio; + + final Dio _dio; + + @override + Future> fetchNews() async { + try { + final response = await _dio.get('/asset/news'); + + return response.data['content'] as Map; + } on Object catch (e, stackTrace) { + l.e('An error occured in TutorialNetworkDataProvider', stackTrace); + + rethrow; + } + } +} diff --git a/lib/feature/tutorials/data/tutorial_repository.dart b/lib/feature/tutorials/data/tutorial_repository.dart new file mode 100644 index 0000000..579a7cd --- /dev/null +++ b/lib/feature/tutorials/data/tutorial_repository.dart @@ -0,0 +1,25 @@ +import 'package:l/l.dart'; +import 'package:uneconly/feature/tutorials/data/tutorial_network_data_provider.dart'; + +abstract class ITutorialRepository { + Future> fetchNews(); +} + +class TutorialRepository implements ITutorialRepository { + TutorialRepository({ + required final ITutorialNetworkDataProvider networkDataProvider, + }) : _networkDataProvider = networkDataProvider; + + final ITutorialNetworkDataProvider _networkDataProvider; + + @override + Future> fetchNews() async { + try { + return await _networkDataProvider.fetchNews(); + } on Object catch (e, stackTrace) { + l.e('An error occured in TutorialRepository', stackTrace); + + rethrow; + } + } +} diff --git a/lib/feature/tutorials/model/tutorial.dart b/lib/feature/tutorials/model/tutorial.dart new file mode 100644 index 0000000..0dade29 --- /dev/null +++ b/lib/feature/tutorials/model/tutorial.dart @@ -0,0 +1,52 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +class TutorialEntity { + final Map news; + + TutorialEntity({ + required this.news, + }); + + TutorialEntity copyWith({ + Map? news, + }) { + return TutorialEntity( + news: news ?? this.news, + ); + } + + Map toMap() { + return { + 'news': news, + }; + } + + factory TutorialEntity.fromMap(Map map) { + return TutorialEntity( + news: Map.from( + (map['news'] as Map), + ), + ); + } + + String toJson() => json.encode(toMap()); + + factory TutorialEntity.fromJson(String source) => + TutorialEntity.fromMap(json.decode(source) as Map); + + @override + String toString() => 'Tutorial(news: $news)'; + + @override + bool operator ==(covariant TutorialEntity other) { + if (identical(this, other)) return true; + + return mapEquals(other.news, news); + } + + @override + int get hashCode => news.hashCode; +} diff --git a/lib/feature/tutorials/widget/home_widget_tutorial.dart b/lib/feature/tutorials/widget/home_widget_tutorial.dart new file mode 100644 index 0000000..695683d --- /dev/null +++ b/lib/feature/tutorials/widget/home_widget_tutorial.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:uneconly/common/localization/localization.dart'; + +class HomeWidgetTutorial extends StatelessWidget { + const HomeWidgetTutorial({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.homeWidget), + ), + body: Align( + alignment: Alignment.center, + child: Image.network( + const String.fromEnvironment('HOME_WIDGET_TUTORIAL'), + fit: BoxFit.cover, + alignment: Alignment.center, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/feature/tutorials/widget/tutorials_page.dart b/lib/feature/tutorials/widget/tutorials_page.dart new file mode 100644 index 0000000..9cc9945 --- /dev/null +++ b/lib/feature/tutorials/widget/tutorials_page.dart @@ -0,0 +1,222 @@ +import 'dart:async'; + +import 'package:divkit/divkit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:octopus/octopus.dart'; +import 'package:uneconly/common/localization/localization.dart'; +import 'package:uneconly/common/model/dependencies.dart'; +import 'package:uneconly/common/routing/routes.dart'; +import 'package:uneconly/feature/schedule/widget/schedule_page.dart'; +import 'package:uneconly/feature/tutorials/bloc/tutorial_bloc.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// {@template tutorials_page} +/// TutorialsPage widget +/// {@endtemplate} +class TutorialsPage extends StatelessWidget { + /// {@macro tutorials_page} + const TutorialsPage({super.key}); + + Widget _errorWidget(BuildContext context) { + // return error message widget and refresh button + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.error, + ), + ElevatedButton( + onPressed: () { + BlocProvider.of(context).add( + const TutorialEvent.read(), + ); + }, + child: Text( + AppLocalizations.of(context)!.tryAgain, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.news), + ), + body: BlocProvider( + create: (context) => TutorialBLoC( + repository: Dependencies.of(context).tutorialRepository, + )..add( + const TutorialEvent.read(), + ), + child: BlocBuilder( + builder: (context, state) { + if (state is ProcessingTutorialState) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (state is ErrorTutorialState) { + return _errorWidget(context); + } + + if (state.data.news.isEmpty) { + return _errorWidget(context); + } + + return DivKitView( + data: DefaultDivKitData.fromJson( + state.data.news, + ), + actionHandler: MyDivkitActionHandler( + uneconlyUrlHandler: UneconlyUrlHandler(handler: (uri) { + if (uri.host == 'home_widget') { + context.octopus.push( + Routes.homeWidgetTutorial, + ); + } + }), + ), + ); + }, + ), + ), + ); + } +} + +class HttpUrlHandler extends DivActionHandler { + @override + bool canHandle(DivContext context, DivActionModel action) { + final actionUrl = action.url; + + if (actionUrl != null && + ['https', 'http'].any((scheme) => actionUrl.scheme == scheme)) { + return true; + } + + return false; + } + + @override + FutureOr handleAction(DivContext context, DivActionModel action) async { + final actionUrl = action.url; + + if (actionUrl == null) { + return false; + } + + if (!canHandle(context, action)) { + return false; + } + + await launchUrl(actionUrl); + + return true; + } +} + +class UneconlyUrlHandler extends DivActionHandler { + final ValueChanged handler; + + UneconlyUrlHandler({ + required this.handler, + }); + + @override + bool canHandle(DivContext context, DivActionModel action) { + final actionUrl = action.url; + + if (actionUrl != null && + ['uneconly'].any((scheme) => actionUrl.scheme == scheme)) { + return true; + } + + return false; + } + + @override + FutureOr handleAction(DivContext context, DivActionModel action) async { + final actionUrl = action.url; + + if (actionUrl == null) { + return false; + } + + if (!canHandle(context, action)) { + return false; + } + + handler(actionUrl); + + return true; + } +} + +class MyDivkitActionHandler extends DivActionHandler { + final typedHandler = DefaultDivActionHandlerTyped(); + final urlHandler = DefaultDivActionHandlerUrl(); + final httpUrlHandler = HttpUrlHandler(); + final UneconlyUrlHandler uneconlyUrlHandler; + + MyDivkitActionHandler({ + required this.uneconlyUrlHandler, + }); + + @override + bool canHandle(DivContext context, DivActionModel action) { + try { + if (typedHandler.canHandle(context, action)) { + return true; + } + if (httpUrlHandler.canHandle(context, action)) { + return true; + } + if (uneconlyUrlHandler.canHandle(context, action)) { + return true; + } + + return urlHandler.canHandle(context, action); + } catch (e, st) { + logger.error( + '[div-action] Can\'t CHECK action: $action', + error: e, + stackTrace: st, + ); + + return false; + } + } + + @override + FutureOr handleAction(DivContext context, DivActionModel action) async { + try { + if (typedHandler.canHandle(context, action)) { + return typedHandler.handleAction(context, action); + } + if (httpUrlHandler.canHandle(context, action)) { + return httpUrlHandler.handleAction(context, action); + } + if (uneconlyUrlHandler.canHandle(context, action)) { + return uneconlyUrlHandler.handleAction(context, action); + } + + return urlHandler.handleAction(context, action); + } catch (e, st) { + logger.error( + '[div-action] Can\'t HANDLE action: $action', + error: e, + stackTrace: st, + ); + + return false; + } + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 66ddc6e..0e6cdb1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -50,5 +50,9 @@ "share": "Share", "lastWeekOfCurrentStudyYear": "This is the last week of current study year. Next study year schedule should be available soon", "checkOutOfficialWebsiteForPreciseInformation": "Check out official website for precise info", - "openOfficialWebsite": "Open official website" + "openOfficialWebsite": "Open official website", + "news": "News", + "error": "Error", + "tryAgain": "Try again", + "homeWidget": "Home widget" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 12b8a24..be01774 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -47,5 +47,9 @@ "share": "Поделиться", "lastWeekOfCurrentStudyYear": "Это последняя неделя текущего учебного года. Скоро должно появиться расписание для следующего учебного года", "checkOutOfficialWebsiteForPreciseInformation": "Проверяйте точную информацию на официальном веб-сайте", - "openOfficialWebsite": "Открыть официальный вебсайт" + "openOfficialWebsite": "Открыть официальный вебсайт", + "news": "Новости", + "error": "Ошибка", + "tryAgain": "Попробовать снова", + "homeWidget": "Виджет на главном экране" } \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f2356e7..15d7639 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import path_provider_foundation import share_plus import shared_preferences_foundation +import sqflite import sqlite3_flutter_libs import url_launcher_macos @@ -15,6 +16,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 6a48d7b..0eb563d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,6 +153,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.2" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" + url: "https://pub.dev" + source: hosted + version: "1.2.0" characters: dependency: transitive description: @@ -289,6 +313,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.6" + div_expressions_resolver: + dependency: transitive + description: + name: div_expressions_resolver + sha256: "39744d5cff5fddab21a98da177da5c6ebffc8fba8fd15deb1c66910c49180f6e" + url: "https://pub.dev" + source: hosted + version: "0.4.3" + divkit: + dependency: "direct main" + description: + name: divkit + sha256: e86a3f3b599f2fc7bf6a6389bffb14d75d95631288558b87f44a5307a2750264 + url: "https://pub.dev" + source: hosted + version: "0.3.0" drift: dependency: "direct main" description: @@ -305,6 +345,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.15.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -350,6 +398,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" flutter_launcher_icons: dependency: "direct dev" description: @@ -371,6 +427,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + url: "https://pub.dev" + source: hosted + version: "2.0.9" flutter_test: dependency: "direct dev" description: flutter @@ -597,6 +661,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" octopus: dependency: "direct main" description: @@ -621,6 +693,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_provider: dependency: "direct main" description: @@ -765,6 +845,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" share_plus: dependency: "direct main" description: @@ -882,6 +970,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" + url: "https://pub.dev" + source: hosted + version: "2.5.4+2" sqlite3: dependency: transitive description: @@ -938,6 +1042,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 + url: "https://pub.dev" + source: hosted + version: "3.2.0" term_glyph: dependency: transitive description: @@ -1042,6 +1154,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" vector_math: dependency: transitive description: @@ -1050,14 +1186,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1e28fc7..af76189 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.9.4+28 +version: 1.9.6+30 environment: sdk: ">=3.0.0 <4.0.0" @@ -55,6 +55,7 @@ dependencies: share_plus: ^10.0.2 appmetrica_plugin: ^2.1.1 url_launcher: ^6.3.0 + divkit: ^0.3.0 dev_dependencies: flutter_test: