Leon 1 anno fa
parent
commit
c8b2ebf15b

+ 84 - 15
android/app/src/main/java/com/hola/MainActivity.java

@@ -11,6 +11,11 @@ import android.graphics.Color;
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioTrack;
+import android.media.MediaRecorder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -36,9 +41,13 @@ import com.google.android.gms.location.LocationServices;
 import com.google.android.gms.tasks.OnFailureListener;
 import com.google.android.gms.tasks.OnSuccessListener;
 
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.util.Calendar;
 import java.util.List;
 
+import java.io.File;
+
 public class MainActivity extends ReactActivity {
 
   private Handler handler;
@@ -83,28 +92,88 @@ public class MainActivity extends ReactActivity {
   }
 
 
-
-
   private void demo() {
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-      NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
-      List<NotificationChannel> channels = notificationManager.getNotificationChannels();
-      for (NotificationChannel channel : channels) {
-        if (channel.getImportance()!=NotificationManager.IMPORTANCE_NONE){
-          Log.i("aa","bbb");
-        }
-        else {
-          Log.i("cc","ddd");
-        }
-      }
-      if (channels.size() > 0) {
 
-      }
+  }
+
+  public void mixAudio(File inputFile1, File inputFile2, File outputFile) throws IOException {
+    int SAMPLE_RATE = 44100;
+    int BUFFER_SIZE = 1024;
+
+    int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE,
+            AudioFormat.CHANNEL_IN_STEREO,
+            AudioFormat.ENCODING_PCM_16BIT);
+    if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+      // TODO: Consider calling
+      //    ActivityCompat#requestPermissions
+      // here to request the missing permissions, and then overriding
+      //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
+      //                                          int[] grantResults)
+      // to handle the case where the user grants the permission. See the documentation
+      // for ActivityCompat#requestPermissions for more details.
+      return;
+    }
+    AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
+            SAMPLE_RATE,
+            AudioFormat.CHANNEL_IN_STEREO,
+            AudioFormat.ENCODING_PCM_16BIT,
+            bufferSize);
+
+    AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
+            SAMPLE_RATE,
+            AudioFormat.CHANNEL_OUT_STEREO,
+            AudioFormat.ENCODING_PCM_16BIT,
+            bufferSize,
+            AudioTrack.MODE_STREAM);
+
+    short[] buffer = new short[bufferSize];
+    FileOutputStream fos = new FileOutputStream(outputFile);
+
+    audioRecord.startRecording();
+    audioTrack.play();
+
+    while (/* condition to keep mixing */) {
+      // Read audio from microphone
+      int bufferReadResult = audioRecord.read(buffer, 0, bufferSize);
+      // Process audio (if needed)
+      // ...
+
+      // Write to output file
+      fos.write(buffer, 0, bufferReadResult);
+
+      // Optionally, read from another AudioRecord or file and mix with buffer
+      // ...
+
+      // Pass to AudioTrack
+      audioTrack.write(buffer, 0, bufferReadResult);
     }
+
+    audioRecord.stop();
+    audioTrack.stop();
+    fos.close();
   }
 
 
+  public static void deleteIfExists(String filePath) {
+    File file = new File(filePath);
+
+    // 判断文件是否存在
+    if (file.exists() && !file.isDirectory()) {
+      // 文件存在,并且不是目录,可以删除
+      boolean deleted = file.delete();
 
+      if (deleted) {
+        // 文件删除成功
+        Log.d("FileHelper", "文件删除成功: " + filePath);
+      } else {
+        // 文件删除失败
+        Log.d("FileHelper", "文件删除失败: " + filePath);
+      }
+    } else {
+      // 文件不存在
+      Log.d("FileHelper", "文件不存在: " + filePath);
+    }
+  }
   @Override
   public void onNewIntent(Intent intent) {
     super.onNewIntent(intent);

BIN
ios/1.m4a


BIN
ios/2.m4a


+ 192 - 102
ios/AppDelegate.mm

@@ -17,6 +17,7 @@
 #import <ReactCommon/RCTTurboModuleManager.h>
 
 #import <react/config/ReactNativeConfig.h>
+#import <AVFoundation/AVFoundation.h>
 
 static NSString *const kRNConcurrentRoot = @"concurrentRoot";
 
@@ -26,9 +27,14 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
   std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
   facebook::react::ContextContainer::Shared _contextContainer;
 }
+
 @end
 #endif
 
+@interface AppDelegate()
+@property (nonatomic, strong) AVAudioPlayer *audioPlayer;
+@end
+
 @implementation AppDelegate
 
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
@@ -58,7 +64,7 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
   
   RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
   
-//  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+  //  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
   center.delegate = self;
   
   
@@ -90,23 +96,107 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
   [super application:application didFinishLaunchingWithOptions:launchOptions];
   
   [self clearAllDeliveredNotifications];
+  
+  [self mixAudio];
   return YES;
 }
 
+- (void)mixAudio{
+  
+  NSError *error = nil;
+  AVMutableComposition *composition = [AVMutableComposition composition];
+  
+  // 创建音轨
+  AVMutableCompositionTrack *compositionTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
+  
+  // 声音文件路径
+  NSString *soundFilePath1 = [[NSBundle mainBundle] pathForResource:@"1" ofType:@"m4a"];
+  NSString *soundFilePath2 = [[NSBundle mainBundle] pathForResource:@"2" ofType:@"m4a"];
+  
+  // 加载音频文件
+  AVAsset *asset1 = [AVAsset assetWithURL:[NSURL fileURLWithPath:soundFilePath1]];
+  AVAsset *asset2 = [AVAsset assetWithURL:[NSURL fileURLWithPath:soundFilePath2]];
+  
+  // 获取音频文件的时长
+  CMTimeRange timeRange1 = CMTimeRangeMake(kCMTimeZero, asset1.duration);
+  CMTimeRange timeRange2 = CMTimeRangeMake(kCMTimeZero, asset2.duration);
+  
+  // 将音频文件的音轨添加到合成的音轨中
+  [compositionTrack insertTimeRange:timeRange1 ofTrack:[[asset1 tracksWithMediaType:AVMediaTypeAudio] firstObject] atTime:kCMTimeZero error:&error];
+  if (error) {
+    // 错误处理
+    NSLog(@"Error composing sound: %@", [error localizedDescription]);
+  }
+  
+  // 在第一段声音之后继续添加第二段声音
+  [compositionTrack insertTimeRange:timeRange2 ofTrack:[[asset2 tracksWithMediaType:AVMediaTypeAudio] firstObject] atTime:asset1.duration error:&error];
+  if (error) {
+    // 错误处理
+    NSLog(@"Error composing sound: %@", [error localizedDescription]);
+  }
+  
+  // 输出文件路径
+  NSString *outputFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"outputSound.m4a"];
+  NSFileManager *fileManager = [NSFileManager defaultManager];
+  BOOL fileExists = [fileManager fileExistsAtPath:outputFilePath];
+  if (fileExists) {
+    // 文件存在,删除文件
+    NSError *error;
+    BOOL success = [fileManager removeItemAtPath:outputFilePath error:&error];
+    
+    if (success) {
+      NSLog(@"文件删除成功");
+    } else {
+      // 处理错误
+      NSLog(@"文件删除失败: %@", [error localizedDescription]);
+    }
+  } else {
+    NSLog(@"文件不存在");
+  }
+  
+  NSURL *outputURL = [NSURL fileURLWithPath:outputFilePath];
+  
+  // 导出合成的音频
+  AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:composition presetName:AVAssetExportPresetAppleM4A];
+  exportSession.outputURL = outputURL;
+  exportSession.outputFileType = AVFileTypeAppleM4A;
+  
+  [exportSession exportAsynchronouslyWithCompletionHandler:^{
+    if (exportSession.status == AVAssetExportSessionStatusCompleted) {
+      // 导出成功,可以播放或者使用输出的文件
+      NSLog(@"Export success: %@", outputFilePath);
+      NSURL *fileURL = [NSURL fileURLWithPath:outputFilePath];
+      NSError *error;
+      self->_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error];
+      
+      if (error) {
+        NSLog(@"Error creating audio player: %@", [error localizedDescription]);
+      } else {
+        [self->_audioPlayer play];
+      }
+      
+    } else {
+      // 导出失败,处理错误
+      NSLog(@"Export failed: %@", [[exportSession error] localizedDescription]);
+    }
+  }];
+  
+}
+
 - (void)applicationDidBecomeActive:(UIApplication *)application {
   [UIApplication sharedApplication].applicationIconBadgeNumber = 0;
 }
 - (void)registerForPushNotifications {
-    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
-    [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error) {
-        if (granted) {
-            dispatch_async(dispatch_get_main_queue(), ^{
-                [[UIApplication sharedApplication] registerForRemoteNotifications];
-            });
-        } else {
-            // 用户拒绝通知或发生错误
-        }
-    }];
+  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+  [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error) {
+    if (granted) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        [[UIApplication sharedApplication] registerForRemoteNotifications];
+      });
+    } else {
+      // 用户拒绝通知或发生错误
+    }
+  }];
 }
 
 /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
