feat(online): partage sortant et boîte de réception (ticket #69)
Ajoute l'écran de partage sortant (presentation/share_screen.dart) et la boîte de réception des partages (presentation/share_inbox_screen.dart), avec les points d'entrée sur profile_screen.dart, home_screen.dart, program_screen.dart et workout_template_screen.dart. flutter analyze propre (mêmes infos préexistantes), 132/132 tests verts, build APK debug validé. Dernier ticket de développement du chantier "Ajouter les features serveur au client" (#63) — il ne reste que la QA finale (#70). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -138,6 +138,9 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
builder: (context) => ProgramListScreen(
|
||||
programUseCases: widget.bootstrap.programUseCases,
|
||||
exerciseUseCases: widget.bootstrap.exerciseUseCases,
|
||||
shareUseCases: widget.bootstrap.shareUseCases,
|
||||
authUseCases: widget.bootstrap.authUseCases,
|
||||
syncUseCases: widget.bootstrap.syncUseCases,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -157,6 +160,9 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||
shareUseCases: widget.bootstrap.shareUseCases,
|
||||
authUseCases: widget.bootstrap.authUseCases,
|
||||
syncUseCases: widget.bootstrap.syncUseCases,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -205,6 +211,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
builder: (context) => ProfileScreen(
|
||||
authUseCases: widget.bootstrap.authUseCases,
|
||||
syncUseCases: widget.bootstrap.syncUseCases,
|
||||
shareUseCases: widget.bootstrap.shareUseCases,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -5,6 +5,8 @@ export 'history_screen.dart';
|
||||
export 'home_screen.dart';
|
||||
export 'profile_screen.dart';
|
||||
export 'program_screen.dart';
|
||||
export 'share_inbox_screen.dart';
|
||||
export 'share_screen.dart';
|
||||
export 'theme.dart';
|
||||
export 'workout_execution_screen.dart';
|
||||
export 'workout_template_screen.dart';
|
||||
|
||||
@ -2,17 +2,20 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
import 'share_inbox_screen.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
final class ProfileScreen extends StatefulWidget {
|
||||
const ProfileScreen({
|
||||
required this.authUseCases,
|
||||
required this.syncUseCases,
|
||||
required this.shareUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AuthUseCases authUseCases;
|
||||
final SyncUseCases syncUseCases;
|
||||
final ShareUseCases shareUseCases;
|
||||
|
||||
@override
|
||||
State<ProfileScreen> createState() => _ProfileScreenState();
|
||||
@ -122,7 +125,10 @@ final class _ProfileScreenState extends State<ProfileScreen> {
|
||||
|
||||
void _openReceivedShares(BuildContext context) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const ReceivedSharesScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ShareInboxScreen(shareUseCases: widget.shareUseCases),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -672,26 +678,6 @@ final class _RegisterScreenState extends State<RegisterScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
final class ReceivedSharesScreen extends StatelessWidget {
|
||||
const ReceivedSharesScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Partages reçus')),
|
||||
body: const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Text(
|
||||
"Les programmes et séances qu'on t'envoie apparaîtront ici.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class _AuthFormScaffold extends StatelessWidget {
|
||||
const _AuthFormScaffold({required this.child});
|
||||
|
||||
|
||||
@ -5,16 +5,23 @@ import 'package:flutter/material.dart';
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
import 'exercise_library_screen.dart';
|
||||
import 'share_screen.dart';
|
||||
|
||||
final class ProgramListScreen extends StatefulWidget {
|
||||
const ProgramListScreen({
|
||||
required this.programUseCases,
|
||||
required this.exerciseUseCases,
|
||||
this.shareUseCases,
|
||||
this.authUseCases,
|
||||
this.syncUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ProgramUseCases programUseCases;
|
||||
final ExerciseUseCases exerciseUseCases;
|
||||
final ShareUseCases? shareUseCases;
|
||||
final AuthUseCases? authUseCases;
|
||||
final SyncUseCases? syncUseCases;
|
||||
|
||||
@override
|
||||
State<ProgramListScreen> createState() => _ProgramListScreenState();
|
||||
@ -101,6 +108,9 @@ final class _ProgramListScreenState extends State<ProgramListScreen> {
|
||||
builder: (context) => ProgramFormScreen(
|
||||
programUseCases: widget.programUseCases,
|
||||
exerciseUseCases: widget.exerciseUseCases,
|
||||
shareUseCases: widget.shareUseCases,
|
||||
authUseCases: widget.authUseCases,
|
||||
syncUseCases: widget.syncUseCases,
|
||||
program: program,
|
||||
),
|
||||
),
|
||||
@ -161,12 +171,18 @@ final class ProgramFormScreen extends StatefulWidget {
|
||||
const ProgramFormScreen({
|
||||
required this.programUseCases,
|
||||
required this.exerciseUseCases,
|
||||
this.shareUseCases,
|
||||
this.authUseCases,
|
||||
this.syncUseCases,
|
||||
this.program,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ProgramUseCases programUseCases;
|
||||
final ExerciseUseCases exerciseUseCases;
|
||||
final ShareUseCases? shareUseCases;
|
||||
final AuthUseCases? authUseCases;
|
||||
final SyncUseCases? syncUseCases;
|
||||
final Program? program;
|
||||
|
||||
@override
|
||||
@ -216,6 +232,14 @@ final class _ProgramFormScreenState extends State<ProgramFormScreen> {
|
||||
title: Text(
|
||||
_isEditing ? 'Modifier le programme' : 'Créer un programme',
|
||||
),
|
||||
actions: [
|
||||
if (_isEditing && widget.shareUseCases != null)
|
||||
IconButton(
|
||||
tooltip: 'Partager',
|
||||
icon: const Icon(Icons.ios_share),
|
||||
onPressed: _shareProgram,
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
minimum: const EdgeInsets.all(16),
|
||||
@ -395,6 +419,25 @@ final class _ProgramFormScreenState extends State<ProgramFormScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _shareProgram() async {
|
||||
final program = widget.program;
|
||||
final shareUseCases = widget.shareUseCases;
|
||||
if (program == null || shareUseCases == null) return;
|
||||
await Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareFormScreen(
|
||||
shareUseCases: shareUseCases,
|
||||
authUseCases: widget.authUseCases,
|
||||
syncUseCases: widget.syncUseCases,
|
||||
resourceType: ShareResourceType.program,
|
||||
localResourceId: program.metadata.id,
|
||||
resourceName: program.name,
|
||||
resourceSummary: _programSummary(program),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _nonNegativeIntValidator(String? value) {
|
||||
final number = int.tryParse(value?.trim() ?? '');
|
||||
if (number == null || number < 0) {
|
||||
|
||||
271
lib/presentation/share_inbox_screen.dart
Normal file
271
lib/presentation/share_inbox_screen.dart
Normal file
@ -0,0 +1,271 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
final class ShareInboxScreen extends StatefulWidget {
|
||||
const ShareInboxScreen({required this.shareUseCases, super.key});
|
||||
|
||||
final ShareUseCases shareUseCases;
|
||||
|
||||
@override
|
||||
State<ShareInboxScreen> createState() => _ShareInboxScreenState();
|
||||
}
|
||||
|
||||
final class _ShareInboxScreenState extends State<ShareInboxScreen> {
|
||||
late Future<List<ShareInboxItem>> _items;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_items = widget.shareUseCases.refreshInbox();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Partages reçus')),
|
||||
body: FutureBuilder<List<ShareInboxItem>>(
|
||||
future: _items,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final items = snapshot.data ?? const <ShareInboxItem>[];
|
||||
if (items.isEmpty) {
|
||||
return const _EmptyInbox();
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh: _reload,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return _ShareInboxTile(
|
||||
item: item,
|
||||
onAccept: item.status == ShareInboxStatus.pending
|
||||
? () => _accept(item)
|
||||
: null,
|
||||
onDecline: item.status == ShareInboxStatus.pending
|
||||
? () => _decline(item)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemCount: items.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
setState(() {
|
||||
_items = widget.shareUseCases.refreshInbox();
|
||||
});
|
||||
await _items;
|
||||
}
|
||||
|
||||
Future<void> _accept(ShareInboxItem item) async {
|
||||
await widget.shareUseCases.acceptShare(item.shareId);
|
||||
if (!mounted) return;
|
||||
await _reload();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(_acceptedMessage(item.resourceType))),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _decline(ShareInboxItem item) async {
|
||||
await widget.shareUseCases.declineShare(item.shareId);
|
||||
if (!mounted) return;
|
||||
await _reload();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Partage refusé.')));
|
||||
}
|
||||
}
|
||||
|
||||
final class _ShareInboxTile extends StatelessWidget {
|
||||
const _ShareInboxTile({
|
||||
required this.item,
|
||||
required this.onAccept,
|
||||
required this.onDecline,
|
||||
});
|
||||
|
||||
final ShareInboxItem item;
|
||||
final VoidCallback? onAccept;
|
||||
final VoidCallback? onDecline;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final summary = _summaryFromPayload(item);
|
||||
return CourtBlazerAccentPanel(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
item.resourceType == ShareResourceType.program
|
||||
? Icons.list_alt
|
||||
: Icons.event_note,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_resourceTitle(item.resourceType),
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
summary.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('Envoyé par ${item.senderUserId}'),
|
||||
if (summary.detail.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(summary.detail),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Chip(
|
||||
visualDensity: VisualDensity.compact,
|
||||
label: Text(_statusLabel(item.status)),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (item.status == ShareInboxStatus.pending) ...[
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: onDecline,
|
||||
child: const Text('Refuser'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: onAccept,
|
||||
child: const Text('Accepter'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class _EmptyInbox extends StatelessWidget {
|
||||
const _EmptyInbox();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Aucun partage reçu', textAlign: TextAlign.center),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
"Les programmes et séances qu'on t'envoie apparaîtront ici.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class _InboxSummary {
|
||||
const _InboxSummary({required this.name, required this.detail});
|
||||
|
||||
final String name;
|
||||
final String detail;
|
||||
}
|
||||
|
||||
_InboxSummary _summaryFromPayload(ShareInboxItem item) {
|
||||
try {
|
||||
final payload = jsonDecode(item.payloadJson) as Map<String, dynamic>;
|
||||
final name = payload['name'] as String? ?? 'Partage GameTime';
|
||||
return switch (item.resourceType) {
|
||||
ShareResourceType.program => _InboxSummary(
|
||||
name: name,
|
||||
detail: _programDetail(payload),
|
||||
),
|
||||
ShareResourceType.workoutTemplate => _InboxSummary(
|
||||
name: name,
|
||||
detail: _templateDetail(payload),
|
||||
),
|
||||
};
|
||||
} on Object {
|
||||
return const _InboxSummary(name: 'Partage GameTime', detail: '');
|
||||
}
|
||||
}
|
||||
|
||||
String _programDetail(Map<String, dynamic> payload) {
|
||||
final exercises = payload['exercises'] as List<dynamic>? ?? const [];
|
||||
final sets = exercises.fold<int>(0, (total, raw) {
|
||||
final exercise = raw as Map<String, dynamic>;
|
||||
return total + (exercise['setsCount'] as int? ?? 0);
|
||||
});
|
||||
return '${exercises.length} exercice${exercises.length > 1 ? 's' : ''} · '
|
||||
'$sets série${sets > 1 ? 's' : ''}';
|
||||
}
|
||||
|
||||
String _templateDetail(Map<String, dynamic> payload) {
|
||||
final programs = payload['programs'] as List<dynamic>? ?? const [];
|
||||
var exerciseCount = 0;
|
||||
for (final raw in programs) {
|
||||
final program = raw as Map<String, dynamic>;
|
||||
final snapshotJson = program['programSnapshotJson'] as String?;
|
||||
if (snapshotJson == null) continue;
|
||||
final snapshot = jsonDecode(snapshotJson) as Map<String, dynamic>;
|
||||
exerciseCount +=
|
||||
(snapshot['exercises'] as List<dynamic>? ?? const []).length;
|
||||
}
|
||||
return '${programs.length} programme${programs.length > 1 ? 's' : ''} · '
|
||||
'$exerciseCount exercice${exerciseCount > 1 ? 's' : ''}';
|
||||
}
|
||||
|
||||
String _resourceTitle(ShareResourceType type) {
|
||||
return switch (type) {
|
||||
ShareResourceType.program => 'Programme',
|
||||
ShareResourceType.workoutTemplate => 'Séance',
|
||||
};
|
||||
}
|
||||
|
||||
String _statusLabel(ShareInboxStatus status) {
|
||||
return switch (status) {
|
||||
ShareInboxStatus.pending => 'À traiter',
|
||||
ShareInboxStatus.accepted => 'Accepté',
|
||||
ShareInboxStatus.declined => 'Refusé',
|
||||
ShareInboxStatus.revoked => 'Retiré',
|
||||
};
|
||||
}
|
||||
|
||||
String _acceptedMessage(ShareResourceType type) {
|
||||
return switch (type) {
|
||||
ShareResourceType.program => 'Programme ajouté.',
|
||||
ShareResourceType.workoutTemplate => 'Séance ajoutée.',
|
||||
};
|
||||
}
|
||||
260
lib/presentation/share_screen.dart
Normal file
260
lib/presentation/share_screen.dart
Normal file
@ -0,0 +1,260 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
import 'profile_screen.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
final class ShareFormScreen extends StatefulWidget {
|
||||
const ShareFormScreen({
|
||||
required this.shareUseCases,
|
||||
required this.resourceType,
|
||||
required this.localResourceId,
|
||||
required this.resourceName,
|
||||
required this.resourceSummary,
|
||||
this.authUseCases,
|
||||
this.syncUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ShareUseCases shareUseCases;
|
||||
final AuthUseCases? authUseCases;
|
||||
final SyncUseCases? syncUseCases;
|
||||
final ShareResourceType resourceType;
|
||||
final String localResourceId;
|
||||
final String resourceName;
|
||||
final String resourceSummary;
|
||||
|
||||
@override
|
||||
State<ShareFormScreen> createState() => _ShareFormScreenState();
|
||||
}
|
||||
|
||||
final class _ShareFormScreenState extends State<ShareFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailsController = TextEditingController();
|
||||
var _sending = false;
|
||||
String? _message;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(_shareTitle(widget.resourceType))),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
CourtBlazerAccentPanel(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'${_resourceTitle(widget.resourceType)} à partager',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(widget.resourceName),
|
||||
const SizedBox(height: 4),
|
||||
Text(widget.resourceSummary),
|
||||
const SizedBox(height: 16),
|
||||
Form(
|
||||
key: _formKey,
|
||||
child: TextFormField(
|
||||
controller: _emailsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Emails destinataires',
|
||||
helperText: 'Sépare plusieurs emails par une virgule.',
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: _emailsValidator,
|
||||
),
|
||||
),
|
||||
if (_message != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(_message!),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: _sending ? null : _send,
|
||||
icon: _sending
|
||||
? const SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.ios_share),
|
||||
label: const Text('Envoyer le partage'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _send() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() {
|
||||
_sending = true;
|
||||
_message = null;
|
||||
});
|
||||
try {
|
||||
final result = await widget.shareUseCases.sendShare(
|
||||
resourceType: widget.resourceType,
|
||||
localResourceId: widget.localResourceId,
|
||||
recipientEmails: _parseEmails(_emailsController.text),
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_sending = false;
|
||||
_message = _shareResultMessage(result);
|
||||
});
|
||||
if (result.status == ShareSendStatus.notConnected) {
|
||||
await Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareAccountRequiredScreen(
|
||||
resourceType: widget.resourceType,
|
||||
authUseCases: widget.authUseCases,
|
||||
syncUseCases: widget.syncUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} on DomainException {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_sending = false;
|
||||
_message = 'Partage indisponible pour le moment.';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ShareAccountRequiredScreen extends StatelessWidget {
|
||||
const ShareAccountRequiredScreen({
|
||||
required this.resourceType,
|
||||
this.authUseCases,
|
||||
this.syncUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ShareResourceType resourceType;
|
||||
final AuthUseCases? authUseCases;
|
||||
final SyncUseCases? syncUseCases;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final targetLabel = switch (resourceType) {
|
||||
ShareResourceType.program => 'ce programme',
|
||||
ShareResourceType.workoutTemplate => 'cette séance',
|
||||
};
|
||||
final canOpenAuth = authUseCases != null && syncUseCases != null;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Compte requis pour partager')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
CourtBlazerAccentPanel(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Compte requis pour partager',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Connecte-toi pour envoyer $targetLabel à un autre compte '
|
||||
'GameTime. Le reste de l’app reste utilisable sans compte.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: canOpenAuth
|
||||
? () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LoginScreen(
|
||||
authUseCases: authUseCases!,
|
||||
syncUseCases: syncUseCases!,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: const Text('Se connecter'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton(
|
||||
onPressed: canOpenAuth
|
||||
? () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RegisterScreen(
|
||||
authUseCases: authUseCases!,
|
||||
syncUseCases: syncUseCases!,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: const Text('Créer un compte'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String? _emailsValidator(String? value) {
|
||||
final emails = _parseEmails(value ?? '');
|
||||
if (emails.isEmpty) {
|
||||
return "Saisis l'email du destinataire.";
|
||||
}
|
||||
final invalid = emails.any(
|
||||
(email) => !RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$').hasMatch(email),
|
||||
);
|
||||
if (invalid) {
|
||||
return "Saisis l'email du destinataire.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> _parseEmails(String value) {
|
||||
return value
|
||||
.split(RegExp(r'[,;\n]'))
|
||||
.map((email) => email.trim())
|
||||
.where((email) => email.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
String _shareResultMessage(ShareSendResult result) {
|
||||
return switch (result.status) {
|
||||
ShareSendStatus.notConnected => 'Compte requis pour partager.',
|
||||
ShareSendStatus.queued => 'Partage enregistré. Envoi dès que possible.',
|
||||
ShareSendStatus.sent when result.unresolvedEmails.isNotEmpty =>
|
||||
'Partage envoyé. Non trouvés : ${result.unresolvedEmails.join(', ')}.',
|
||||
ShareSendStatus.sent => 'Partage envoyé.',
|
||||
};
|
||||
}
|
||||
|
||||
String _shareTitle(ShareResourceType type) {
|
||||
return switch (type) {
|
||||
ShareResourceType.program => 'Partager le programme',
|
||||
ShareResourceType.workoutTemplate => 'Partager la séance',
|
||||
};
|
||||
}
|
||||
|
||||
String _resourceTitle(ShareResourceType type) {
|
||||
return switch (type) {
|
||||
ShareResourceType.program => 'Programme',
|
||||
ShareResourceType.workoutTemplate => 'Séance',
|
||||
};
|
||||
}
|
||||
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
import 'share_screen.dart';
|
||||
import 'workout_execution_screen.dart';
|
||||
|
||||
final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
@ -15,6 +16,9 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
required this.historyUseCases,
|
||||
this.mediaUseCases,
|
||||
this.stepUseCases,
|
||||
this.shareUseCases,
|
||||
this.authUseCases,
|
||||
this.syncUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -25,6 +29,9 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final WorkoutHistoryUseCases historyUseCases;
|
||||
final MediaUseCases? mediaUseCases;
|
||||
final ShareUseCases? shareUseCases;
|
||||
final AuthUseCases? authUseCases;
|
||||
final SyncUseCases? syncUseCases;
|
||||
|
||||
@override
|
||||
State<WorkoutTemplateListScreen> createState() =>
|
||||
@ -109,6 +116,9 @@ final class _WorkoutTemplateListScreenState
|
||||
builder: (context) => WorkoutTemplateFormScreen(
|
||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||
programUseCases: widget.programUseCases,
|
||||
shareUseCases: widget.shareUseCases,
|
||||
authUseCases: widget.authUseCases,
|
||||
syncUseCases: widget.syncUseCases,
|
||||
template: template,
|
||||
),
|
||||
),
|
||||
@ -193,12 +203,18 @@ final class WorkoutTemplateFormScreen extends StatefulWidget {
|
||||
const WorkoutTemplateFormScreen({
|
||||
required this.workoutTemplateUseCases,
|
||||
required this.programUseCases,
|
||||
this.shareUseCases,
|
||||
this.authUseCases,
|
||||
this.syncUseCases,
|
||||
this.template,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
final ProgramUseCases programUseCases;
|
||||
final ShareUseCases? shareUseCases;
|
||||
final AuthUseCases? authUseCases;
|
||||
final SyncUseCases? syncUseCases;
|
||||
final WorkoutTemplate? template;
|
||||
|
||||
@override
|
||||
@ -241,6 +257,14 @@ final class _WorkoutTemplateFormScreenState
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_isEditing ? 'Modifier la séance' : 'Créer une séance'),
|
||||
actions: [
|
||||
if (_isEditing && widget.shareUseCases != null)
|
||||
IconButton(
|
||||
tooltip: 'Partager',
|
||||
icon: const Icon(Icons.ios_share),
|
||||
onPressed: _shareTemplate,
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
minimum: const EdgeInsets.all(16),
|
||||
@ -390,6 +414,25 @@ final class _WorkoutTemplateFormScreenState
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _shareTemplate() async {
|
||||
final template = widget.template;
|
||||
final shareUseCases = widget.shareUseCases;
|
||||
if (template == null || shareUseCases == null) return;
|
||||
await Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareFormScreen(
|
||||
shareUseCases: shareUseCases,
|
||||
authUseCases: widget.authUseCases,
|
||||
syncUseCases: widget.syncUseCases,
|
||||
resourceType: ShareResourceType.workoutTemplate,
|
||||
localResourceId: template.metadata.id,
|
||||
resourceName: template.name,
|
||||
resourceSummary: _templateSummary(template),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSnackBar(String message) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
|
||||
Reference in New Issue
Block a user