소스 검색

[fit]修改ui给的动画,增加隐私分析遮罩,修改存储环形

云天逵 9 달 전
부모
커밋
759bcddd45

+ 3 - 3
lib/data/repositories/user_repository.dart

@@ -6,7 +6,6 @@ import '../api/atmob_api.dart';
 import '../api/response/user_info_response.dart';
 
 class UserRepository {
-
   UserRepository._();
 
   Rxn<UserInfoResponse> get userInfo => _userInfo;
@@ -44,11 +43,12 @@ class UserRepository {
     }
 
     // 将时间戳转换为DateTime并比较
-    final endTime = DateTime.fromMillisecondsSinceEpoch(memberInfo.endTimestamp ?? 0);
+    final endTime =
+        DateTime.fromMillisecondsSinceEpoch(memberInfo.endTimestamp ?? 0);
     final now = DateTime.now();
 
     return now.isBefore(endTime);
   }
 }
 
-final userRepository = UserRepository._();
+final userRepository = UserRepository._();

+ 53 - 5
lib/module/home/home_controller.dart

@@ -14,6 +14,7 @@ import 'package:clean/module/screenshots_blurry/screenshots_view.dart';
 import 'package:clean/module/similar_photo/similar_photo_view.dart';
 import 'package:clean/router/app_pages.dart';
 import 'package:clean/utils/toast_util.dart';
+import 'package:flutter/Material.dart';
 import 'package:get/get.dart';
 import 'package:permission_handler/permission_handler.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
@@ -22,6 +23,7 @@ import '../../data/api/response/user_info_response.dart';
 import '../../data/consts/event_report_id.dart';
 import '../../data/repositories/config_repository.dart';
 import '../../handler/event_handler.dart';