@@ -130,7 +220,7 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
 
 - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
 {
-//  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
+  //  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
 #if DEBUG
   return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
 #else
@@ -158,14 +248,14 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
 }
 
 - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
-                                                      jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
+jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
 {
   return nullptr;
 }
 
 - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
-                                                     initParams:
-                                                         (const facebook::react::ObjCTurboModule::InitParams &)params
+initParams:
+(const facebook::react::ObjCTurboModule::InitParams &)params
 {
   return nullptr;
 }
@@ -181,7 +271,7 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
 
 //注册 APNS 成功并上报 DeviceToken
 - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
-//  [JPUSHService registerDeviceToken:deviceToken];
+  //  [JPUSHService registerDeviceToken:deviceToken];
 }
 
 - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{
@@ -192,8 +282,8 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
 - (void)application:(UIApplication *)application didReceiveRemoteNotification:  (NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
   // iOS 10 以下 Required
   NSLog(@"iOS 7 APNS");
-//  [JPUSHService handleRemoteNotification:userInfo];
-//  [[NSNotificationCenter defaultCenter] postNotificationName:J_APNS_NOTIFICATION_ARRIVED_EVENT object:userInfo];
+  //  [JPUSHService handleRemoteNotification:userInfo];
+  //  [[NSNotificationCenter defaultCenter] postNotificationName:J_APNS_NOTIFICATION_ARRIVED_EVENT object:userInfo];
   completionHandler(UIBackgroundFetchResultNewData);
 }
 
@@ -203,13 +293,13 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
   if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
     // Apns
     NSLog(@"iOS 10 APNS 前台收到消息");
-//    [JPUSHService handleRemoteNotification:userInfo];
-//    [[NSNotificationCenter defaultCenter] postNotificationName:J_APNS_NOTIFICATION_ARRIVED_EVENT object:userInfo];
+    //    [JPUSHService handleRemoteNotification:userInfo];
+    //    [[NSNotificationCenter defaultCenter] postNotificationName:J_APNS_NOTIFICATION_ARRIVED_EVENT object:userInfo];
   }
   else {
     // 本地通知 todo
     NSLog(@"iOS 10 本地通知 前台收到消息");
-//    [[NSNotificationCenter defaultCenter] postNotificationName:J_LOCAL_NOTIFICATION_ARRIVED_EVENT object:userInfo];
+    //    [[NSNotificationCenter defaultCenter] postNotificationName:J_LOCAL_NOTIFICATION_ARRIVED_EVENT object:userInfo];
   }
   //需要执行这个方法,选择是否提醒用户,有 Badge、Sound、Alert 三种类型可以选择设置
   completionHandler(UNNotificationPresentationOptionAlert);
@@ -228,7 +318,7 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
 
 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler
 {
-    //应用程序在后台,用户通过点击本地推送、远程推送进入app时调用此方法
+  //应用程序在后台,用户通过点击本地推送、远程推送进入app时调用此方法
   self.timestamp = [[NSDate date] timeIntervalSince1970]*1000;
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(postNotificationData:) userInfo:response repeats:YES];
 }
@@ -239,59 +329,59 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
   if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
     // Apns
     NSLog(@"iOS 10 APNS 消息事件回调");
-//    [JPUSHService handleRemoteNotification:userInfo];
-//    // 保障应用被杀死状态下,用户点击推送消息,打开app后可以收到点击通知事件
-//    [[RCTJPushEventQueue sharedInstance]._notificationQueue insertObject:userInfo atIndex:0];
-//    [[NSNotificationCenter defaultCenter] postNotificationName:J_APNS_NOTIFICATION_OPENED_EVENT object:userInfo];
+    //    [JPUSHService handleRemoteNotification:userInfo];
+    //    // 保障应用被杀死状态下,用户点击推送消息,打开app后可以收到点击通知事件
+    //    [[RCTJPushEventQueue sharedInstance]._notificationQueue insertObject:userInfo atIndex:0];
+    //    [[NSNotificationCenter defaultCenter] postNotificationName:J_APNS_NOTIFICATION_OPENED_EVENT object:userInfo];
   }
   else {
     // 本地通知
     NSLog(@"iOS 10 本地通知 消息事件回调");
     // 保障应用被杀死状态下,用户点击推送消息,打开app后可以收到点击通知事件
-//    [[RCTJPushEventQueue sharedInstance]._localNotificationQueue insertObject:userInfo atIndex:0];
-//    [[NSNotificationCenter defaultCenter] postNotificationName:J_LOCAL_NOTIFICATION_OPENED_EVENT object:userInfo];
+    //    [[RCTJPushEventQueue sharedInstance]._localNotificationQueue insertObject:userInfo atIndex:0];
+    //    [[NSNotificationCenter defaultCenter] postNotificationName:J_LOCAL_NOTIFICATION_OPENED_EVENT object:userInfo];
   }
   self.timestamp = [[NSDate date] timeIntervalSince1970]*1000;
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(postNotificationData:) userInfo:response repeats:YES];
-//  NSString *categoryIdentifier = response.notification.request.content.categoryIdentifier;
-//  [self.nativeBridge.bridge.eventDispatcher sendAppEventWithName:@"notificationReceive" body:@{@"category_id":categoryIdentifier,@"action_id":response.actionIdentifier}];
+  //  NSString *categoryIdentifier = response.notification.request.content.categoryIdentifier;
+  //  [self.nativeBridge.bridge.eventDispatcher sendAppEventWithName:@"notificationReceive" body:@{@"category_id":categoryIdentifier,@"action_id":response.actionIdentifier}];
   
   // 处理用户交互
-//      if ([response.actionIdentifier isEqualToString:@"ALLOW_ACTION"]) {
-//          // 用户点击了"允许"按钮,发起网络请求
-//        NSLog(@"User allow the request");
-//        
-////        [self.nativeBridge.bridge.eventDispatcher sendAppEventWithName:@"notificationReceive" body:@{@"name":@"1",@"value":@"2"}];
-//        
-////          [self makeNetworkRequest];
-//      } else if ([response.actionIdentifier isEqualToString:@"DENY_ACTION"]) {
-//          // 用户点击了"拒绝"按钮
-//          NSLog(@"User denied the request");
-////        [self makeNetworkRequest];
-//      } else if ([response.actionIdentifier isEqualToString:@"START_TIMER_NOW"]){
-//        if ([categoryIdentifier isEqualToString:@"REMINDER_FS_START_FAST"]){
-//          
-//        }
-//        else if ([categoryIdentifier isEqualToString:@"REMINDER_FS_START_SLEEP"]){
-//          
-//        }
-//      } else if ([response.actionIdentifier isEqualToString:@"PICK_EARLIER_START"]){
-//        if ([categoryIdentifier isEqualToString:@"REMINDER_FS_START_FAST"]){
-//          
-//        }
-//        else if ([categoryIdentifier isEqualToString:@"REMINDER_FS_START_SLEEP"]){
-//          
-//        }
-//      }
-//      else if ([response.actionIdentifier isEqualToString:@"END_TIMER_NOW"]){
-//        
-//      }
-//      else if ([response.actionIdentifier isEqualToString:@"PICK_EARLIER_END"]){
-//        
-//      }
-//      else if ([response.actionIdentifier isEqualToString:@"SKIP"]){
-//        
-//      }
+  //      if ([response.actionIdentifier isEqualToString:@"ALLOW_ACTION"]) {
+  //          // 用户点击了"允许"按钮,发起网络请求
+  //        NSLog(@"User allow the request");
+  //
+  ////        [self.nativeBridge.bridge.eventDispatcher sendAppEventWithName:@"notificationReceive" body:@{@"name":@"1",@"value":@"2"}];
+  //
+  ////          [self makeNetworkRequest];
+  //      } else if ([response.actionIdentifier isEqualToString:@"DENY_ACTION"]) {
+  //          // 用户点击了"拒绝"按钮
+  //          NSLog(@"User denied the request");
+  ////        [self makeNetworkRequest];
+  //      } else if ([response.actionIdentifier isEqualToString:@"START_TIMER_NOW"]){
+  //        if ([categoryIdentifier isEqualToString:@"REMINDER_FS_START_FAST"]){
+  //
+  //        }
+  //        else if ([categoryIdentifier isEqualToString:@"REMINDER_FS_START_SLEEP"]){
+  //
+  //        }
+  //      } else if ([response.actionIdentifier isEqualToString:@"PICK_EARLIER_START"]){
+  //        if ([categoryIdentifier isEqualToString:@"REMINDER_FS_START_FAST"]){
+  //
+  //        }
+  //        else if ([categoryIdentifier isEqualToString:@"REMINDER_FS_START_SLEEP"]){
+  //
+  //        }
+  //      }
+  //      else if ([response.actionIdentifier isEqualToString:@"END_TIMER_NOW"]){
+  //
+  //      }
+  //      else if ([response.actionIdentifier isEqualToString:@"PICK_EARLIER_END"]){
+  //
+  //      }
+  //      else if ([response.actionIdentifier isEqualToString:@"SKIP"]){
+  //
+  //      }
   // 系统要求执行这个方法
   completionHandler();
 }
@@ -333,25 +423,25 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
 - (void)makeNetworkRequest{
   //https://api.fast.dev.liveplus.fun/api/static-resource-urls
   NSURL *url = [NSURL URLWithString:@"https://api.fast.dev.liveplus.fun/api/static-resource-urls"];
-      NSURLRequest *request = [NSURLRequest requestWithURL:url];
-      
-      NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
-          if (error) {
-              NSLog(@"Error: %@", error.localizedDescription);
-          } else {
-              // 处理返回的数据
-              NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
-              NSLog(@"Response: %@", json);
-          }
-      }];
-      
-      [dataTask resume];
+  NSURLRequest *request = [NSURLRequest requestWithURL:url];
+  
+  NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
+    if (error) {
+      NSLog(@"Error: %@", error.localizedDescription);
+    } else {
+      // 处理返回的数据
+      NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
+      NSLog(@"Response: %@", json);
+    }
+  }];
+  
+  [dataTask resume];
 }
 
 //自定义消息
 - (void)networkDidReceiveMessage:(NSNotification *)notification {
   NSDictionary * userInfo = [notification userInfo];
-//  [[NSNotificationCenter defaultCenter] postNotificationName:J_CUSTOM_NOTIFICATION_EVENT object:userInfo];
+  //  [[NSNotificationCenter defaultCenter] postNotificationName:J_CUSTOM_NOTIFICATION_EVENT object:userInfo];
 }
 
 - (void)demo{
@@ -360,47 +450,47 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
   content.title = @"Daily Reminder";
   content.body = @"It's 9:00 AM, time to start your day!";
   content.sound = [UNNotificationSound defaultSound];
-
+  
   // 2. 设置触发条件 - 每天 9:00 AM
   NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
   dateComponents.hour = 9;
   dateComponents.minute = 0;
   UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents repeats:YES];
-
+  
   // 3. 创建 notification request
   UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"DailyReminder" content:content trigger:trigger];
-
+  
   // 4. 添加 notification request 到通知中心
   UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
   [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
-      if (error != nil) {
-          NSLog(@"Error adding notification request: %@", error);
-      }
+    if (error != nil) {
+      NSLog(@"Error adding notification request: %@", error);
+    }
   }];
 }
 
 - (void)scheduleCalendarNotificationWithTitle:(NSString *)title body:(NSString *)body date:(NSDate *)date repeats:(BOOL)repeats identifier:(NSString *)identifier {
-    NSLog(@"%s", __FUNCTION__);
-    UNMutableNotificationContent *content = [UNMutableNotificationContent new];
-    content.title = title;
-    content.body = body;
-
-    NSCalendar *calendar = NSCalendar.currentCalendar;
-    NSDateComponents *components = [calendar components:(NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond) fromDate:date];
-    UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:repeats];
-
-    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
-    [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
-        if (error) {
-            NSLog(@"%@", error);
-        }
-    }];
+  NSLog(@"%s", __FUNCTION__);
+  UNMutableNotificationContent *content = [UNMutableNotificationContent new];
+  content.title = title;
+  content.body = body;
+  
+  NSCalendar *calendar = NSCalendar.currentCalendar;
+  NSDateComponents *components = [calendar components:(NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond) fromDate:date];
+  UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:repeats];
+  
+  UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
+  [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
+    if (error) {
+      NSLog(@"%@", error);
+    }
+  }];
 }
 
 
 - (void)clearAllDeliveredNotifications {
-    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
-    [center removeAllDeliveredNotifications];
+  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+  [center removeAllDeliveredNotifications];
 }
 
 - (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(nullable UNNotification *)notification{

+ 8 - 0
ios/hola.xcodeproj/project.pbxproj

@@ -14,6 +14,8 @@
 		35319C272B2773AB00471ACA /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 35319C262B2773AB00471ACA /* libz.tbd */; };
 		35319C292B2773B600471ACA /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 35319C282B2773B600471ACA /* libresolv.tbd */; };
 		35319C2B2B2773BE00471ACA /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35319C2A2B2773BE00471ACA /* UserNotifications.framework */; };
