From 4f2a2a4581044c5349f28f54258bb9d6205967eb Mon Sep 17 00:00:00 2001 From: Oleg Date: Sat, 17 Aug 2024 16:37:53 +0300 Subject: [PATCH] add share schedule option --- ios/Podfile.lock | 6 + lib/common/utils/string_utils.dart | 8 ++ lib/feature/schedule/bloc/schedule_bloc.dart | 51 ++++++- .../schedule/domain/schedule_transformer.dart | 57 ++++++++ .../widget/schedule_actions_popup.dart | 57 ++++++++ .../schedule/widget/schedule_page.dart | 136 +++++++++++------- lib/l10n/app_en.arb | 3 +- lib/l10n/app_ru.arb | 3 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 64 +++++++++ pubspec.yaml | 1 + test/string_utils_test.dart | 14 ++ .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 16 files changed, 357 insertions(+), 58 deletions(-) create mode 100644 lib/feature/schedule/domain/schedule_transformer.dart create mode 100644 lib/feature/schedule/widget/schedule_actions_popup.dart create mode 100644 test/string_utils_test.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8cb6707..7c577a3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - share_plus (0.0.1): + - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -31,6 +33,7 @@ DEPENDENCIES: - 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`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) @@ -45,6 +48,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/home_widget/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqlite3_flutter_libs: @@ -54,6 +59,7 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqlite3: 19d8c26842078b45fa2deed63c4bbbe0c0e786ce sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b diff --git a/lib/common/utils/string_utils.dart b/lib/common/utils/string_utils.dart index 14cdabb..3ccce65 100644 --- a/lib/common/utils/string_utils.dart +++ b/lib/common/utils/string_utils.dart @@ -1,3 +1,11 @@ String capitalize(String s) { return "${s[0].toUpperCase()}${s.substring(1).toLowerCase()}"; } + +String trimSeparators(String s) { + final sWithoutNewLines = s.replaceAll('\n', ' '); + + final splitted = sWithoutNewLines.split(' ').where((e) => e.isNotEmpty); + + return splitted.map((e) => e.trim()).join(' '); +} diff --git a/lib/feature/schedule/bloc/schedule_bloc.dart b/lib/feature/schedule/bloc/schedule_bloc.dart index 2d60680..c78450b 100644 --- a/lib/feature/schedule/bloc/schedule_bloc.dart +++ b/lib/feature/schedule/bloc/schedule_bloc.dart @@ -1,11 +1,13 @@ import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:l/l.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart' as bloc_concurrency; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:uneconly/common/model/short_group_info.dart'; import 'package:uneconly/feature/schedule/data/schedule_repository.dart'; +import 'package:uneconly/feature/schedule/domain/schedule_transformer.dart'; import 'package:uneconly/feature/schedule/model/schedule.dart'; import 'package:uneconly/feature/schedule/model/schedule_details.dart'; import 'package:uneconly/feature/select/data/group_repository.dart'; @@ -42,6 +44,10 @@ class ScheduleEvent with _$ScheduleEvent { /// Delete const factory ScheduleEvent.delete({required Schedule item}) = DeleteScheduleEvent; + + /// Share + const factory ScheduleEvent.share(ValueChanged onShare) = + ShareScheduleEvent; } /* Schedule States */ @@ -98,6 +104,22 @@ class ScheduleState with _$ScheduleState { /// Is in progress state bool get isProcessing => maybeMap(orElse: () => true, idle: (_) => false); + + ScheduleDetails? getSelectedScheduleDetails() { + if (selectedWeek == null) { + return null; + } + + return data[selectedWeek]; + } + + ScheduleDetails? getCurrentScheduleDetails() { + if (currentWeek == null) { + return null; + } + + return data[currentWeek]; + } } /// Buisiness Logic Component ScheduleBLoC @@ -122,17 +144,17 @@ class ScheduleBLoC extends Bloc on( (event, emit) => event.map>( fetch: (event) => _fetch(event, emit), - create: (value) { + create: (event) { throw UnimplementedError(); }, - update: (value) { + update: (event) { throw UnimplementedError(); }, - delete: (value) { + delete: (event) { throw UnimplementedError(); }, - changeGroup: (ChangeGroupScheduleEvent event) => - _changeGroup(event, emit), + changeGroup: (event) => _changeGroup(event, emit), + share: (event) => _share(event, emit), ), transformer: bloc_concurrency.sequential(), ); @@ -358,4 +380,23 @@ class ScheduleBLoC extends Bloc ); } } + + Future _share( + ShareScheduleEvent event, + Emitter emit, + ) async { + final selectedScheduleDetails = state.getSelectedScheduleDetails(); + final groupInfo = state.shortGroupInfo; + + if (selectedScheduleDetails == null || groupInfo == null) { + return; + } + + final content = ScheduleTransformer().transformScheduleToString( + selectedScheduleDetails.schedule, + groupInfo, + ); + + event.onShare(content); + } } diff --git a/lib/feature/schedule/domain/schedule_transformer.dart b/lib/feature/schedule/domain/schedule_transformer.dart new file mode 100644 index 0000000..10a684f --- /dev/null +++ b/lib/feature/schedule/domain/schedule_transformer.dart @@ -0,0 +1,57 @@ +import 'package:intl/intl.dart'; +import 'package:uneconly/common/model/short_group_info.dart'; +import 'package:uneconly/common/utils/string_utils.dart'; +import 'package:uneconly/feature/schedule/model/schedule.dart'; + +class ScheduleTransformer { + String transformScheduleToString( + Schedule schedule, + ShortGroupInfo groupInfo, + ) { + const space = ' '; + const newLine = '\n'; + String result = ''; + + final groupName = groupInfo.groupName; + + if (groupName != null) { + result += groupName; + result += newLine; + } + + result += 'Неделя ${schedule.week}'; + result += newLine; + result += newLine; + + if (schedule.daySchedules.isEmpty) { + result += 'Нет расписания на эту неделю'; + result += newLine; + } + + for (final daySchedule in schedule.daySchedules) { + result += DateFormat('dd.MM.yyyy').format(daySchedule.day); + result += space; + result += DateFormat('EEE', 'ru').format(daySchedule.day); + result += newLine; + + if (daySchedule.lessons.isEmpty) { + result += 'Нет пар'; + result += newLine; + } + + for (final lesson in daySchedule.lessons) { + result += + '${DateFormat('HH:mm').format(lesson.start)} - ${DateFormat('HH:mm').format(lesson.end)}'; + result += newLine; + result += lesson.name; + result += newLine; + result += trimSeparators(lesson.location); + result += newLine; + } + + result += newLine; + } + + return result; + } +} diff --git a/lib/feature/schedule/widget/schedule_actions_popup.dart b/lib/feature/schedule/widget/schedule_actions_popup.dart new file mode 100644 index 0000000..a6abab7 --- /dev/null +++ b/lib/feature/schedule/widget/schedule_actions_popup.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +enum ScheduleAction { + share, + favorite; +} + +class ScheduleActionConfig { + final ScheduleAction action; + final VoidCallback onPressed; + final Widget child; + + ScheduleActionConfig( + this.child, { + required this.action, + required this.onPressed, + }); +} + +class ScheduleActionsPopup extends StatelessWidget { + final List actions; + + const ScheduleActionsPopup({ + super.key, + required this.actions, + }); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + popUpAnimationStyle: AnimationStyle( + curve: Curves.easeIn, + duration: const Duration(milliseconds: 300), + ), + offset: const Offset(0, 40), + onSelected: (value) { + for (final action in actions) { + if (action.action == value) { + action.onPressed(); + + return; + } + } + }, + itemBuilder: (context) { + return actions + .map( + (e) => PopupMenuItem( + value: e.action, + child: e.child, + ), + ) + .toList(); + }, + ); + } +} // _SchedulePageState diff --git a/lib/feature/schedule/widget/schedule_page.dart b/lib/feature/schedule/widget/schedule_page.dart index 9980500..66f3a30 100644 --- a/lib/feature/schedule/widget/schedule_page.dart +++ b/lib/feature/schedule/widget/schedule_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:octopus/octopus.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:uneconly/common/localization/localization.dart'; import 'package:uneconly/common/model/dependencies.dart'; import 'package:uneconly/common/model/short_group_info.dart'; @@ -17,6 +18,7 @@ import 'package:uneconly/feature/schedule/data/schedule_network_data_provider.da import 'package:uneconly/feature/schedule/data/schedule_repository.dart'; import 'package:uneconly/feature/schedule/model/schedule.dart'; import 'package:uneconly/feature/schedule/model/schedule_details.dart'; +import 'package:uneconly/feature/schedule/widget/schedule_actions_popup.dart'; import 'package:uneconly/feature/schedule/widget/schedule_widget.dart'; import 'package:uneconly/feature/select/data/group_network_data_provider.dart'; import 'package:uneconly/feature/select/data/group_repository.dart'; @@ -446,6 +448,67 @@ class _SchedulePageState extends State onPageChanged(context, newPage.round(), week); } + void onFavoritePressed( + BuildContext context, + ScheduleState state, + ) { + setState(() { + isFavorite = !isFavorite; + }); + + if (isFavorite) { + Dependencies.of(context).settingsRepository.addGroupToFavorites( + Group( + id: state.shortGroupInfo?.groupId ?? + widget.shortGroupInfo.groupId, + name: state.shortGroupInfo?.groupName ?? + widget.shortGroupInfo.groupName ?? + '', + facultyId: 0, + course: 0, + ), + ); + } else { + Dependencies.of(context).settingsRepository.removeGroupFromFavorites( + Group( + id: state.shortGroupInfo?.groupId ?? + widget.shortGroupInfo.groupId, + name: state.shortGroupInfo?.groupName ?? + widget.shortGroupInfo.groupName ?? + '', + facultyId: 0, + course: 0, + ), + ); + } + } + + void onSharePressed(BuildContext context, ScheduleState state) { + final bloc = context.read(); + + bloc.add( + ScheduleEvent.share( + (content) async { + await Share.share(content); + + // await showDialog( + // context: context, + // builder: (context) { + // return Material( + // child: GestureDetector( + // onTap: () { + // Navigator.of(context).pop(); + // }, + // child: SingleChildScrollView(child: Text(content)), + // ), + // ); + // }, + // ); + }, + ), + ); + } + Widget _buildPageView( BuildContext context, ScheduleState state, @@ -491,58 +554,29 @@ class _SchedulePageState extends State : null, appBar: AppBar( title: Text(title), + centerTitle: true, actions: [ - if (widget.isViewMode) - Padding( - padding: const EdgeInsets.only( - right: 10, - ), - child: IconButton( - onPressed: () { - setState(() { - isFavorite = !isFavorite; - }); - - if (isFavorite) { - Dependencies.of(context) - .settingsRepository - .addGroupToFavorites( - Group( - id: state.shortGroupInfo?.groupId ?? - widget.shortGroupInfo.groupId, - name: state.shortGroupInfo?.groupName ?? - widget.shortGroupInfo.groupName ?? - '', - facultyId: 0, - course: 0, - ), - ); - } else { - Dependencies.of(context) - .settingsRepository - .removeGroupFromFavorites( - Group( - id: state.shortGroupInfo?.groupId ?? - widget.shortGroupInfo.groupId, - name: state.shortGroupInfo?.groupName ?? - widget.shortGroupInfo.groupName ?? - '', - facultyId: 0, - course: 0, - ), - ); - } - }, - tooltip: isFavorite - ? AppLocalizations.of(context)!.removeFromFavorites - : AppLocalizations.of(context)!.addToFavorites, - icon: isFavorite - ? const Icon(Icons.star) - : const Icon( - Icons.star_outline, - ), + ScheduleActionsPopup( + actions: [ + if (widget.isViewMode) + ScheduleActionConfig( + Text( + isFavorite + ? AppLocalizations.of(context)!.removeFromFavorites + : AppLocalizations.of(context)!.addToFavorites, + ), + action: ScheduleAction.favorite, + onPressed: () => onFavoritePressed(context, state), + ), + ScheduleActionConfig( + Text( + AppLocalizations.of(context)!.share, + ), + action: ScheduleAction.share, + onPressed: () => onSharePressed(context, state), ), - ), + ], + ), ], ), body: PageView.builder( @@ -620,4 +654,4 @@ class _SchedulePageState extends State ), ); } -} // _SchedulePageState +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 14a266e..1f951fc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -46,5 +46,6 @@ "clearCache": "Clear cache", "cacheIsCleared": "Cache is successfully cleared!", "errorWhileCleaningCache": "Unexpected error happened during cache clean. Try again later", - "cacheIsEmpty": "Cache is empty. Nothing to clean!" + "cacheIsEmpty": "Cache is empty. Nothing to clean!", + "share": "Share" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 30e48ea..8c3c844 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -43,5 +43,6 @@ "clearCache": "Очистить кэш", "cacheIsCleared": "Кэш успешно очищен!", "errorWhileCleaningCache": "Возникла неожиданная ошибка при очистке кэша. Попробуйте снова", - "cacheIsEmpty": "Кэш уже пуст!" + "cacheIsEmpty": "Кэш уже пуст!", + "share": "Поделиться" } \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 2c1ec4f..4c0025f 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7ea2a80..ad279a8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST sqlite3_flutter_libs + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 140712b..088835f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,11 +6,13 @@ import FlutterMacOS import Foundation import path_provider_foundation +import share_plus import shared_preferences_foundation import sqlite3_flutter_libs 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")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 1be8910..ba4af68 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -733,6 +741,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" + url: "https://pub.dev" + source: hosted + version: "10.0.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" + url: "https://pub.dev" + source: hosted + version: "5.0.0" shared_preferences: dependency: "direct main" description: @@ -922,6 +946,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + url: "https://pub.dev" + source: hosted + version: "3.1.2" uuid: dependency: transitive description: @@ -978,6 +1034,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + url: "https://pub.dev" + source: hosted + version: "5.5.4" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 47c6b9a..5f07bf2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: shared_preferences: ^2.1.0 home_widget: ^0.6.0 octopus: ^0.0.9 + share_plus: ^10.0.2 dev_dependencies: flutter_test: diff --git a/test/string_utils_test.dart b/test/string_utils_test.dart new file mode 100644 index 0000000..58f03fb --- /dev/null +++ b/test/string_utils_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:uneconly/common/utils/string_utils.dart'; + +void main() { + test( + 'trim separators trims all spaces and newlines between words in string', + () { + expect( + trimSeparators('Room 75. \nCentral Bank street 23'), + equals('Room 75. Central Bank street 23'), + ); + }, + ); +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 988f3c8..0143d6e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8abff95..b707726 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + share_plus sqlite3_flutter_libs + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST