diff --git a/wrestling_scoreboard_client/lib/l10n/app_de.arb b/wrestling_scoreboard_client/lib/l10n/app_de.arb index 02952e85..bd3cdb69 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_de.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_de.arb @@ -24,9 +24,9 @@ "apiProvider": "API-Anbieter", "importFromApiProvider": "Synchronisiere mit API-Anbieter", "warningImportFromApiProvider": "Diese Aktion importiert Objekte dieser Organisation und versucht diese zu integrieren. Bist du sicher, dass du fortfahren möchtest?", - "recommendFirstImportFromApiProvider": "Der letzte Import konnte nicht bestimmt werden. Möchtest du die Daten importieren?", - "recommendImportFromApiProvider": "Der letzte Import war am {date} um {time} Uhr. Möchtest du die Daten aktualisieren?", - "@recommendImportFromApiProvider": { + "proposeFirstImportFromApiProvider": "Der letzte Import konnte nicht bestimmt werden. Möchtest du die Daten importieren?", + "proposeImportFromApiProvider": "Der letzte Import war am {date} um {time} Uhr. Möchtest du die Daten aktualisieren?", + "@proposeImportFromApiProvider": { "placeholders": { "date": { "type": "DateTime", @@ -38,6 +38,7 @@ } } }, + "proposeApiImportDuration": "Dauer für den Vorschlag eines API-Imports", "reportProvider": "Report-Anbieter", "database": "Datenbank", "exportDatabase": "Datenbank exportieren", @@ -232,6 +233,7 @@ "injuryDuration": "Dauer der Verletzungszeit", "bleedingInjuryDuration": "Dauer der Verletzungszeit mit Blut", "periodCount": "Anzahl der Kampfabschnitte", + "days": "Days", "referee": "Kampfrichter", "refereeAbbr": "SR", diff --git a/wrestling_scoreboard_client/lib/l10n/app_en.arb b/wrestling_scoreboard_client/lib/l10n/app_en.arb index 54259a9e..bc3e8b26 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_en.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_en.arb @@ -27,9 +27,9 @@ "apiProvider": "API Provider", "importFromApiProvider": "Sync with API provider", "warningImportFromApiProvider": "This action imports objects of this organization and tries to integrate them. Are you sure, you want to continue?", - "recommendFirstImportFromApiProvider": "Cannot determine the last import. Would you like import the data?", - "recommendImportFromApiProvider": "The last import was on {date} at {time}. Would you like to update the data?", - "@recommendImportFromApiProvider": { + "proposeFirstImportFromApiProvider": "Cannot determine the last import. Would you like import the data?", + "proposeImportFromApiProvider": "The last import was on {date} at {time}. Would you like to update the data?", + "@proposeImportFromApiProvider": { "placeholders": { "date": { "type": "DateTime", @@ -41,6 +41,7 @@ } } }, + "proposeApiImportDuration": "Duration for proposing an API import", "reportProvider": "Report Provider", "database": "Database", "exportDatabase": "Export database", @@ -235,6 +236,7 @@ "injuryDuration": "Injury duration", "bleedingInjuryDuration": "Bleeding injury duration", "periodCount": "Number of periods", + "days": "Days", "referee": "Referee", "refereeAbbr": "REF", diff --git a/wrestling_scoreboard_client/lib/localization/duration.dart b/wrestling_scoreboard_client/lib/localization/duration.dart index abd4822e..0603b9be 100644 --- a/wrestling_scoreboard_client/lib/localization/duration.dart +++ b/wrestling_scoreboard_client/lib/localization/duration.dart @@ -1,4 +1,13 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + extension DurationLocalization on Duration { + String formatDaysHoursMinutes(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + return '$inDays ${localizations.days}, ${inHours.remainder(24).toString().padLeft(2, '0')}:${inMinutes.remainder(60).toString().padLeft(2, '0')}'; + } + String formatMinutesAndSeconds() { return '$inMinutes:${inSeconds.remainder(60).toString().padLeft(2, '0')}'; } diff --git a/wrestling_scoreboard_client/lib/provider/account_provider.g.dart b/wrestling_scoreboard_client/lib/provider/account_provider.g.dart index 3e4d740b..8dfc64a7 100644 --- a/wrestling_scoreboard_client/lib/provider/account_provider.g.dart +++ b/wrestling_scoreboard_client/lib/provider/account_provider.g.dart @@ -63,11 +63,10 @@ final class UserNotifierProvider extends $NotifierProvider r'5cfac4dea161f15a98b2aecf867e76d5d41812ce'; +String _$userNotifierHash() => r'36bea4cc9b89f18b89d1fd78203395da8209b59d'; abstract class _$UserNotifier extends $Notifier>> { Raw> build(); - @$internal @override Raw> runBuild() => build(); diff --git a/wrestling_scoreboard_client/lib/provider/app_state_provider.g.dart b/wrestling_scoreboard_client/lib/provider/app_state_provider.g.dart index c508a2b4..0d84e064 100644 --- a/wrestling_scoreboard_client/lib/provider/app_state_provider.g.dart +++ b/wrestling_scoreboard_client/lib/provider/app_state_provider.g.dart @@ -67,7 +67,6 @@ String _$windowStateNotifierHash() => r'c450d92ac1e13d9404d9adb1e68f47177fb150f0 abstract class _$WindowStateNotifier extends $Notifier>> { Raw> build(); - @$internal @override Raw> runBuild() => build(); diff --git a/wrestling_scoreboard_client/lib/provider/audio_provider.g.dart b/wrestling_scoreboard_client/lib/provider/audio_provider.g.dart index 5a607993..07b32144 100644 --- a/wrestling_scoreboard_client/lib/provider/audio_provider.g.dart +++ b/wrestling_scoreboard_client/lib/provider/audio_provider.g.dart @@ -67,7 +67,6 @@ String _$bellPlayerNotifierHash() => r'629ce6a4cd9db09a0250a179cd31607a10216d97' abstract class _$BellPlayerNotifier extends $Notifier>> { Raw> build(); - @$internal @override Raw> runBuild() => build(); diff --git a/wrestling_scoreboard_client/lib/provider/local_preferences.dart b/wrestling_scoreboard_client/lib/provider/local_preferences.dart index cf730f54..03c5524d 100644 --- a/wrestling_scoreboard_client/lib/provider/local_preferences.dart +++ b/wrestling_scoreboard_client/lib/provider/local_preferences.dart @@ -12,6 +12,8 @@ class Preferences { /// Network timeout in milliseconds. static const keyNetworkTimeout = 'network-timeout'; + static const keyProposeApiImportDuration = 'propose-api-import-duration'; + static const keyBellSound = 'bell-sound'; static const keyFontFamily = 'font-family'; static const keyFavorites = 'favorites'; diff --git a/wrestling_scoreboard_client/lib/provider/local_preferences_provider.dart b/wrestling_scoreboard_client/lib/provider/local_preferences_provider.dart index 27cbb8e2..3ca8ead1 100644 --- a/wrestling_scoreboard_client/lib/provider/local_preferences_provider.dart +++ b/wrestling_scoreboard_client/lib/provider/local_preferences_provider.dart @@ -219,6 +219,22 @@ class OrgAuthNotifier extends _$OrgAuthNotifier { } } +@Riverpod(keepAlive: true) +class ProposeApiImportDurationNotifier extends _$ProposeApiImportDurationNotifier { + @override + Raw> build() async { + var proposeApiImportDurationSecs = await Preferences.getInt(Preferences.keyProposeApiImportDuration); + return proposeApiImportDurationSecs != null + ? Duration(seconds: proposeApiImportDurationSecs) + : const Duration(days: 2); + } + + Future setState(Duration? val) async { + Preferences.setInt(Preferences.keyProposeApiImportDuration, val?.inSeconds); + state = Future.value(val); + } +} + @Riverpod(keepAlive: true) class JwtNotifier extends _$JwtNotifier { @override diff --git a/wrestling_scoreboard_client/lib/provider/local_preferences_provider.g.dart b/wrestling_scoreboard_client/lib/provider/local_preferences_provider.g.dart index 7aaadceb..6fccb629 100644 --- a/wrestling_scoreboard_client/lib/provider/local_preferences_provider.g.dart +++ b/wrestling_scoreboard_client/lib/provider/local_preferences_provider.g.dart @@ -605,6 +605,75 @@ abstract class _$OrgAuthNotifier extends $Notifier>> runBuild() => build(); } +@ProviderFor(ProposeApiImportDurationNotifier) +const proposeApiImportDurationNotifierProvider = ProposeApiImportDurationNotifierProvider._(); + +final class ProposeApiImportDurationNotifierProvider + extends $NotifierProvider>> { + const ProposeApiImportDurationNotifierProvider._( + {super.runNotifierBuildOverride, ProposeApiImportDurationNotifier Function()? create}) + : _createCb = create, + super( + from: null, + argument: null, + name: r'proposeApiImportDurationNotifierProvider', + isAutoDispose: false, + dependencies: null, + allTransitiveDependencies: null, + ); + + final ProposeApiImportDurationNotifier Function()? _createCb; + + @override + String debugGetCreateSourceHash() => _$proposeApiImportDurationNotifierHash(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Raw> value) { + return $ProviderOverride( + origin: this, + providerOverride: $ValueProvider>>(value), + ); + } + + @$internal + @override + ProposeApiImportDurationNotifier create() => _createCb?.call() ?? ProposeApiImportDurationNotifier(); + + @$internal + @override + ProposeApiImportDurationNotifierProvider $copyWithCreate( + ProposeApiImportDurationNotifier Function() create, + ) { + return ProposeApiImportDurationNotifierProvider._(create: create); + } + + @$internal + @override + ProposeApiImportDurationNotifierProvider $copyWithBuild( + Raw> Function( + Ref>>, + ProposeApiImportDurationNotifier, + ) build, + ) { + return ProposeApiImportDurationNotifierProvider._(runNotifierBuildOverride: build); + } + + @$internal + @override + $NotifierProviderElement>> $createElement( + ProviderContainer container) => + $NotifierProviderElement(this, container); +} + +String _$proposeApiImportDurationNotifierHash() => r'eeddaf8abe731269562eac53d8bd127bc34484ff'; + +abstract class _$ProposeApiImportDurationNotifier extends $Notifier>> { + Raw> build(); + @$internal + @override + Raw> runBuild() => build(); +} + @ProviderFor(JwtNotifier) const jwtNotifierProvider = JwtNotifierProvider._(); diff --git a/wrestling_scoreboard_client/lib/provider/network_provider.g.dart b/wrestling_scoreboard_client/lib/provider/network_provider.g.dart index 82e19b8c..fbb046b9 100644 --- a/wrestling_scoreboard_client/lib/provider/network_provider.g.dart +++ b/wrestling_scoreboard_client/lib/provider/network_provider.g.dart @@ -63,11 +63,10 @@ final class DataManagerNotifierProvider extends $NotifierProvider r'ca415c0257cc0041067af426ad3887c4353f72e2'; +String _$dataManagerNotifierHash() => r'503f87972a6b26b65ba10e6df7580c973fc8a08f'; abstract class _$DataManagerNotifier extends $Notifier>> { Raw> build(); - @$internal @override Raw> runBuild() => build(); @@ -137,7 +136,6 @@ String _$webSocketManagerNotifierHash() => r'265576c4812245258bd39d047948d2def4d abstract class _$WebSocketManagerNotifier extends $Notifier>> { Raw> build(); - @$internal @override Raw> runBuild() => build(); diff --git a/wrestling_scoreboard_client/lib/view/screens/more/settings.dart b/wrestling_scoreboard_client/lib/view/screens/more/settings.dart index 86690b8b..766120b2 100644 --- a/wrestling_scoreboard_client/lib/view/screens/more/settings.dart +++ b/wrestling_scoreboard_client/lib/view/screens/more/settings.dart @@ -209,6 +209,47 @@ class CustomSettingsScreen extends ConsumerWidget { ); }, ), + Restricted( + privilege: UserPrivilege.write, + child: LoadingBuilder( + future: ref.watch(proposeApiImportDurationNotifierProvider), + builder: (context, proposeApiImportDuration) { + return SettingsSection( + title: localizations.services, + action: TextButton( + onPressed: () async { + proposeApiImportDuration = const Duration(days: 2); + await ref + .read(proposeApiImportDurationNotifierProvider.notifier) + .setState(proposeApiImportDuration); + }, + child: Text(localizations.reset), + ), + children: [ + ListTile( + leading: const Icon(Icons.timelapse), + title: Text(localizations.proposeApiImportDuration), + subtitle: Text(proposeApiImportDuration.formatDaysHoursMinutes(context)), + onTap: () async { + final val = await showDialog( + context: context, + builder: (BuildContext context) { + // TODO: Allow days in duration picker + return DurationDialog( + initialValue: proposeApiImportDuration, + maxValue: const Duration(days: 365), + ); + }, + ); + if (val != null) { + await ref.read(proposeApiImportDurationNotifierProvider.notifier).setState(val); + } + }, + ), + ], + ); + }), + ), LoadingBuilder( future: ref.watch(networkTimeoutNotifierProvider), builder: (context, networkTimeout) { @@ -293,14 +334,6 @@ class CustomSettingsScreen extends ConsumerWidget { ); }, ), - // SettingsSection( - // title: localizations.services, - // action: TextButton( - // onPressed: () {}, - // child: Text(localizations.reset), - // ), - // children: [], - // ), Restricted( privilege: UserPrivilege.admin, child: SettingsSection( diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/shared/actions.dart b/wrestling_scoreboard_client/lib/view/screens/overview/shared/actions.dart index daf5acda..6edb2bea 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/shared/actions.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/shared/actions.dart @@ -45,39 +45,7 @@ class _OrganizationImportActionState extends ConsumerState true, - ); - if (result == true && context.mounted) { - await _import(localizations); - } - } - } - }); + checkProposeImport(context, ref, orgId: widget.orgId, id: widget.id, importType: widget.importType); } @override @@ -92,41 +60,91 @@ class _OrganizationImportActionState extends ConsumerState true, ); if (result == true && context.mounted) { - await _import(localizations); + await _processImport(context, ref, orgId: widget.orgId, id: widget.id, importType: widget.importType); } }, icon: const Icon(Icons.import_export), ); } +} + +Future checkProposeImport( + BuildContext context, + WidgetRef ref, { + required int orgId, + required int id, + required OrganizationImportType importType, +}) async { + final dataManager = await ref.read(dataManagerNotifierProvider); + DateTime? lastUpdated; + switch (importType) { + case OrganizationImportType.organization: + lastUpdated = await dataManager.organizationLastImportUtcDateTime(id); + case OrganizationImportType.team: + lastUpdated = await dataManager.organizationTeamLastImportUtcDateTime(id); + case OrganizationImportType.league: + lastUpdated = await dataManager.organizationLeagueLastImportUtcDateTime(id); + case OrganizationImportType.competition: + lastUpdated = await dataManager.organizationCompetitionLastImportUtcDateTime(id); + case OrganizationImportType.teamMatch: + lastUpdated = await dataManager.organizationTeamMatchLastImportUtcDateTime(id); + } + lastUpdated = lastUpdated?.toLocal(); - Future _import(AppLocalizations localizations) async { - await catchAsync( - context, - () => showLoadingDialog( - label: AppLocalizations.of(context)!.importFromApiProvider, + final proposeApiImportDuration = await ref.read(proposeApiImportDurationNotifierProvider); + if (lastUpdated == null || lastUpdated.compareTo(DateTime.now().subtract(proposeApiImportDuration)) < 0) { + if (context.mounted) { + final localizations = AppLocalizations.of(context)!; + final result = await showOkCancelDialog( + context: context, + child: Text(lastUpdated == null + ? localizations.proposeFirstImportFromApiProvider + : localizations.proposeImportFromApiProvider(lastUpdated, lastUpdated)), + getResult: () => true, + ); + if (result == true && context.mounted) { + await _processImport(context, ref, orgId: orgId, id: id, importType: importType); + } + } + } +} + +Future _processImport( + BuildContext context, + WidgetRef ref, { + required int orgId, + required int id, + required OrganizationImportType importType, +}) async { + await catchAsync( + context, + () { + final localizations = AppLocalizations.of(context)!; + return showLoadingDialog( + label: localizations.importFromApiProvider, runAsync: (BuildContext context) async { final dataManager = await ref.read(dataManagerNotifierProvider); - final authService = (await ref.read(orgAuthNotifierProvider))[widget.orgId]; - switch (widget.importType) { + final authService = (await ref.read(orgAuthNotifierProvider))[orgId]; + switch (importType) { case OrganizationImportType.organization: - await dataManager.organizationImport(widget.id, authService: authService); + await dataManager.organizationImport(id, authService: authService); case OrganizationImportType.team: - await dataManager.organizationTeamImport(widget.id, authService: authService); + await dataManager.organizationTeamImport(id, authService: authService); case OrganizationImportType.league: - await dataManager.organizationLeagueImport(widget.id, authService: authService); + await dataManager.organizationLeagueImport(id, authService: authService); case OrganizationImportType.competition: - await dataManager.organizationCompetitionImport(widget.id, authService: authService); + await dataManager.organizationCompetitionImport(id, authService: authService); case OrganizationImportType.teamMatch: - await dataManager.organizationTeamMatchImport(widget.id, authService: authService); + await dataManager.organizationTeamMatchImport(id, authService: authService); } if (context.mounted) { await showOkDialog(context: context, child: Text(localizations.actionSuccessful)); } }, context: context, - ), - ); - } + ); + }, + ); } enum OrganizationImportType { diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart index 6ab7ea4b..0f01afae 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart @@ -9,7 +9,6 @@ import 'package:wrestling_scoreboard_client/localization/date_time.dart'; import 'package:wrestling_scoreboard_client/localization/season.dart'; import 'package:wrestling_scoreboard_client/provider/data_provider.dart'; import 'package:wrestling_scoreboard_client/provider/network_provider.dart'; -import 'package:wrestling_scoreboard_client/services/network/data_manager.dart'; import 'package:wrestling_scoreboard_client/services/print/pdf/team_match_transcript.dart'; import 'package:wrestling_scoreboard_client/utils/export.dart'; import 'package:wrestling_scoreboard_client/view/screens/display/match/match_display.dart'; @@ -221,23 +220,23 @@ class TeamMatchOverview extends ConsumerWidget { title: homeLineup.team.name, icon: Icons.view_list, onTap: () async => handleSelectedLineup( + context, + ref, homeLineup, match, navigator, - (await ref.read(dataManagerNotifierProvider)), league: match.league!, - ref: ref, )), ContentItem( title: guestLineup.team.name, icon: Icons.view_list, onTap: () async => handleSelectedLineup( + context, + ref, guestLineup, match, navigator, - (await ref.read(dataManagerNotifierProvider)), league: match.league!, - ref: ref, )), ], ), @@ -294,8 +293,15 @@ class TeamMatchOverview extends ConsumerWidget { context.push('/${TeamMatchOverview.route}/${match.id}/${MatchDisplay.route}'); } - handleSelectedLineup(Lineup lineup, TeamMatch match, NavigatorState navigator, DataManager dataManager, - {required League league, required WidgetRef ref}) async { + handleSelectedLineup( + BuildContext context, + WidgetRef ref, + Lineup lineup, + TeamMatch match, + NavigatorState navigator, { + required League league, + }) async { + final dataManager = await ref.read(dataManagerNotifierProvider); final participations = await dataManager.readMany(filterObject: lineup); final weightClasses = (await dataManager.readMany(filterObject: league.division)) .where((element) => element.seasonPartition == match.seasonPartition) @@ -312,7 +318,14 @@ class TeamMatchOverview extends ConsumerWidget { matches.sort((a, b) => a.date.compareTo(b.date)); final resolvedMatch = matches.lastWhereOrNull((match) => match.home.team == lineup.team || match.guest.team == lineup.team); - if (resolvedMatch != null) { + if (resolvedMatch != null && resolvedMatch.organization != null && context.mounted) { + await checkProposeImport( + context, + ref, + orgId: resolvedMatch.organization!.id!, + id: resolvedMatch.id!, + importType: OrganizationImportType.teamMatch, + ); proposedLineup = resolvedMatch.home.team == lineup.team ? resolvedMatch.home : resolvedMatch.guest; proposedParticipations = await dataManager.readMany(filterObject: proposedLineup); }