+		35604B222C513DB100BF0379 /* 1.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 35604B212C513DB000BF0379 /* 1.m4a */; };
+		35604B242C513DC100BF0379 /* 2.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 35604B232C513DC100BF0379 /* 2.m4a */; };
 		35B0D6E02B4D4EE10059F156 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 35B0D6DF2B4D4EE10059F156 /* AppDelegate.mm */; };
 		35B0D6EB2B5B872F0059F156 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 35B0D6E92B5B872F0059F156 /* assets */; };
 		35B0D6ED2B5B8D360059F156 /* main.jsbundle in Resources */ = {isa = PBXBuildFile; fileRef = 35B0D6EC2B5B8D360059F156 /* main.jsbundle */; };
@@ -48,6 +50,8 @@
 		35319C262B2773AB00471ACA /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
 		35319C282B2773B600471ACA /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
 		35319C2A2B2773BE00471ACA /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
+		35604B212C513DB000BF0379 /* 1.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 1.m4a; sourceTree = "<group>"; };
+		35604B232C513DC100BF0379 /* 2.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 2.m4a; sourceTree = "<group>"; };
 		35B0D6DE2B4D4EE10059F156 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
 		35B0D6DF2B4D4EE10059F156 /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = "<group>"; };
 		35B0D6E12B4D58540059F156 /* hola.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = hola.entitlements; path = hola/hola.entitlements; sourceTree = "<group>"; };
@@ -109,6 +113,8 @@
 		13B07FAE1A68108700A75B9A /* hola */ = {
 			isa = PBXGroup;
 			children = (
+				35604B212C513DB000BF0379 /* 1.m4a */,
+				35604B232C513DC100BF0379 /* 2.m4a */,
 				35B0D6E12B4D58540059F156 /* hola.entitlements */,
 				13B07FB51A68108700A75B9A /* Images.xcassets */,
 				13B07FB61A68108700A75B9A /* Info.plist */,
@@ -303,6 +309,8 @@
 			files = (
 				81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
 				35B0D6EB2B5B872F0059F156 /* assets in Resources */,
+				35604B222C513DB100BF0379 /* 1.m4a in Resources */,
+				35604B242C513DC100BF0379 /* 2.m4a in Resources */,
 				13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
 				35B0D6ED2B5B8D360059F156 /* main.jsbundle in Resources */,
 			);

+ 1 - 1
src/features/trackSomething/components/Metric.tsx

@@ -280,7 +280,7 @@ export default function Component(props: any) {
                             themeColor={item.theme_color}
                             onClickDetail={() => { goDetail(item) }}
                             onClick={() => { record(item) }}
-                            showTag={item.type == 'composite'}
+                            showTag={item.classification == 'derived'}
                             tagName={t('feature.track_something.metric.composite')}
                         />
                     })

+ 4 - 4
src/features/trackSomething/components/MetricHistory.tsx

@@ -186,14 +186,14 @@ export default function Component(props: { records: any[] }) {
                                                 <Text className="unit" style={{ marginBottom: 3, marginLeft: 3 }}>in</Text>
                                             </View> :
                                             <View style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-end' }}>
-                                                <Text className="value" style={{ backgroundColor: global.isDebug ? 'red' : 'transparent' }}>{record.type == 'composite' ? record.value : record.items[0].value}</Text>
+                                                <Text className="value" style={{ backgroundColor: global.isDebug ? 'red' : 'transparent' }}>{record.classification == 'derived' ? record.value : record.items[0].value}</Text>
                                                 {
-                                                    record.type == 'basic' && record.items.length > 1 && <Text className="value">/{record.items[1].value}</Text>
+                                                    record.classification != 'derived' && record.items.length > 1 && <Text className="value">/{record.items[1].value}</Text>
                                                 }
                                                 {
-                                                    record.type == 'basic' && record.items.length > 2 && <Text className="value">/{record.items[2].value}</Text>
+                                                    record.classification != 'derived' && record.items.length > 2 && <Text className="value">/{record.items[2].value}</Text>
                                                 }
-                                                <Text className="unit" style={{ marginBottom: 3, marginLeft: 3 }}>{record.type == 'composite' ? record.unit ? record.unit : '' : record.items[0].unit}</Text>
+                                                <Text className="unit" style={{ marginBottom: 3, marginLeft: 3 }}>{record.classification == 'derived' ? record.unit ? record.unit : '' : record.items[0].unit}</Text>
                                             </View>
                                     }