Merge branch 'dev/feature-add-multiplayer-partymode'
* dev/feature-add-multiplayer-partymode: feat: add multiplayer workout
This commit is contained in:
commit
ce6c479e92
11 changed files with 792 additions and 33 deletions
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:slrpg_app/src/features/multiplayer/presentation/screens/leaderboard_screen.dart';
|
import 'package:slrpg_app/src/features/multiplayer/presentation/screens/leaderboard_screen.dart';
|
||||||
|
import 'package:slrpg_app/src/features/multiplayer/presentation/screens/lobby_screen.dart';
|
||||||
|
|
||||||
import '../../features/authentication/presentation/screens/login_screen.dart';
|
import '../../features/authentication/presentation/screens/login_screen.dart';
|
||||||
import '../../features/authentication/presentation/screens/profile_screen.dart';
|
import '../../features/authentication/presentation/screens/profile_screen.dart';
|
||||||
|
|
@ -140,6 +141,26 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||||
path: '/leaderboard',
|
path: '/leaderboard',
|
||||||
builder: (context, state) => const LeaderboardScreen(),
|
builder: (context, state) => const LeaderboardScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/lobby/:partyId',
|
||||||
|
builder: (context, state) {
|
||||||
|
final partyId = state.pathParameters['partyId']!;
|
||||||
|
return LobbyScreen(partyId: partyId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/battle/:partyId',
|
||||||
|
builder: (context, state) {
|
||||||
|
final partyId = state.pathParameters['partyId']!;
|
||||||
|
|
||||||
|
final extra = state.extra as Map<String, dynamic>?;
|
||||||
|
return BattleScreen(
|
||||||
|
week: extra?['week'] ?? 1,
|
||||||
|
day: extra?['day'] ?? 1,
|
||||||
|
workoutId: extra?['workoutId'],
|
||||||
|
partyId: partyId);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:slrpg_app/l10n/app_localizations.dart';
|
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||||
|
import 'package:slrpg_app/src/features/multiplayer/data/repositories/party_repository.dart';
|
||||||
|
|
||||||
import '../../../../core/constants/app_constants.dart';
|
import '../../../../core/constants/app_constants.dart';
|
||||||
import '../../../../core/debug/debug_config_screen.dart';
|
import '../../../../core/debug/debug_config_screen.dart';
|
||||||
|
|
@ -227,6 +228,87 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showMultiplayerDialog() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('MULTIPLAYER RAID'),
|
||||||
|
content:
|
||||||
|
const Text('Join forces with other heroes to defeat epic bosses!'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_showJoinCodeDialog();
|
||||||
|
},
|
||||||
|
child: const Text('JOIN PARTY'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primaryColor),
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
try {
|
||||||
|
final party =
|
||||||
|
await ref.read(partyRepositoryProvider).createParty();
|
||||||
|
if (mounted) context.go('/lobby/${party.id}');
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted)
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('CREATE PARTY',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showJoinCodeDialog() {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('ENTER PARTY CODE'),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'e.g. A1B2',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('CANCEL'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final code = controller.text.trim().toUpperCase();
|
||||||
|
if (code.isNotEmpty) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
try {
|
||||||
|
final party =
|
||||||
|
await ref.read(partyRepositoryProvider).joinParty(code);
|
||||||
|
if (mounted) context.go('/lobby/${party.id}');
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted)
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('JOIN'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final userRepo = ref.watch(userRepositoryProvider);
|
final userRepo = ref.watch(userRepositoryProvider);
|
||||||
|
|
@ -334,6 +416,12 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
color: AppTheme.secondaryColor),
|
color: AppTheme.secondaryColor),
|
||||||
onPressed: () => context.go('/leaderboard'),
|
onPressed: () => context.go('/leaderboard'),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.groups,
|
||||||
|
color: AppTheme.secondaryColor),
|
||||||
|
tooltip: 'Multiplayer Lobby',
|
||||||
|
onPressed: _showMultiplayerDialog,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:pocketbase/pocketbase.dart';
|
||||||
|
import 'package:slrpg_app/src/features/multiplayer/domain/entities/party.dart';
|
||||||
|
import 'package:slrpg_app/src/features/multiplayer/domain/entities/party_member.dart';
|
||||||
|
import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart';
|
||||||
|
import '../../../../core/constants/app_constants.dart';
|
||||||
|
import '../../../../shared/data/remote/api_client.dart';
|
||||||
|
|
||||||
|
final partyRepositoryProvider = Provider((ref) {
|
||||||
|
return PartyRepository(ref.read(apiClientProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
class PartyRepository {
|
||||||
|
final ApiClient _api;
|
||||||
|
final PocketBase _pb;
|
||||||
|
|
||||||
|
PartyRepository(this._api) : _pb = PocketBase(AppConstants.apiBaseUrl);
|
||||||
|
|
||||||
|
Future<void> _syncAuth() async {
|
||||||
|
final token = await _api.getToken();
|
||||||
|
if (token != null) {
|
||||||
|
_pb.authStore.save(token, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Party> createParty() async {
|
||||||
|
final response = await _api.dio.post('/api/v1/party/create');
|
||||||
|
final partyId = response.data['party_id'];
|
||||||
|
return getPartyDetails(partyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Party> joinParty(String code) async {
|
||||||
|
final response = await _api.dio.post(
|
||||||
|
'/api/v1/party/join',
|
||||||
|
data: {'code': code},
|
||||||
|
);
|
||||||
|
final partyId = response.data['party_id'];
|
||||||
|
return getPartyDetails(partyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Party> getPartyDetails(String partyId) async {
|
||||||
|
final response =
|
||||||
|
await _api.dio.get('/api/collections/parties/records/$partyId');
|
||||||
|
return Party.fromJson(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setReady(String partyId, bool isReady) async {
|
||||||
|
final userId = (await _api.getUserId())!;
|
||||||
|
|
||||||
|
final members = await _pb.collection('party_members').getList(
|
||||||
|
filter: 'party_id="$partyId" && user_id="$userId"',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (members.items.isNotEmpty) {
|
||||||
|
await _pb.collection('party_members').update(
|
||||||
|
members.items.first.id,
|
||||||
|
body: {'is_ready': isReady},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startRaid(String partyId, {int? customHp}) async {
|
||||||
|
final body = <String, dynamic>{'status': 'active'};
|
||||||
|
|
||||||
|
if (customHp != null) {
|
||||||
|
body['current_hp'] = customHp;
|
||||||
|
body['max_hp'] = customHp;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _pb.collection('parties').update(partyId, body: body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Future<void> startRaid(String partyId) async {
|
||||||
|
// await _pb.collection('parties').update(partyId, body: {'status': 'active'});
|
||||||
|
// }
|
||||||
|
|
||||||
|
Stream<Party> subscribeToParty(String partyId) async* {
|
||||||
|
await _syncAuth();
|
||||||
|
|
||||||
|
yield await getPartyDetails(partyId);
|
||||||
|
|
||||||
|
final controller = StreamController<Party>();
|
||||||
|
|
||||||
|
_pb.collection('parties').subscribe(partyId, (e) {
|
||||||
|
if (e.action == 'update') {
|
||||||
|
controller.add(Party.fromJson(e.record!.toJson()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<PartyMember>> subscribeToMembers(String partyId) async* {
|
||||||
|
await _syncAuth();
|
||||||
|
|
||||||
|
Future<List<PartyMember>> fetchMembers() async {
|
||||||
|
final records = await _pb.collection('party_members').getFullList(
|
||||||
|
filter: 'party_id="$partyId"',
|
||||||
|
expand: 'user_id',
|
||||||
|
);
|
||||||
|
return records.map((r) => PartyMember.fromRecord(r.toJson())).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
yield await fetchMembers();
|
||||||
|
|
||||||
|
final controller = StreamController<List<PartyMember>>();
|
||||||
|
|
||||||
|
_pb.collection('party_members').subscribe('*', (e) async {
|
||||||
|
if (e.record!.getStringValue('party_id') == partyId) {
|
||||||
|
controller.add(await fetchMembers());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dealDamage(String partyId, int damage) async {
|
||||||
|
await _api.dio.post(
|
||||||
|
'/api/v1/party/damage',
|
||||||
|
data: {
|
||||||
|
'party_id': partyId,
|
||||||
|
'damage': damage,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> leaveParty(String partyId, String userId) async {
|
||||||
|
try {
|
||||||
|
final result = await _pb.collection('party_members').getList(
|
||||||
|
filter: 'party_id="$partyId" && user_id="$userId"',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.items.isNotEmpty) {
|
||||||
|
final memberId = result.items.first.id;
|
||||||
|
await _pb.collection('party_members').delete(memberId);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error leaving party: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/src/features/multiplayer/domain/entities/party.dart
Normal file
18
lib/src/features/multiplayer/domain/entities/party.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'party.freezed.dart';
|
||||||
|
part 'party.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class Party with _$Party {
|
||||||
|
const factory Party({
|
||||||
|
required String id,
|
||||||
|
required String code,
|
||||||
|
@JsonKey(name: 'host_id') required String hostId,
|
||||||
|
required String status, // 'waiting', 'active', 'finished'
|
||||||
|
@Default(0) @JsonKey(name: 'current_hp') int currentHp,
|
||||||
|
@Default(0) @JsonKey(name: 'max_hp') int maxHp,
|
||||||
|
}) = _Party;
|
||||||
|
|
||||||
|
factory Party.fromJson(Map<String, dynamic> json) => _$PartyFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:slrpg_app/src/features/gamification/domain/entities/avatar_config.dart';
|
||||||
|
|
||||||
|
part 'party_member.freezed.dart';
|
||||||
|
part 'party_member.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class PartyMember with _$PartyMember {
|
||||||
|
const factory PartyMember({
|
||||||
|
required String id,
|
||||||
|
@JsonKey(name: 'user_id') required String userId,
|
||||||
|
@JsonKey(name: 'is_ready') required bool isReady,
|
||||||
|
@JsonKey(includeFromJson: false) String? username,
|
||||||
|
@JsonKey(includeFromJson: false) AvatarConfig? avatar,
|
||||||
|
}) = _PartyMember;
|
||||||
|
|
||||||
|
factory PartyMember.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$PartyMemberFromJson(json);
|
||||||
|
|
||||||
|
factory PartyMember.fromRecord(Map<String, dynamic> json) {
|
||||||
|
var member = PartyMember.fromJson(json);
|
||||||
|
|
||||||
|
if (json['expand'] != null && json['expand']['user_id'] != null) {
|
||||||
|
final userMap = json['expand']['user_id'] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
AvatarConfig? avatarConfig;
|
||||||
|
if (userMap['avatar_config'] != null) {
|
||||||
|
try {
|
||||||
|
avatarConfig = AvatarConfig.fromJson(userMap['avatar_config']);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return member.copyWith(
|
||||||
|
username: userMap['name'] ?? userMap['username'] ?? 'Unknown',
|
||||||
|
avatar: avatarConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:slrpg_app/src/features/workout_runner/application/workout_generator_service.dart';
|
||||||
|
import 'package:slrpg_app/src/shared/data/repositories/workout_repository.dart';
|
||||||
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
|
import '../../data/repositories/party_repository.dart';
|
||||||
|
import '../../domain/entities/party.dart';
|
||||||
|
import '../../domain/entities/party_member.dart';
|
||||||
|
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
||||||
|
import '../../../../shared/data/repositories/cycle_repository.dart';
|
||||||
|
|
||||||
|
class LobbyScreen extends ConsumerStatefulWidget {
|
||||||
|
final String partyId;
|
||||||
|
|
||||||
|
const LobbyScreen({super.key, required this.partyId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<LobbyScreen> createState() => _LobbyScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LobbyScreenState extends ConsumerState<LobbyScreen> {
|
||||||
|
Future<void> _leaveParty() async {
|
||||||
|
final currentUser = await ref.read(userRepositoryProvider).getLocalUser();
|
||||||
|
if (currentUser != null) {
|
||||||
|
await ref
|
||||||
|
.read(partyRepositoryProvider)
|
||||||
|
.leaveParty(widget.partyId, currentUser.serverId!);
|
||||||
|
}
|
||||||
|
if (mounted) context.go('/hub');
|
||||||
|
}
|
||||||
|
|
||||||
|
int _calculateRaidHp(int memberCount) {
|
||||||
|
const int baseVolumePerPlayer = 50;
|
||||||
|
return (baseVolumePerPlayer * memberCount * 1.1).round();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final partyAsync = ref.watch(partyStreamProvider(widget.partyId));
|
||||||
|
final membersAsync = ref.watch(partyMembersStreamProvider(widget.partyId));
|
||||||
|
final currentUser = ref.watch(userRepositoryProvider).getLocalUser();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('RAID LOBBY'),
|
||||||
|
centerTitle: true,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: _leaveParty,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: partyAsync.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (err, _) => Center(child: Text('Error: $err')),
|
||||||
|
data: (party) {
|
||||||
|
if (party.status == 'active') {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: _findNextWorkout(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done &&
|
||||||
|
snapshot.hasData) {
|
||||||
|
final next = snapshot.data as Map<String, int>;
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
context.go('/battle/${party.id}',
|
||||||
|
extra: {'week': next['week'], 'day': next['day']});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return const Center(child: Text('Entering Battle...'));
|
||||||
|
}
|
||||||
|
return const Center(
|
||||||
|
child: Text('Raid is starting... Preparing workout...'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
color: AppTheme.surfaceColor,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text('PARTY CODE',
|
||||||
|
style: TextStyle(color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: party.code));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Code copied!')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
party.code,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 8,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text('(Tap to copy)',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: membersAsync.when(
|
||||||
|
loading: () =>
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) =>
|
||||||
|
Center(child: Text('Error loading members: $e')),
|
||||||
|
data: (members) {
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: members.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final member = members[index];
|
||||||
|
return _MemberCard(member: member);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: currentUser,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) return const SizedBox();
|
||||||
|
final myId = snapshot.data!.serverId;
|
||||||
|
final isHost = party.hostId == myId;
|
||||||
|
|
||||||
|
final myMember = membersAsync.value
|
||||||
|
?.where((m) => m.userId == myId)
|
||||||
|
.firstOrNull;
|
||||||
|
final isReady = myMember?.isReady ?? false;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppTheme.surfaceColor,
|
||||||
|
border: Border(top: BorderSide(color: Colors.white10)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
isReady ? Colors.grey : AppTheme.primaryColor,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(partyRepositoryProvider)
|
||||||
|
.setReady(party.id, !isReady);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
isReady ? 'READY' : 'NOT READY',
|
||||||
|
style:
|
||||||
|
const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isHost) ...[
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.redAccent,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
final members = membersAsync.value ?? [];
|
||||||
|
final memberCount = members.length;
|
||||||
|
final raidHp = _calculateRaidHp(memberCount);
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(partyRepositoryProvider)
|
||||||
|
.startRaid(party.id, customHp: raidHp);
|
||||||
|
// ref
|
||||||
|
// .read(partyRepositoryProvider)
|
||||||
|
// .startRaid(party.id);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'START RAID',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, int>> _findNextWorkout() async {
|
||||||
|
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||||
|
final workoutRepo = ref.read(workoutRepositoryProvider);
|
||||||
|
|
||||||
|
final activeCycle = await cycleRepo.getCurrentCycle();
|
||||||
|
|
||||||
|
int targetWeek = 1;
|
||||||
|
int targetDay = 1;
|
||||||
|
|
||||||
|
if (activeCycle != null) {
|
||||||
|
final cycleId = activeCycle.serverId ?? activeCycle.id.toString();
|
||||||
|
|
||||||
|
for (int w = 1; w <= 4; w++) {
|
||||||
|
for (int d = 1; d <= 4; d++) {
|
||||||
|
final workout = await workoutRepo.getWorkoutByWeekDay(
|
||||||
|
cycleId: cycleId, week: w, day: d);
|
||||||
|
|
||||||
|
if (workout == null || workout.completedAt == null) {
|
||||||
|
return {'week': w, 'day': d};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {'week': 1, 'day': 1};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {'week': 1, 'day': 1};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MemberCard extends StatelessWidget {
|
||||||
|
final PartyMember member;
|
||||||
|
|
||||||
|
const _MemberCard({required this.member});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(
|
||||||
|
leading: SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: member.avatar != null
|
||||||
|
? AvatarRenderer(config: member.avatar!, size: 40)
|
||||||
|
: const CircleAvatar(child: Icon(Icons.person)),
|
||||||
|
),
|
||||||
|
title: Text(member.username ?? 'Unknown'),
|
||||||
|
trailing: member.isReady
|
||||||
|
? const Icon(Icons.check_circle, color: Colors.green)
|
||||||
|
: const Icon(Icons.circle_outlined, color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final partyStreamProvider =
|
||||||
|
StreamProvider.autoDispose.family<Party, String>((ref, partyId) {
|
||||||
|
return ref.watch(partyRepositoryProvider).subscribeToParty(partyId);
|
||||||
|
});
|
||||||
|
|
||||||
|
final partyMembersStreamProvider = StreamProvider.autoDispose
|
||||||
|
.family<List<PartyMember>, String>((ref, partyId) {
|
||||||
|
return ref.watch(partyRepositoryProvider).subscribeToMembers(partyId);
|
||||||
|
});
|
||||||
|
|
@ -23,16 +23,23 @@ import '../widgets/emom_timer_widget.dart';
|
||||||
import '../widgets/timer_widget.dart';
|
import '../widgets/timer_widget.dart';
|
||||||
import '../../../wiki/presentation/widgets/exercise_guide_sheet.dart';
|
import '../../../wiki/presentation/widgets/exercise_guide_sheet.dart';
|
||||||
|
|
||||||
|
// --- NEUE IMPORTS FÜR MULTIPLAYER ---
|
||||||
|
import '../../../multiplayer/data/repositories/party_repository.dart';
|
||||||
|
import '../../../multiplayer/presentation/screens/lobby_screen.dart'; // Für partyStreamProvider
|
||||||
|
// ------------------------------------
|
||||||
|
|
||||||
class BattleScreen extends ConsumerStatefulWidget {
|
class BattleScreen extends ConsumerStatefulWidget {
|
||||||
final int week;
|
final int week;
|
||||||
final int day;
|
final int day;
|
||||||
final int? workoutId;
|
final int? workoutId;
|
||||||
|
final String? partyId; // Multiplayer Party ID
|
||||||
|
|
||||||
const BattleScreen({
|
const BattleScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.week,
|
required this.week,
|
||||||
required this.day,
|
required this.day,
|
||||||
this.workoutId,
|
this.workoutId,
|
||||||
|
this.partyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -52,6 +59,11 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
if (widget.partyId != null) {
|
||||||
|
debugPrint("⚔️ MULTIPLAYER BATTLE STARTED! Party ID: ${widget.partyId}");
|
||||||
|
} else {
|
||||||
|
debugPrint("👤 SINGLEPLAYER BATTLE");
|
||||||
|
}
|
||||||
_loadWorkout();
|
_loadWorkout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +92,14 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
final currentExercise = _exercises[_currentExerciseIndex];
|
final currentExercise = _exercises[_currentExerciseIndex];
|
||||||
final currentSet = currentExercise.sets[_currentSetIndex];
|
final currentSet = currentExercise.sets[_currentSetIndex];
|
||||||
|
|
||||||
|
// --- MULTIPLAYER HOOK: Schaden senden ---
|
||||||
|
if (widget.partyId != null) {
|
||||||
|
// Bei EMOM ist das Target meist fix, wir nehmen das als Damage
|
||||||
|
final damage = currentSet.repsTarget;
|
||||||
|
ref.read(partyRepositoryProvider).dealDamage(widget.partyId!, damage);
|
||||||
|
}
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
final updatedSet = currentSet.copyWith(
|
final updatedSet = currentSet.copyWith(
|
||||||
repsActual: currentSet.repsTarget,
|
repsActual: currentSet.repsTarget,
|
||||||
completed: true,
|
completed: true,
|
||||||
|
|
@ -117,6 +137,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Map<String, dynamic>> _getExerciseConfig(int day, UserCollection user) {
|
List<Map<String, dynamic>> _getExerciseConfig(int day, UserCollection user) {
|
||||||
|
// ... (Code bleibt identisch) ...
|
||||||
final variants = user.exerciseVariants ?? {};
|
final variants = user.exerciseVariants ?? {};
|
||||||
|
|
||||||
Map<String, dynamic> getVariant(String slot, String defaultId,
|
Map<String, dynamic> getVariant(String slot, String defaultId,
|
||||||
|
|
@ -188,6 +209,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadWorkout() async {
|
Future<void> _loadWorkout() async {
|
||||||
|
// ... (Code bleibt identisch) ...
|
||||||
final userRepo = ref.read(userRepositoryProvider);
|
final userRepo = ref.read(userRepositoryProvider);
|
||||||
final workoutRepo = ref.read(workoutRepositoryProvider);
|
final workoutRepo = ref.read(workoutRepositoryProvider);
|
||||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||||
|
|
@ -276,6 +298,15 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _completeSet() {
|
void _completeSet() {
|
||||||
|
// --- MULTIPLAYER HOOK: Schaden senden ---
|
||||||
|
if (widget.partyId != null) {
|
||||||
|
// _repsCompleted enthält hier bereits die eingegebene Anzahl (auch bei AMRAP)
|
||||||
|
ref
|
||||||
|
.read(partyRepositoryProvider)
|
||||||
|
.dealDamage(widget.partyId!, _repsCompleted);
|
||||||
|
}
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
final currentExercise = _exercises[_currentExerciseIndex];
|
final currentExercise = _exercises[_currentExerciseIndex];
|
||||||
final currentSet = currentExercise.sets[_currentSetIndex];
|
final currentSet = currentExercise.sets[_currentSetIndex];
|
||||||
|
|
||||||
|
|
@ -387,13 +418,13 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
await questService.reportEvent(QuestTrigger.repCount, data: totalReps);
|
await questService.reportEvent(QuestTrigger.repCount, data: totalReps);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.workoutId != null) {
|
|
||||||
final workoutRepo = ref.read(workoutRepositoryProvider);
|
final workoutRepo = ref.read(workoutRepositoryProvider);
|
||||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||||
final cycle = await cycleRepo.getCurrentCycle();
|
final cycle = await cycleRepo.getCurrentCycle();
|
||||||
|
|
||||||
final cycleIdRef = cycle?.serverId ?? cycle?.id.toString() ?? '';
|
final cycleIdRef = cycle?.serverId ?? cycle?.id.toString() ?? '';
|
||||||
|
|
||||||
|
if (widget.workoutId != null) {
|
||||||
var workout = await workoutRepo.getWorkoutByWeekDay(
|
var workout = await workoutRepo.getWorkoutByWeekDay(
|
||||||
cycleId: cycleIdRef, week: widget.week, day: widget.day);
|
cycleId: cycleIdRef, week: widget.week, day: widget.day);
|
||||||
|
|
||||||
|
|
@ -402,12 +433,32 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
final updatedWorkout = workout.copyWith(exercises: updatedExercises);
|
final updatedWorkout = workout.copyWith(exercises: updatedExercises);
|
||||||
|
|
||||||
await workoutRepo.completeWorkout(updatedWorkout, xpEarned: xpEarned);
|
await workoutRepo.completeWorkout(updatedWorkout, xpEarned: xpEarned);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final exercisesJson = _exercises.map((e) => e.toJson()).toList();
|
||||||
|
|
||||||
|
await workoutRepo.createCompletedWorkout(
|
||||||
|
cycleId: cycleIdRef,
|
||||||
|
week: widget.week,
|
||||||
|
day: widget.day,
|
||||||
|
exercises: exercisesJson,
|
||||||
|
xpEarned: xpEarned,
|
||||||
|
durationSeconds: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
ref.read(syncServiceProvider).sync();
|
ref.read(syncServiceProvider).sync();
|
||||||
}
|
|
||||||
}
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
if (widget.partyId != null) {
|
||||||
|
final userId =
|
||||||
|
(await ref.read(userRepositoryProvider).getLocalUser())?.serverId;
|
||||||
|
if (userId != null) {
|
||||||
|
await ref
|
||||||
|
.read(partyRepositoryProvider)
|
||||||
|
.leaveParty(widget.partyId!, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -446,6 +497,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showLevelUpDialog(int oldLevel, int newLevel) {
|
void _showLevelUpDialog(int oldLevel, int newLevel) {
|
||||||
|
// ... (Code bleibt identisch) ...
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -602,8 +654,19 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
child: Text(l10n.cancelButton),
|
child: Text(l10n.cancelButton),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
if (widget.partyId != null) {
|
||||||
|
final userId = (await ref
|
||||||
|
.read(userRepositoryProvider)
|
||||||
|
.getLocalUser())
|
||||||
|
?.serverId;
|
||||||
|
if (userId != null) {
|
||||||
|
await ref
|
||||||
|
.read(partyRepositoryProvider)
|
||||||
|
.leaveParty(widget.partyId!, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
context.go('/hub');
|
context.go('/hub');
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
|
|
@ -643,6 +706,39 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NEUES WIDGET: MULTIPLAYER LIVE HP BAR ---
|
||||||
|
Widget _buildMultiplayerHpBar() {
|
||||||
|
final partyAsync = ref.watch(partyStreamProvider(widget.partyId!));
|
||||||
|
|
||||||
|
return partyAsync.when(
|
||||||
|
data: (party) {
|
||||||
|
if (party.status == 'finished') {
|
||||||
|
return Container(
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green, borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: const Center(
|
||||||
|
child: Text('BOSS DEFEATED!',
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return EnemyHPBar(
|
||||||
|
current: party.currentHp,
|
||||||
|
max: party.maxHp > 0 ? party.maxHp : 1000,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () =>
|
||||||
|
const SizedBox(height: 20, child: LinearProgressIndicator()),
|
||||||
|
error: (_, __) => const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
child: Text('Connection lost',
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 10))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... (buildRestScreen und _buildNextSetPlates bleiben identisch) ...
|
||||||
Widget _buildRestScreen(Map<String, dynamic> inventory) {
|
Widget _buildRestScreen(Map<String, dynamic> inventory) {
|
||||||
final nextExerciseInfo = _exercises[_currentExerciseIndex];
|
final nextExerciseInfo = _exercises[_currentExerciseIndex];
|
||||||
final nextSet = nextExerciseInfo.sets[_currentSetIndex];
|
final nextSet = nextExerciseInfo.sets[_currentSetIndex];
|
||||||
|
|
@ -841,10 +937,14 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
const Icon(Icons.favorite,
|
const Icon(Icons.favorite,
|
||||||
color: AppTheme.errorColor, size: 24),
|
color: AppTheme.errorColor, size: 24),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
EnemyHPBar(
|
// --- MULTIPLAYER CHECK: Zeige Live oder Lokale HP ---
|
||||||
|
widget.partyId != null
|
||||||
|
? _buildMultiplayerHpBar()
|
||||||
|
: EnemyHPBar(
|
||||||
current: totalHP - completedHP,
|
current: totalHP - completedHP,
|
||||||
max: totalHP,
|
max: totalHP,
|
||||||
),
|
),
|
||||||
|
// ----------------------------------------------------
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -994,12 +1094,14 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatTime(int seconds) {
|
String _formatTime(int seconds) {
|
||||||
|
// ... (Code bleibt identisch) ...
|
||||||
final minutes = seconds ~/ 60;
|
final minutes = seconds ~/ 60;
|
||||||
final secs = seconds % 60;
|
final secs = seconds % 60;
|
||||||
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
|
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showAmrapDialog(WorkoutSet set) {
|
void _showAmrapDialog(WorkoutSet set) {
|
||||||
|
// ... (Code bleibt identisch) ...
|
||||||
int tempReps = set.repsTarget;
|
int tempReps = set.repsTarget;
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
|
@ -1144,6 +1246,14 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
width: 80,
|
width: 80,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// --- MULTIPLAYER CHECK BEI EMOM ---
|
||||||
|
widget.partyId != null
|
||||||
|
? Container(
|
||||||
|
height: 40,
|
||||||
|
child: _buildMultiplayerHpBar(),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${totalHP - completedHP}/$totalHP HP',
|
'${totalHP - completedHP}/$totalHP HP',
|
||||||
|
|
@ -1167,6 +1277,9 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
// ----------------------------------
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -1213,6 +1326,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _adjustEmomSets(int newTotalSets) {
|
void _adjustEmomSets(int newTotalSets) {
|
||||||
|
// ... (Code bleibt identisch) ...
|
||||||
final currentEx = _exercises[_currentExerciseIndex];
|
final currentEx = _exercises[_currentExerciseIndex];
|
||||||
|
|
||||||
if (newTotalSets == currentEx.sets.length) return;
|
if (newTotalSets == currentEx.sets.length) return;
|
||||||
|
|
@ -1247,6 +1361,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showEmomFinishDialog() {
|
void _showEmomFinishDialog() {
|
||||||
|
// ... (Code bleibt identisch) ...
|
||||||
final currentEx = _exercises[_currentExerciseIndex];
|
final currentEx = _exercises[_currentExerciseIndex];
|
||||||
int setsCount = currentEx.sets.length;
|
int setsCount = currentEx.sets.length;
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
@ -1351,6 +1466,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... _InfoBox und _CounterButton bleiben identisch ...
|
||||||
class _InfoBox extends StatelessWidget {
|
class _InfoBox extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String value;
|
||||||
|
|
|
||||||
|
|
@ -176,10 +176,16 @@ class ApiClient {
|
||||||
);
|
);
|
||||||
|
|
||||||
final token = response.data['token'];
|
final token = response.data['token'];
|
||||||
|
final record = response.data['record'];
|
||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
await _storage.write(key: AppConstants.keyAuthToken, value: token);
|
await _storage.write(key: AppConstants.keyAuthToken, value: token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (record != null && record['id'] != null) {
|
||||||
|
await _storage.write(key: AppConstants.keyUserId, value: record['id']);
|
||||||
|
}
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.e('Login failed', error: e);
|
_logger.e('Login failed', error: e);
|
||||||
|
|
@ -212,6 +218,17 @@ class ApiClient {
|
||||||
'avatar_config': avatarConfig ?? {},
|
'avatar_config': avatarConfig ?? {},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final token = response.data['token'];
|
||||||
|
final record = response.data['record'];
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
await _storage.write(key: AppConstants.keyAuthToken, value: token);
|
||||||
|
}
|
||||||
|
if (record != null && record['id'] != null) {
|
||||||
|
await _storage.write(key: AppConstants.keyUserId, value: record['id']);
|
||||||
|
}
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.e('Registration failed', error: e);
|
_logger.e('Registration failed', error: e);
|
||||||
|
|
@ -371,4 +388,12 @@ class ApiClient {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> getToken() async {
|
||||||
|
return await _storage.read(key: AppConstants.keyAuthToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getUserId() async {
|
||||||
|
return await _storage.read(key: AppConstants.keyUserId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,29 @@ class WorkoutRepository {
|
||||||
.write(companion);
|
.write(companion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> createCompletedWorkout({
|
||||||
|
required String cycleId,
|
||||||
|
required int week,
|
||||||
|
required int day,
|
||||||
|
required List<Map<String, dynamic>> exercises,
|
||||||
|
required int xpEarned,
|
||||||
|
required int durationSeconds,
|
||||||
|
}) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final companion = WorkoutsCompanion.insert(
|
||||||
|
userId: (await apiClient.getUserId()) ?? 'local',
|
||||||
|
cycleId: cycleId,
|
||||||
|
week: week,
|
||||||
|
day: day,
|
||||||
|
exercises: exercises,
|
||||||
|
xpEarned: Value(xpEarned),
|
||||||
|
completedAt: Value(now),
|
||||||
|
updatedAt: Value(now),
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.into(db.workouts).insert(companion);
|
||||||
|
}
|
||||||
|
|
||||||
Future<WorkoutCollection?> getWorkoutByWeekDay({
|
Future<WorkoutCollection?> getWorkoutByWeekDay({
|
||||||
required String cycleId,
|
required String cycleId,
|
||||||
String? localCycleId,
|
String? localCycleId,
|
||||||
|
|
|
||||||
|
|
@ -797,6 +797,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
pocketbase:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pocketbase
|
||||||
|
sha256: e6aca23d3181a23d367c6650fc4ebe6855bfba4d18b3746ceb1974fb152d8cf9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.19.1"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ dependencies:
|
||||||
# Networking
|
# Networking
|
||||||
dio: ^5.4.3+1
|
dio: ^5.4.3+1
|
||||||
pretty_dio_logger: ^1.3.1
|
pretty_dio_logger: ^1.3.1
|
||||||
|
pocketbase: ^0.19.0
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
flutter_secure_storage: ^10.0.0
|
flutter_secure_storage: ^10.0.0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue