zhipeng 1 год назад
Родитель
Сommit
6001dfcc72

BIN
assets/anim/anim_recording.zip


+ 2 - 0
assets/color/color.xml

@@ -6,4 +6,6 @@
     <color name="primaryTextColor">#25262A</color>
     <color name="secondaryTextColor">#5F5F61</color>
     <color name="tertiaryTextColor">#AFAFAF</color>
+
+    <color name="recordBackgroundColor">#25262A</color>
 </resources>

BIN
assets/images/icon_record_add_shortcut.webp


BIN
assets/images/icon_record_cancel_disable.webp


BIN
assets/images/icon_record_cancel_enable.webp


BIN
assets/images/icon_record_logo.webp


BIN
assets/images/icon_record_pause.webp


BIN
assets/images/icon_record_replay_pause.webp


BIN
assets/images/icon_record_replay_start.webp


BIN
assets/images/icon_record_save_disable.webp


BIN
assets/images/icon_record_save_enable.webp


BIN
assets/images/icon_record_start.webp


+ 16 - 0
lib/base/base_page.dart

@@ -1,3 +1,5 @@
+import 'dart:ui';
+
 import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -14,6 +16,8 @@ abstract class BasePage<T extends BaseController> extends GetView<T> {
       value: SystemUiOverlayStyle.light.copyWith(
         statusBarColor: Colors.transparent, // 设置状态栏颜色为透明
         statusBarIconBrightness: _getStatusBarDarkFont(),
+        systemNavigationBarColor: navigationBarColor(),
+        systemNavigationBarIconBrightness: _getNavigationBarDarkFont(),
       ),
       child: GestureDetector(
         onTap: () {
@@ -37,10 +41,18 @@ abstract class BasePage<T extends BaseController> extends GetView<T> {
     return statusBarDarkFont() ? Brightness.dark : Brightness.light;
   }
 
+  Brightness _getNavigationBarDarkFont() {
+    return statusBarDarkFont() ? Brightness.dark : Brightness.light;
+  }
+
   bool statusBarDarkFont() {
     return true;
   }
 
+  bool navigationBarDarkFont() {
+    return true;
+  }
+
   bool immersive() {
     return false;
   }
@@ -50,4 +62,8 @@ abstract class BasePage<T extends BaseController> extends GetView<T> {
   Color backgroundColor() {
     return ColorName.bgColorPrimary;
   }
+
+  Color navigationBarColor() {
+    return ColorName.bgColorPrimary;
+  }
 }

+ 1 - 1
lib/module/chat/view.dart

@@ -35,7 +35,7 @@ class ChatPage extends BasePage<ChatController> {
           backgroundColor: Colors.transparent,
           appBar: AppBar(
             leading: IconButton(
-              icon: const Icon(Icons.arrow_back_ios),
+              icon: const Icon(Icons.arrow_back_ios_new_rounded),
               onPressed: () {
                 Navigator.pop(context);
               },

+ 1 - 3
lib/module/home/view.dart

@@ -253,9 +253,7 @@ class HomePage extends BasePage<HomePageController> {
   SliverToBoxAdapter buildGoRecordView() {
     return SliverToBoxAdapter(
         child: GestureDetector(
-      onTap: () {
-        showUnfinishedRecordPopup();
-      },
+      onTap: () => Get.toNamed(RoutePath.record),
       child: Container(
         margin: EdgeInsets.only(right: 8.w),
         decoration: BoxDecoration(

+ 10 - 0
lib/module/record/controller.dart

@@ -1,8 +1,18 @@
 import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:get/get.dart';
+
+import '../../widget/frame_animation_view.dart';
 
 class RecordController extends BaseController {
+  final FrameAnimationController frameAnimationController =
+      FrameAnimationController();
+
+  final RxString recordStatus = '准备开始录音'.obs;
+
   @override
   void onInit() {
     super.onInit();
   }
+
+  void addShortcut() {}
 }

+ 183 - 2
lib/module/record/view.dart

@@ -1,12 +1,193 @@
 import 'package:electronic_assistant/base/base_page.dart';
 import 'package:electronic_assistant/module/record/controller.dart';
+import 'package:electronic_assistant/resource/colors.gen.dart';
+import 'package:electronic_assistant/utils/expand.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+
+import '../../resource/assets.gen.dart';
+import '../../widget/frame_animation_view.dart';
 
 class RecordPage extends BasePage<RecordController> {
   const RecordPage({super.key});
 
   @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  bool statusBarDarkFont() {
+    return false;
+  }
+
+  @override
+  Color backgroundColor() {
+    return ColorName.recordBackgroundColor;
+  }
+
+  @override
+  Color navigationBarColor() {
+    return "#4A4F67".color;
+  }
+
+  @override
   Widget buildBody(BuildContext context) {
-    throw UnimplementedError();
+    return Stack(alignment: Alignment.bottomCenter, children: [
+      _buildBottomGradient(),
+      Scaffold(
+        appBar: AppBar(
+          leading: IconButton(
+            icon: const Icon(Icons.arrow_back_ios_new_rounded),
+            color: ColorName.white,
+            onPressed: () {
+              Navigator.pop(context);
+            },
+          ),
+          scrolledUnderElevation: 0,
+          backgroundColor: ColorName.transparent,
+          systemOverlayStyle: SystemUiOverlayStyle.light,
+          actions: [
+            _buildAddShortcut(false),
+          ],
+        ),
+        backgroundColor: ColorName.transparent,
+        body: Flex(
+          direction: Axis.vertical,
+          children: [
+            _buildRecordStatus(),
+            const Spacer(flex: 271),
+            _buildRecordAnim(),
+            const Spacer(flex: 407),
+            _buildRecordControl(),
+          ],
+        ),
+      ),
+    ]);
+  }
+
+  Widget _buildAddShortcut(bool visible) {
+    return GestureDetector(
+      onTap: () {
+        controller.addShortcut();
+      },
+      child: Visibility(
+        visible: visible,
+        child: Row(
+          children: [
+            Image(
+                image: Assets.images.iconRecordAddShortcut.provider(),
+                width: 24.w,
+                height: 24.w),
+            Padding(
+              padding: EdgeInsets.only(left: 8.w, right: 16.w),
+              child: Text(
+                '添加到桌面',
+                style: TextStyle(color: ColorName.white, fontSize: 14.w),
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildRecordStatus() {
+    return Container(
+      padding: EdgeInsets.symmetric(horizontal: 16.w),
+      margin: EdgeInsets.only(top: 20.w),
+      child: Row(
+        children: [
+          Container(
+            margin: EdgeInsets.only(right: 8.w),
+            child: Image(
+                image: Assets.images.iconRecordLogo.provider(),
+                width: 45.w,
+                height: 48.w),
+          ),
+          Obx(() {
+            return Text(
+              controller.recordStatus.value,
+              style: TextStyle(color: ColorName.white, fontSize: 17.w),
+            );
+          }),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildRecordAnim() {
+    return FrameAnimationView(
+      framePath: 'assets/anim/anim_recording.zip',
+      speed: 2,
+      width: 360.w,
+      height: 180.w,
+    );
+  }
+
+  Widget _buildRecordControl() {
+    return Stack(
+      alignment: Alignment.bottomCenter,
+      children: [
+        Container(
+          padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 30.w),
+          decoration: BoxDecoration(
+            color: "#4A4F67".color,
+            borderRadius: BorderRadius.only(
+                topLeft: Radius.circular(40.w),
+                topRight: Radius.circular(40.w)),
+          ),
+          child: Row(
+            children: [
+              Image(
+                  image: Assets.images.iconRecordCancelDisable.provider(),
+                  width: 56.w,
+                  height: 56.w),
+              const Spacer(),
+              Image(
+                  image: Assets.images.iconRecordSaveDisable.provider(),
+                  width: 56.w,
+                  height: 56.w),
+            ],
+          ),
+        ),
+        Column(
+          children: [
+            Image(
+                image: Assets.images.iconRecordStart.provider(),
+                width: 92.w,
+                height: 92.w),
+            Padding(
+              padding: EdgeInsets.only(top: 10.w, bottom: 35.w),
+              child: Text(
+                "00:00:00",
+                style: TextStyle(
+                  color: ColorName.white,
+                  fontSize: 16.w,
+                ),
+              ),
+            )
+          ],
+        )
+      ],
+    );
+  }
+
+  Widget _buildBottomGradient() {
+    return Container(
+      height: 0.38.sh,
+      decoration: BoxDecoration(
+        gradient: LinearGradient(
+          begin: Alignment.topCenter,
+          end: Alignment.bottomCenter,
+          colors: [
+            "#006177F2".color,
+            "#806177F2".color,
+          ],
+        ),
+      ),
+    );
   }
-}
+}

+ 1 - 1
lib/module/splash/view.dart

@@ -17,7 +17,7 @@ class SplashPage extends BasePage {
       Get.offNamed(RoutePath.mainTab);
     });
     return GestureDetector(
-        child: Center(
+        child: const Center(
             child: Text(
       '启屏页',
       style: TextStyle(fontSize: 36),

+ 1 - 1
lib/module/talk/view.dart

@@ -24,7 +24,7 @@ class TalkPage extends BasePage<TalkController> {
     Get.toNamed(RoutePath.talkDetail, arguments: item);
   }
 
-  final pages = [SummaryView(), TodoView(), OriginalView()];
+  final pages = [const SummaryView(), const TodoView(), const OriginalView()];
 
   @override
   Widget buildBody(BuildContext context) {

+ 1 - 1
lib/router/app_pages.dart

@@ -72,6 +72,6 @@ final generalPages = [
   GetPage(name: RoutePath.chat, page: () => const ChatPage()),
   GetPage(name: RoutePath.task, page: () => const TaskPage()),
   GetPage(name: RoutePath.taskSearch, page: () => const TaskSearchPage()),
-  GetPage(name: RoutePath.record, page: () => const RecordPage()),
+  GetPage(name: RoutePath.record, page: () =>  const RecordPage()),
   GetPage(name: RoutePath.talkDetail, page: () => TalkPage()),
 ];

+ 8 - 3
lib/utils/expand.dart

@@ -1,9 +1,14 @@
 import 'dart:ui';
 
 extension HexColor on String {
+  Color get color => toColor();
+
   Color toColor() {
-    final hexCode = replaceAll('#', '');
-    return Color(int.parse('FF$hexCode', radix: 16));
+    String hex = replaceAll('#', '');
+    if (hex.length == 6) {
+      hex = 'FF$hex';
+    }
+    return Color(int.parse(hex, radix: 16));
   }
 }
 
@@ -48,4 +53,4 @@ extension StringExtensions on String {
     }
     return result;
   }
-}
+}

+ 39 - 23
lib/widget/frame_animation_view.dart

@@ -22,14 +22,13 @@ class FrameAnimationView extends StatefulWidget {
 
   final double? height;
 
-  const FrameAnimationView(
-      {super.key,
-      required this.framePath,
-      this.frameRate = 25,
-      this.controller,
-      this.speed,
-      this.width,
-      this.height});
+  const FrameAnimationView({super.key,
+    required this.framePath,
+    this.frameRate = 25,
+    this.controller,
+    this.speed,
+    this.width,
+    this.height});
 
   @override
   State<StatefulWidget> createState() {
@@ -50,9 +49,10 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
     loadFrameFromAssets()
         .then((_) => initializeImageFiles())
         .then((_) => precacheImageFiles())
-        .then((_) => widget.controller == null || widget.controller!.autoPlay
-            ? startAnimation()
-            : null)
+        .then((_) =>
+    widget.controller == null || widget.controller!.autoPlay
+        ? startAnimation()
+        : null)
         .catchError((error) => debugPrint('FrameAnimationView error: $error'));
   }
 
@@ -60,6 +60,11 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
   void dispose() {
     super.dispose();
     _timer?.cancel();
+    imageFiles.clear();
+    for (var image in images) {
+      image.dispose();
+    }
+    images.clear();
   }
 
   @override
@@ -97,8 +102,12 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
     final Directory cacheDir = await createCacheDirectory();
 
     // if the cache directory is not empty and frame images count is equal to the zip file's files count
-    if (cacheDir.listSync().isNotEmpty &&
-        cacheDir.listSync().length == archive.length) {
+    if (cacheDir
+        .listSync()
+        .isNotEmpty &&
+        cacheDir
+            .listSync()
+            .length == archive.length) {
       return;
     }
 
@@ -120,7 +129,7 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
   Future<void> copyToCache() async {
     // Read AssetManifest.json file
     final String manifestContent =
-        await rootBundle.loadString('AssetManifest.json');
+    await rootBundle.loadString('AssetManifest.json');
 
     // Parse JSON string into Map
     final Map<String, dynamic> manifestMap = json.decode(manifestContent);
@@ -134,8 +143,12 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
     final Directory cacheDir = await createCacheDirectory();
 
     // if the cache directory is not empty and frame images count is equal to the zip file's files count
-    if (cacheDir.listSync().isNotEmpty &&
-        cacheDir.listSync().length == filePaths.length) {
+    if (cacheDir
+        .listSync()
+        .isNotEmpty &&
+        cacheDir
+            .listSync()
+            .length == filePaths.length) {
       return;
     }
 
@@ -143,7 +156,9 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
     for (final String path in filePaths) {
       final ByteData data = await rootBundle.load(path);
       final List<int> bytes = data.buffer.asUint8List();
-      final File newFile = File('${cacheDir.path}/${path.split('/').last}');
+      final File newFile = File('${cacheDir.path}/${path
+          .split('/')
+          .last}');
       if (newFile.existsSync()) {
         newFile.deleteSync();
       } else {
@@ -168,7 +183,8 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
 
     // create unique directory by framePath's md5
     final Directory frameDir = Directory(
-        '${cacheDir.path}/${md5.convert(utf8.encode(widget.framePath)).toString()}');
+        '${cacheDir.path}/${md5.convert(utf8.encode(widget.framePath))
+            .toString()}');
 
     // Check if the directory exists, if not, create it
     if (!await frameDir.exists()) {
@@ -194,7 +210,7 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
       setState(() {
         int targetFrame = (_currentFrame + 1) % imageFiles.length;
         _currentFrame =
-            targetFrame >= images.length ? images.length - 1 : targetFrame;
+        targetFrame >= images.length ? images.length - 1 : targetFrame;
       });
     });
   }
@@ -206,10 +222,10 @@ class FrameAnimationViewState extends State<FrameAnimationView> {
         .listSync()
         .map((file) => File(file.path))
         .where((file) =>
-            file.path.endsWith('.png') ||
-            file.path.endsWith('.jpg') ||
-            file.path.endsWith('.jpeg') ||
-            file.path.endsWith('.webp'))
+    file.path.endsWith('.png') ||
+        file.path.endsWith('.jpg') ||
+        file.path.endsWith('.jpeg') ||
+        file.path.endsWith('.webp'))
         .toList();
 
     if (imageFiles.isEmpty) {