diff --git a/client/deepdame/lib/features/profile/data/models/monthly_win_rate.dart b/client/deepdame/lib/features/profile/data/models/monthly_win_rate.dart new file mode 100644 index 0000000..ee99e3d --- /dev/null +++ b/client/deepdame/lib/features/profile/data/models/monthly_win_rate.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'monthly_win_rate.freezed.dart'; +part 'monthly_win_rate.g.dart'; + +@freezed +abstract class MonthlyWinRate with _$MonthlyWinRate { + const factory MonthlyWinRate({ + required String month, + required int played, + required int won, + required double winRate, + }) = _MonthlyWinRate; + + factory MonthlyWinRate.fromJson(Map json) => + _$MonthlyWinRateFromJson(json); +} diff --git a/client/deepdame/lib/features/profile/data/models/monthly_win_rate.freezed.dart b/client/deepdame/lib/features/profile/data/models/monthly_win_rate.freezed.dart new file mode 100644 index 0000000..6a667ec --- /dev/null +++ b/client/deepdame/lib/features/profile/data/models/monthly_win_rate.freezed.dart @@ -0,0 +1,286 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'monthly_win_rate.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$MonthlyWinRate { + + String get month; int get played; int get won; double get winRate; +/// Create a copy of MonthlyWinRate +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$MonthlyWinRateCopyWith get copyWith => _$MonthlyWinRateCopyWithImpl(this as MonthlyWinRate, _$identity); + + /// Serializes this MonthlyWinRate to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is MonthlyWinRate&&(identical(other.month, month) || other.month == month)&&(identical(other.played, played) || other.played == played)&&(identical(other.won, won) || other.won == won)&&(identical(other.winRate, winRate) || other.winRate == winRate)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,month,played,won,winRate); + +@override +String toString() { + return 'MonthlyWinRate(month: $month, played: $played, won: $won, winRate: $winRate)'; +} + + +} + +/// @nodoc +abstract mixin class $MonthlyWinRateCopyWith<$Res> { + factory $MonthlyWinRateCopyWith(MonthlyWinRate value, $Res Function(MonthlyWinRate) _then) = _$MonthlyWinRateCopyWithImpl; +@useResult +$Res call({ + String month, int played, int won, double winRate +}); + + + + +} +/// @nodoc +class _$MonthlyWinRateCopyWithImpl<$Res> + implements $MonthlyWinRateCopyWith<$Res> { + _$MonthlyWinRateCopyWithImpl(this._self, this._then); + + final MonthlyWinRate _self; + final $Res Function(MonthlyWinRate) _then; + +/// Create a copy of MonthlyWinRate +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? month = null,Object? played = null,Object? won = null,Object? winRate = null,}) { + return _then(_self.copyWith( +month: null == month ? _self.month : month // ignore: cast_nullable_to_non_nullable +as String,played: null == played ? _self.played : played // ignore: cast_nullable_to_non_nullable +as int,won: null == won ? _self.won : won // ignore: cast_nullable_to_non_nullable +as int,winRate: null == winRate ? _self.winRate : winRate // ignore: cast_nullable_to_non_nullable +as double, + )); +} + +} + + +/// Adds pattern-matching-related methods to [MonthlyWinRate]. +extension MonthlyWinRatePatterns on MonthlyWinRate { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _MonthlyWinRate value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _MonthlyWinRate() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _MonthlyWinRate value) $default,){ +final _that = this; +switch (_that) { +case _MonthlyWinRate(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _MonthlyWinRate value)? $default,){ +final _that = this; +switch (_that) { +case _MonthlyWinRate() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String month, int played, int won, double winRate)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _MonthlyWinRate() when $default != null: +return $default(_that.month,_that.played,_that.won,_that.winRate);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String month, int played, int won, double winRate) $default,) {final _that = this; +switch (_that) { +case _MonthlyWinRate(): +return $default(_that.month,_that.played,_that.won,_that.winRate);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String month, int played, int won, double winRate)? $default,) {final _that = this; +switch (_that) { +case _MonthlyWinRate() when $default != null: +return $default(_that.month,_that.played,_that.won,_that.winRate);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _MonthlyWinRate implements MonthlyWinRate { + const _MonthlyWinRate({required this.month, required this.played, required this.won, required this.winRate}); + factory _MonthlyWinRate.fromJson(Map json) => _$MonthlyWinRateFromJson(json); + +@override final String month; +@override final int played; +@override final int won; +@override final double winRate; + +/// Create a copy of MonthlyWinRate +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$MonthlyWinRateCopyWith<_MonthlyWinRate> get copyWith => __$MonthlyWinRateCopyWithImpl<_MonthlyWinRate>(this, _$identity); + +@override +Map toJson() { + return _$MonthlyWinRateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MonthlyWinRate&&(identical(other.month, month) || other.month == month)&&(identical(other.played, played) || other.played == played)&&(identical(other.won, won) || other.won == won)&&(identical(other.winRate, winRate) || other.winRate == winRate)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,month,played,won,winRate); + +@override +String toString() { + return 'MonthlyWinRate(month: $month, played: $played, won: $won, winRate: $winRate)'; +} + + +} + +/// @nodoc +abstract mixin class _$MonthlyWinRateCopyWith<$Res> implements $MonthlyWinRateCopyWith<$Res> { + factory _$MonthlyWinRateCopyWith(_MonthlyWinRate value, $Res Function(_MonthlyWinRate) _then) = __$MonthlyWinRateCopyWithImpl; +@override @useResult +$Res call({ + String month, int played, int won, double winRate +}); + + + + +} +/// @nodoc +class __$MonthlyWinRateCopyWithImpl<$Res> + implements _$MonthlyWinRateCopyWith<$Res> { + __$MonthlyWinRateCopyWithImpl(this._self, this._then); + + final _MonthlyWinRate _self; + final $Res Function(_MonthlyWinRate) _then; + +/// Create a copy of MonthlyWinRate +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? month = null,Object? played = null,Object? won = null,Object? winRate = null,}) { + return _then(_MonthlyWinRate( +month: null == month ? _self.month : month // ignore: cast_nullable_to_non_nullable +as String,played: null == played ? _self.played : played // ignore: cast_nullable_to_non_nullable +as int,won: null == won ? _self.won : won // ignore: cast_nullable_to_non_nullable +as int,winRate: null == winRate ? _self.winRate : winRate // ignore: cast_nullable_to_non_nullable +as double, + )); +} + + +} + +// dart format on diff --git a/client/deepdame/lib/features/profile/data/models/monthly_win_rate.g.dart b/client/deepdame/lib/features/profile/data/models/monthly_win_rate.g.dart new file mode 100644 index 0000000..92e8be9 --- /dev/null +++ b/client/deepdame/lib/features/profile/data/models/monthly_win_rate.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'monthly_win_rate.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_MonthlyWinRate _$MonthlyWinRateFromJson(Map json) => + _MonthlyWinRate( + month: json['month'] as String, + played: (json['played'] as num).toInt(), + won: (json['won'] as num).toInt(), + winRate: (json['winRate'] as num).toDouble(), + ); + +Map _$MonthlyWinRateToJson(_MonthlyWinRate instance) => + { + 'month': instance.month, + 'played': instance.played, + 'won': instance.won, + 'winRate': instance.winRate, + }; diff --git a/client/deepdame/lib/features/profile/data/repositories/win_trend_repository.dart b/client/deepdame/lib/features/profile/data/repositories/win_trend_repository.dart new file mode 100644 index 0000000..18d90a1 --- /dev/null +++ b/client/deepdame/lib/features/profile/data/repositories/win_trend_repository.dart @@ -0,0 +1,31 @@ +import 'package:deepdame/core/network/dio_client.dart'; +import 'package:deepdame/features/profile/data/models/monthly_win_rate.dart'; +import 'package:dio/dio.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'win_trend_repository.g.dart'; + +@riverpod +WinTrendRepository winTrendRepository(Ref ref) { + return WinTrendRepository(ref.watch(dioProvider)); +} + +class WinTrendRepository { + WinTrendRepository(this._dio); + + final Dio _dio; + + Future> getWinTrend( + String userId, { + int months = 6, + }) async { + final response = await _dio.get>( + '/api/v1/stats/$userId/win-trend', + queryParameters: {'months': months}, + ); + if (response.data == null) return []; + return response.data! + .map((e) => MonthlyWinRate.fromJson(e as Map)) + .toList(); + } +} diff --git a/client/deepdame/lib/features/profile/data/repositories/win_trend_repository.g.dart b/client/deepdame/lib/features/profile/data/repositories/win_trend_repository.g.dart new file mode 100644 index 0000000..9ea299a --- /dev/null +++ b/client/deepdame/lib/features/profile/data/repositories/win_trend_repository.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'win_trend_repository.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(winTrendRepository) +final winTrendRepositoryProvider = WinTrendRepositoryProvider._(); + +final class WinTrendRepositoryProvider + extends + $FunctionalProvider< + WinTrendRepository, + WinTrendRepository, + WinTrendRepository + > + with $Provider { + WinTrendRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'winTrendRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$winTrendRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + WinTrendRepository create(Ref ref) { + return winTrendRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(WinTrendRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$winTrendRepositoryHash() => + r'5fe70aae6cf171878b9da96b51fd2d09878703a2'; diff --git a/client/deepdame/lib/features/profile/presentation/providers/win_trend_provider.dart b/client/deepdame/lib/features/profile/presentation/providers/win_trend_provider.dart new file mode 100644 index 0000000..df6ee36 --- /dev/null +++ b/client/deepdame/lib/features/profile/presentation/providers/win_trend_provider.dart @@ -0,0 +1,12 @@ +import 'package:deepdame/features/profile/data/models/monthly_win_rate.dart'; +import 'package:deepdame/features/profile/data/repositories/win_trend_repository.dart'; +import 'package:deepdame/features/user/application/user_controller.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'win_trend_provider.g.dart'; + +@riverpod +Future> winTrend(Ref ref) async { + final user = await ref.watch(userControllerProvider.future); + return ref.watch(winTrendRepositoryProvider).getWinTrend(user.id); +} diff --git a/client/deepdame/lib/features/profile/presentation/providers/win_trend_provider.g.dart b/client/deepdame/lib/features/profile/presentation/providers/win_trend_provider.g.dart new file mode 100644 index 0000000..fa86c85 --- /dev/null +++ b/client/deepdame/lib/features/profile/presentation/providers/win_trend_provider.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'win_trend_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(winTrend) +final winTrendProvider = WinTrendProvider._(); + +final class WinTrendProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + WinTrendProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'winTrendProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$winTrendHash(); + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + return winTrend(ref); + } +} + +String _$winTrendHash() => r'0b896b92d019061bf904daea68739b892634245b'; diff --git a/client/deepdame/lib/features/profile/profile_screen.dart b/client/deepdame/lib/features/profile/profile_screen.dart index 2e2d90b..db63837 100644 --- a/client/deepdame/lib/features/profile/profile_screen.dart +++ b/client/deepdame/lib/features/profile/profile_screen.dart @@ -1,6 +1,7 @@ import 'package:deepdame/features/profile/widgets/profile_header_section.dart'; import 'package:deepdame/features/profile/widgets/profile_recent_games_section.dart'; import 'package:deepdame/features/profile/widgets/profile_stats_row.dart'; +import 'package:deepdame/features/profile/widgets/profile_win_rate_section.dart'; import 'package:flutter/material.dart'; class ProfileScreen extends StatelessWidget { @@ -13,28 +14,55 @@ class ProfileScreen extends StatelessWidget { return Scaffold( body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16), - Text( - 'Profile', - style: textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text( + 'Profile', + style: textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 40), + const Center(child: ProfileHeaderSection()), + const SizedBox(height: 20), + const ProfileStatsRow(), + const SizedBox(height: 28), + const ProfileWinRateSection(), + const SizedBox(height: 28), + Text( + 'Recent Games', + style: textTheme.titleSmall?.copyWith( + color: colorScheme.onSurface.withValues(alpha: 0.55), + fontWeight: FontWeight.w600, + letterSpacing: 0.2, + ), + ), + const SizedBox(height: 12), + ], + ), + ), + const Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 16), + physics: AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileRecentGamesSection(), + SizedBox(height: 24), + ], ), ), - const SizedBox(height: 40), - const Center(child: ProfileHeaderSection()), - const SizedBox(height: 20), - const ProfileStatsRow(), - const SizedBox(height: 28), - const ProfileRecentGamesSection(), - const SizedBox(height: 24), - ], - ), + ), + ], ), ), ); diff --git a/client/deepdame/lib/features/profile/widgets/profile_recent_games_section.dart b/client/deepdame/lib/features/profile/widgets/profile_recent_games_section.dart index 39d3642..46fe5b9 100644 --- a/client/deepdame/lib/features/profile/widgets/profile_recent_games_section.dart +++ b/client/deepdame/lib/features/profile/widgets/profile_recent_games_section.dart @@ -17,15 +17,6 @@ class ProfileRecentGamesSection extends ConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'RECENT GAMES', - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurface.withValues(alpha: 0.50), - fontWeight: FontWeight.w700, - letterSpacing: 0.8, - ), - ), - const SizedBox(height: 12), historyAsync.when( loading: () => const _GameHistorySkeleton(), error: (_, _) => _ErrorRow( diff --git a/client/deepdame/lib/features/profile/widgets/profile_win_rate_section.dart b/client/deepdame/lib/features/profile/widgets/profile_win_rate_section.dart new file mode 100644 index 0000000..b9cfb58 --- /dev/null +++ b/client/deepdame/lib/features/profile/widgets/profile_win_rate_section.dart @@ -0,0 +1,226 @@ +import 'package:deepdame/features/home/presentation/widgets/skeleton_wrapper.dart'; +import 'package:deepdame/features/profile/data/models/monthly_win_rate.dart'; +import 'package:deepdame/features/profile/presentation/providers/win_trend_provider.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ProfileWinRateSection extends ConsumerWidget { + const ProfileWinRateSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final trendAsync = ref.watch(winTrendProvider); + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Win Rate (6 months)', + style: textTheme.titleSmall?.copyWith( + color: colorScheme.onSurface.withValues(alpha: 0.55), + fontWeight: FontWeight.w600, + letterSpacing: 0.2, + ), + ), + const SizedBox(height: 12), + trendAsync.when( + loading: () => const _WinRateSkeleton(), + error: (_, _) => const SizedBox.shrink(), + data: (trend) => _WinRateChart(trend: trend), + ), + ], + ); + } +} + +class _WinRateChart extends StatelessWidget { + const _WinRateChart({required this.trend}); + + final List trend; + + static const _monthLabels = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + + String _label(String month) { + final m = int.tryParse(month.split('-').last) ?? 1; + return _monthLabels[m - 1]; + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Container( + padding: const EdgeInsets.fromLTRB(12, 16, 12, 8), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.onSurface.withValues(alpha: 0.09), + ), + ), + child: SizedBox( + height: 155, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: 100, + minY: 0, + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (_) => colorScheme.surfaceContainerHigh, + tooltipBorderRadius: BorderRadius.circular(8), + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final entry = trend[groupIndex]; + if (entry.played == 0) return null; + return BarTooltipItem( + '${rod.toY.toStringAsFixed(0)}%', + textTheme.labelSmall!.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w700, + ), + ); + }, + ), + ), + titlesData: FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + getTitlesWidget: (value, _) { + final i = value.toInt(); + if (i < 0 || i >= trend.length) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + _label(trend[i].month), + style: textTheme.labelSmall?.copyWith( + color: colorScheme.onSurface.withValues(alpha: 0.45), + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ); + }, + ), + ), + leftTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + topTitles: const AxisTitles(), + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + barGroups: trend.asMap().entries.map((e) { + final hasData = e.value.played > 0; + return BarChartGroupData( + x: e.key, + barRods: [ + BarChartRodData( + toY: hasData ? e.value.winRate * 100 : 4, + color: hasData ? colorScheme.primary : Colors.transparent, + width: 22, + borderRadius: BorderRadius.circular(6), + backDrawRodData: BackgroundBarChartRodData( + show: true, + toY: 100, + color: colorScheme.onSurface.withValues(alpha: 0.06), + ), + ), + ], + ); + }).toList(), + ), + ), + ), + ); + } +} + +class _WinRateSkeleton extends StatelessWidget { + const _WinRateSkeleton(); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final ph = colorScheme.onSurface.withValues(alpha: 0.08); + + return SkeletonWrapper( + child: Container( + padding: const EdgeInsets.fromLTRB(12, 16, 12, 8), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: ph), + ), + child: SizedBox( + height: 155, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _SkeletonBar(heightFraction: 0.45, ph: ph), + _SkeletonBar(heightFraction: 0.30, ph: ph), + _SkeletonBar(heightFraction: 0.60, ph: ph), + _SkeletonBar(heightFraction: 0.75, ph: ph), + _SkeletonBar(heightFraction: 0.55, ph: ph), + _SkeletonBar(heightFraction: 0.80, ph: ph), + ], + ), + ), + ), + ); + } +} + +class _SkeletonBar extends StatelessWidget { + const _SkeletonBar({required this.heightFraction, required this.ph}); + + final double heightFraction; + final Color ph; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + width: 22, + height: 118 * heightFraction, + decoration: BoxDecoration( + color: ph, + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(height: 8), + Container( + width: 20, + height: 10, + decoration: BoxDecoration( + color: ph, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ); + } +} diff --git a/client/deepdame/pubspec.yaml b/client/deepdame/pubspec.yaml index c342e28..fd0749e 100644 --- a/client/deepdame/pubspec.yaml +++ b/client/deepdame/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.0.21+22 +version: 2.0.22+23 environment: sdk: ^3.10.4