+import '../../widget/multi_segment_circle_indicator.dart';
 
 class HomeController extends BaseController {
   Rx<double> totalSpace = 0.0.obs;
@@ -71,6 +73,12 @@ class HomeController extends BaseController {
 
   UserInfoResponse? get userInfo => userRepository.userInfo.value;
 
+  List<PieData> get pieDataList => [
+        PieData("PhotoSpace", photoSpacePercentage, Colors.blue),
+        PieData("OtherUsed", usedSpacePercentage - photoSpacePercentage, Colors.red),
+        PieData("Unused", freeSpacePercentage, Colors.grey.withOpacity(0.1)),
+      ];
+
   @override
   Future<void> onInit() async {
     // TODO: implement onInit
@@ -78,10 +86,19 @@ class HomeController extends BaseController {
 
     if (Platform.isAndroid) {
       await loadPhotosFromDirectory();
+      // 延迟3秒
+      Future.delayed(const Duration(seconds: 3), () {
+        isStorageScanned.value = true;
+        totalSpace.value = 100.0;
+        usedSpace.value = 50.0;
+        photoSpace.value = 30.0;
+        freeSpace.value = 50.0;
+      });
 
-    }
 
 
+    }
+
     if (await Permission.photos.request().isGranted) {
       PhotoManager.clearFileCache();
       getStorageInfo();
@@ -129,15 +146,46 @@ class HomeController extends BaseController {
       try {
         final List<AssetEntity> result = await ImagePickerUtil.loadAssets();
         ImagePickerUtil.peoplePhotos.value = result ?? [];
-
+        if (ImagePickerUtil.peoplePhotos.isNotEmpty) {
+          for (var personPhotos in ImagePickerUtil.peoplePhotos) {
+            peoplePhotos.add(personPhotos);
+            if (peoplePhotos.length == 2) {
+              break;
+            }
+          }
+        }
         ImagePickerUtil.locationPhotos['location'] = result ?? [];
-
+        if (ImagePickerUtil.locationPhotos.isNotEmpty) {
+          // 获取第一个地点的第一张照片
+          final firstLocationPhotos =
+              ImagePickerUtil.locationPhotos.values.first;
+          if (firstLocationPhotos.isNotEmpty) {
+            var asset = firstLocationPhotos.first;
+            locationPhoto.value = asset;
+          }
+        }
         ImagePickerUtil.screenshotPhotos.value = result ?? [];
+        if (ImagePickerUtil.screenshotPhotos.isNotEmpty) {
+          var asset = ImagePickerUtil.screenshotPhotos.first;
+          screenshotPhoto.value = asset;
+        }
 
         ImagePickerUtil.similarPhotos.add(result ?? []);
-
+        if (ImagePickerUtil.similarPhotos.isNotEmpty) {
+          for (var group in ImagePickerUtil.similarPhotos) {
+            for (var asset in group) {
+              similarPhotos.add(asset);
+              if (similarPhotos.length == 4) {
+                break;
+              }
+            }
+          }
+        }
         ImagePickerUtil.blurryPhotos.value = result ?? [];
-
+        if (ImagePickerUtil.blurryPhotos.isNotEmpty) {
+          var asset = ImagePickerUtil.blurryPhotos.first;
+          blurryPhoto.value = asset;
+        }
 
       } catch (e) {
         print('Error loading photos: $e');

+ 151 - 218
lib/module/home/home_view.dart

@@ -1,19 +1,16 @@
-import 'dart:io';
-
 import 'package:clean/base/base_view.dart';
 import 'package:clean/module/home/home_controller.dart';
 import 'package:clean/module/image_picker/image_picker_util.dart';
 import 'package:clean/resource/assets.gen.dart';
 import 'package:clean/resource/string.gen.dart';
-import 'package:clean/router/app_pages.dart';
-import 'package:clean/utils/image_util.dart';
-import 'package:get/get.dart';
 import 'package:flutter/Material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
 import 'package:lottie/lottie.dart';
-import 'package:syncfusion_flutter_charts/charts.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
+import '../../widget/multi_segment_circle_indicator.dart';
+
 class HomePage extends BaseView<HomeController> {
   const HomePage({super.key});
 
@@ -105,97 +102,26 @@ class HomePage extends BaseView<HomeController> {
 
   Widget circularChartCard() {
     return SizedBox(
-      width: 120.00.w,
+      width: 120.w,
       child: Obx(() {
-        return SfCircularChart(
-          series: <CircularSeries>[
-            DoughnutSeries<PieData, String>(
-              dataSource: [
-                PieData('photo Space', controller.photoSpacePercentage,
-                    Colors.blue),
-                PieData(
-                    'Used Space',
-                    controller.usedSpacePercentage -
-                        controller.photoSpacePercentage,
-                    Colors.red),
-                PieData(
-                  'Unused Space',
-                  controller.isStorageScanned.value
-                      ? controller.freeSpacePercentage
-                      : 100,
-                  Colors.white.withValues(alpha: 0.10000000149011612),
-                ),
-              ],
-              xValueMapper: (PieData data, _) => data.label,
-              yValueMapper: (PieData data, _) => data.value,
-              pointColorMapper: (PieData data, _) => data.color,
-              cornerStyle: CornerStyle.bothFlat,
-              radius: '100%',
-              // 设置饼图的半径
-              innerRadius: '80%',
-              // 设置饼图的内半径
-              startAngle: 0,
-              // 设置开始角度
-              endAngle: 360, // 设置结束角度
-            ),
-          ],
-          annotations: <CircularChartAnnotation>[
-            CircularChartAnnotation(
-              widget: Container(
-                  child: Column(
-                mainAxisSize: MainAxisSize.min,
-                crossAxisAlignment: CrossAxisAlignment.center,
-                children: [
-                  Row(
-                    mainAxisAlignment: MainAxisAlignment.center,
-                    crossAxisAlignment: CrossAxisAlignment.end,
-                    children: [
-                      Obx(() {
-                        return Text(
-                          controller.isStorageScanned.value
-                              ? controller.usedSpacePercentage
-                                  .toStringAsFixed(0)
-                              : "0",
-                          textAlign: TextAlign.end,
-                          style: TextStyle(
-                            color: Colors.white
-                                .withValues(alpha: 0.8999999761581421),
-                            fontSize: 30.sp,
-                            height: 1,
-                            fontWeight: FontWeight.w400,
-                          ),
-                        );
-                      }),
-                      Text(
-                        '%',
-                        textAlign: TextAlign.end,
-                        style: TextStyle(
-                          color: Colors.white
-                              .withValues(alpha: 0.8999999761581421),
-                          fontSize: 14.87.r,
-                          fontWeight: FontWeight.w500,
-                        ),
-                      ),
-                    ],
-                  ),
-                  Text(
-                    controller.isStorageScanned.value ? 'used' : 'Scanning...',
-                    textAlign: TextAlign.center,
-                    style: TextStyle(
-                      color: Colors.white.withValues(alpha: 0.6000000238418579),
-                      fontSize:
-                          controller.isStorageScanned.value ? 14.87.sp : 12.sp,
-                      height: 1,
-                      fontWeight: FontWeight.w500,
-                    ),
-                  )
-                ],
-              )),
-              horizontalAlignment: ChartAlignment.center,
-              verticalAlignment: ChartAlignment.center,
-              radius: '0%',
-            ),
-          ],
+        final isScanned = controller.isStorageScanned.value;
+
+        if (!isScanned) {
+          return MultiSegmentCircleIndicator(
+            strokeWidth: 12.r,
+            segments: [PieData("Scanning", 100, Colors.grey.withOpacity(0.1))],
+            subtitle: "Scanning...",
+            animationDuration: Duration.zero,
+            centerValue: 0,
+          );
+        }
+        return MultiSegmentCircleIndicator(
+          key: ValueKey(isScanned),
+          strokeWidth: 12.r,
+          segments: controller.pieDataList,
+          subtitle: "used",
+          centerValue: controller.usedSpacePercentage,
+          animationDuration: const Duration(seconds: 2),
         );
       }),
     );
@@ -372,130 +298,136 @@ class HomePage extends BaseView<HomeController> {
   }
 
   Widget similarCard() {
-    return GestureDetector(onTap: () {
-      controller.similarCleanClick();
-    },
+    return GestureDetector(
+        onTap: () {
+          controller.similarCleanClick();
+        },
         child: Container(
-      width: 328.w,
-      height: 155.h,
-      margin: EdgeInsets.only(top: 20.h),
-      padding: EdgeInsets.symmetric(horizontal: 16.w),
-      decoration: ShapeDecoration(
-        color: Colors.white.withValues(alpha: 0.12),
-        shape: RoundedRectangleBorder(
-          borderRadius: BorderRadius.circular(16.r),
-        ),
-      ),
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          SizedBox(height: 12.h),
-          Row(
-            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          width: 328.w,
+          height: 155.h,
+          margin: EdgeInsets.only(top: 20.h),
+          padding: EdgeInsets.symmetric(horizontal: 16.w),
+          decoration: ShapeDecoration(
+            color: Colors.white.withValues(alpha: 0.12),
+            shape: RoundedRectangleBorder(
+              borderRadius: BorderRadius.circular(16.r),
+            ),
+          ),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
             children: [
-              Column(
-                crossAxisAlignment: CrossAxisAlignment.start,
+              SizedBox(height: 12.h),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                 children: [
-                  Text(
-                    'Similar',
-                    style: TextStyle(
-                      color: Colors.white,
-                      fontSize: 16.sp,
-                      fontWeight: FontWeight.w700,
-                    ),
-                  ),
-                  Obx(() {
-                    return Text.rich(
-                      TextSpan(
-                        children: [
-                          TextSpan(
-                            text: "${ImagePickerUtil.similarPhotoCount.value}",
-                            style: TextStyle(
-                              color: Colors.white,
-                              fontSize: 12.sp,
-                              fontWeight: FontWeight.w400,
-                            ),
-                          ),
+                  Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Text(
+                        'Similar',
+                        style: TextStyle(
+                          color: Colors.white,
+                          fontSize: 16.sp,
+                          fontWeight: FontWeight.w700,
+                        ),
+                      ),
+                      Obx(() {
+                        return Text.rich(
                           TextSpan(
-                            text: ' duplicate photos detected',
-                            style: TextStyle(
-                              color: Colors.white
-                                  .withValues(alpha: 0.800000011920929),
-                              fontSize: 12.sp,
-                              fontWeight: FontWeight.w400,
-                            ),
+                            children: [
+                              TextSpan(
+                                text:
+                                    "${ImagePickerUtil.similarPhotoCount.value}",
+                                style: TextStyle(
+                                  color: Colors.white,
+                                  fontSize: 12.sp,
+                                  fontWeight: FontWeight.w400,
+                                ),
+                              ),
+                              TextSpan(
+                                text: ' duplicate photos detected',
+                                style: TextStyle(
+                                  color: Colors.white
+                                      .withValues(alpha: 0.800000011920929),
+                                  fontSize: 12.sp,
+                                  fontWeight: FontWeight.w400,
+                                ),
+                              ),
+                            ],
                           ),
-                        ],
-                      ),
+                        );
+                      }),
+                    ],
+                  ),
+                  Obx(() {
+                    return CleanUpButton(
+                      label: !controller.isScanned.value
+                          ? 'Scanning...'
+                          : 'Clean up',
+                      size: ImagePickerUtil.formatFileSize(
+                          ImagePickerUtil.similarPhotosSize.value),
+                      onTap: () {
+                        controller.similarCleanClick();
+                      },
                     );
                   }),
                 ],
               ),
+              // SizedBox(height: 19.h),
+              Spacer(),
               Obx(() {
-                return CleanUpButton(
-                  label:
-                  !controller.isScanned.value ? 'Scanning...' : 'Clean up',
-                  size: ImagePickerUtil.formatFileSize(
-                      ImagePickerUtil.similarPhotosSize.value),
-                  onTap: () {
-                    controller.similarCleanClick();
-                  },
-                );
-              }),
-            ],
-          ),
-          // SizedBox(height: 19.h),
-          Spacer(),
-          Obx(() {
-            return Row(
-              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-              children: List.generate(4, (index) {
-                var image = Assets.images.iconHomeNoPhoto.image(
-                  width: 70.w * 0.45,
-                  height: 70.w * 0.45,
-                );
-                if (controller.similarPhotos.length > index) {
-                  image = AssetEntityImage(
-                      width: 70.w,
-                      height: 70.w,
-                      controller.similarPhotos[index],
-                      isOriginal: false,
-                      thumbnailSize: const ThumbnailSize.square(300),
-                      fit: BoxFit.cover,
-                      errorBuilder: (context, error, stackTrace) {
+                return Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: List.generate(4, (index) {
+                    var image = Assets.images.iconHomeNoPhoto.image(
+                      width: 70.w * 0.45,
+                      height: 70.w * 0.45,
+                    );
+                    if (controller.similarPhotos.length > index) {
+                      image = AssetEntityImage(
+                          width: 70.w,
+                          height: 70.w,
+                          controller.similarPhotos[index],
+                          isOriginal: false,
+                          thumbnailSize: const ThumbnailSize.square(300),
+                          fit: BoxFit.cover,
+                          errorBuilder: (context, error, stackTrace) {
                         return Assets.images.iconHomeNoPhoto.image(
                           width: 70.w * 0.45,
                           height: 70.w * 0.45,
                         );
                       });
-                }
-                return ImageContainer(
-                  size: 70.w,
-                  image: Opacity(
-                    opacity: 0.22,
-                    child: const CircularProgressIndicator(color: Colors.white38,)
-                  ),
-                  // AssetEntityImage(
-                  //         width: 70.w,
-                  //         height: 70.w,
-                  //         controller.similarPhotos[index],
-                  //         isOriginal: false,
-                  //         thumbnailSize: const ThumbnailSize.square(300),
-                  //         fit: BoxFit.cover,
-                  //         errorBuilder: (context, error, stackTrace) {
-                  //           return Assets.images.iconHomeNoPhoto.image(
-                  //             width: 70.w * 0.45,
-                  //             height: 70.w * 0.45,
-                  //           );
-                  //         },
+                    }
+                    return controller.similarPhotos.isNotEmpty
+                        ? image
+                        : ImageContainer(
+                            size: 70.w,
+                            image: Opacity(
+                              opacity: 0.22,
+                              child: Lottie.asset(Assets.anim.animNoPhoto,
+                                  repeat: true, width: 100.w, height: 100.w),
+                            ),
+                            // AssetEntityImage(
+                            //         width: 70.w,
+                            //         height: 70.w,
+                            //         controller.similarPhotos[index],
+                            //         isOriginal: false,
+                            //         thumbnailSize: const ThumbnailSize.square(300),
+                            //         fit: BoxFit.cover,
+                            //         errorBuilder: (context, error, stackTrace) {
+                            //           return Assets.images.iconHomeNoPhoto.image(
+                            //             width: 70.w * 0.45,
+                            //             height: 70.w * 0.45,
+                            //           );
+                            //         },
+                          );
+                  }),
                 );
               }),
-            );
-          }),
-          Spacer(),
-        ],
-      ),
-    ));
+              Spacer(),
+            ],
+          ),
+        ));
   }
 
   Widget quickPhotoCard() {
@@ -571,10 +503,15 @@ class HomePage extends BaseView<HomeController> {
                           });
                         }
                         return ImageContainer(
-                          image: Opacity(
-                            opacity: 0.22,
-                            child: const CircularProgressIndicator(color: Colors.white38,),
-                          ),
+                          image: controller.peoplePhotos.isNotEmpty
+                              ? image
+                              : Opacity(
+                                  opacity: 0.22,
+                                  child: Lottie.asset(Assets.anim.animNoPhoto,
+                                      repeat: true,
+                                      width: 140.w,
+                                      height: 140.w),
+                                ),
                           size: 146.w,
                           // Image.file(
                           //   width: 146.w,
@@ -657,7 +594,8 @@ class HomePage extends BaseView<HomeController> {
                         child: controller.locationPhoto.value == null
                             ? Opacity(
                                 opacity: 0.22,
-                                child: const CircularProgressIndicator(color: Colors.white38,),
+                                child: Lottie.asset(Assets.anim.animNoPhoto,
+                                    repeat: true, width: 160.w, height: 160.w),
                               )
                             : AssetEntityImage(
                                 width: 304.w,
@@ -722,7 +660,9 @@ class HomePage extends BaseView<HomeController> {
               controller.screenshotPhoto.value == null
                   ? Opacity(
                       opacity: 0.22,
-                      child: const CircularProgressIndicator(color: Colors.white38,),
+                      child: const CircularProgressIndicator(
+                        color: Colors.white38,
+                      ),
                     )
                   : AssetEntityImage(
                       width: 144.w,
@@ -750,7 +690,8 @@ class HomePage extends BaseView<HomeController> {
                 controller.blurryPhoto.value == null
                     ? Opacity(
                         opacity: 0.22,
-                        child: const CircularProgressIndicator(color: Colors.white38,),
+                        child: Lottie.asset(Assets.anim.animNoPhoto,
+                            repeat: true, width: 100.w, height: 100.w),
                       )
                     : AssetEntityImage(
                         width: 144.w,
@@ -927,11 +868,3 @@ class ImageContainer extends StatelessWidget {
     );
   }
 }
-
-class PieData {
-  final String label;
-  final double value;
-  final Color color;
-
-  PieData(this.label, this.value, this.color);
-}

+ 5 - 0
lib/module/photo_info/photo_info_view.dart

@@ -70,6 +70,7 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
               Visibility(
                 visible: controller.type.value == FileType.analysis,
                 child: Positioned(
+
                   left: 0,
                   right: 0,
                   bottom: 0,
@@ -77,6 +78,10 @@ class PhotoInfoPage extends BasePage<PhotoInfoController> {
                     crossAxisAlignment: CrossAxisAlignment.start,
                     children: [
                       Container(
+                        decoration: BoxDecoration(
+                          color: Colors.black.withValues(alpha: 0.10), // 半透明黑色遮罩
+                          borderRadius: BorderRadius.circular(12.r),
+                        ),
                         margin: EdgeInsets.only(left: 18.w),
                         child: Obx(() {
                           return SingleChildScrollView(

+ 30 - 27
lib/utils/styles.dart

@@ -7,49 +7,52 @@ import '../resource/assets.gen.dart';
 class Styles {
   Styles._();
 
-  static ImageFrameBuilder? animFrameBuilder({double? opacity, double? width,
-    double? height, bool? repeat,}) {
-    return (context, child,
-        frame, wasSynchronouslyLoaded) {
+  static ImageFrameBuilder? animFrameBuilder({
+    double? opacity,
+    double? width,
+    double? height,
+    bool? repeat,
+  }) {
+    return (context, child, frame, wasSynchronouslyLoaded) {
       if (wasSynchronouslyLoaded) {
         return child;
       }
       return AnimatedSwitcher(
-          duration: const Duration(
-              milliseconds: 0),
+          duration: const Duration(milliseconds: 0),
           child: frame != null
               ? child
               : Center(
-              child: Opacity(
-                opacity: opacity ?? 0.22,
-                child: Lottie.asset(
-                    Assets
-                        .anim.animNoPhoto,
-                    width: width ?? 140.w,
-                    height: height ?? 140.w,
-                    repeat: repeat ?? true
-                ),
-              )));
+                  child: Opacity(
+                  opacity: opacity ?? 0.22,
+                  child: Lottie.asset(Assets.anim.animNoPhoto,
+                      width: width ?? 140.w,
+                      height: height ?? 140.w,
+                      repeat: repeat ?? true),
+                )));
     };
   }
 
-  static ImageFrameBuilder? customFrameBuilder({double? opacity, double? width,
-    double? height, bool? repeat,}) {
-    return (context, child,
-        frame, wasSynchronouslyLoaded) {
+  static ImageFrameBuilder? customFrameBuilder({
+    double? opacity,
+    double? width,
+    double? height,
+    bool? repeat,
+  }) {
+    return (context, child, frame, wasSynchronouslyLoaded) {
       if (wasSynchronouslyLoaded) {
         return child;
       }
       return AnimatedSwitcher(
-          duration: const Duration(
-              milliseconds: 0),
+          duration: const Duration(milliseconds: 0),
           child: frame != null
               ? child
               : Center(
-              child: Opacity(
-                opacity: opacity ?? 0.22,
-                child:const CircularProgressIndicator(color: Colors.white38,),
-              )));
+                  child: Opacity(
+                      opacity: opacity ?? 0.22,
+                      child: Lottie.asset(Assets.anim.animNoPhoto,
+                          width: width ?? 140.w,
+                          height: height ?? 140.w,
+                          repeat: repeat ?? true))));
     };
   }
-}
+}

+ 136 - 0
lib/widget/multi_segment_circle_indicator.dart

@@ -0,0 +1,136 @@
+import 'dart:ui';
+
+import 'package:flutter/Material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+class PieData {
+  final String label;
+  final double value;
+  final Color color;
+
+  PieData(this.label, this.value, this.color);
+}
+
+class MultiSegmentCircleIndicator extends StatelessWidget {
+  final List<PieData> segments;
+  final double radius;
+  final double strokeWidth;
+  final Duration animationDuration;
+  final String Function(double percent)? centerTextBuilder;
+  final String? subtitle;
+  final double centerValue;
+
+  const MultiSegmentCircleIndicator({
+    super.key,
+    required this.segments,
+    this.radius = 60,
+    this.strokeWidth = 12,
+    this.animationDuration = const Duration(seconds: 1),
+    this.centerTextBuilder,
+    required this.centerValue,
+    this.subtitle,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return TweenAnimationBuilder<double>(
+      tween: Tween<double>(begin: 0, end: 1),
+      duration: animationDuration,
+      builder: (context, value, child) {
+        final animatedSegments = segments
+            .map((e) => PieData(e.label, e.value * value, e.color))
+            .toList();
+
+        return Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            SizedBox(
+              width: radius * 2,
+              height: radius * 2,
+              child: CustomPaint(
+                painter: _CirclePainter(
+                  segments: animatedSegments,
+                  strokeWidth: strokeWidth,
+                ),
+                child: Center(
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    textBaseline: TextBaseline.alphabetic,
+                    children: [
+                      Text.rich(TextSpan(children: [
+                        TextSpan(
+                          text: centerTextBuilder?.call(centerValue * value) ??
+                              (centerValue * value).toStringAsFixed(0),
+                          style: TextStyle(
+                            color: Colors.white.withAlpha(229),
+                            fontSize: 30.sp,
+                            fontWeight: FontWeight.w400,
+                          ),
+                        ),
+                        TextSpan(
+                          text: '%',
+                          style: TextStyle(
+                            color: Colors.white.withAlpha(229),
+                            fontSize: 13.03.sp,
+                            fontWeight: FontWeight.w500,
+                          ),
+                        ),
+                      ])),
+                      if (subtitle != null)
+                        Text(
+                          subtitle!,
+                          style: TextStyle(
+                            fontSize: 14,
+                            color: Colors.white.withOpacity(0.6),
+                          ),
+                        ),
+                    ],
+                  ),
+                ),
+              ),
+            ),
+            const SizedBox(height: 12),
+          ],
+        );
+      },
+    );
+  }
+}
+
+class _CirclePainter extends CustomPainter {
+  final List<PieData> segments;
+  final double strokeWidth;
+
+  _CirclePainter({required this.segments, this.strokeWidth = 16});
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final center = size.center(Offset.zero);
+    final radius = (size.shortestSide - strokeWidth) / 2;
+
+    final paint = Paint()
+      ..style = PaintingStyle.stroke
+      ..strokeWidth = strokeWidth
+      ..strokeCap = StrokeCap.round;
+
+    double startAngle = -90.0;
+
+    for (final segment in segments) {
+      final sweepAngle = 360 * (segment.value / 100);
+      paint.color = segment.color;
+      canvas.drawArc(
+        Rect.fromCircle(center: center, radius: radius),
+        _toRadians(startAngle),
+        _toRadians(sweepAngle),
+        false,
+        paint,
+      );
+      startAngle += sweepAngle;
+    }
+  }
+
+  double _toRadians(double degrees) => degrees * 3.1415926 / 180;
+
+  @override
+  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
+}