Browse Source

增加部分克隆页面

zk 1 year ago
parent
commit
52c7c79057
21 changed files with 479 additions and 24 deletions
  1. 0 3
      app/src/main/java/com/atmob/voiceai/data/api/AtmobApi.java
  2. 6 0
      app/src/main/java/com/atmob/voiceai/data/api/GenerateApi.java
  3. 1 1
      app/src/main/java/com/atmob/voiceai/data/api/request/BaseRequest.java
  4. 62 0
      app/src/main/java/com/atmob/voiceai/data/repositories/CloneRepository.java
  5. 3 0
      app/src/main/java/com/atmob/voiceai/data/repositories/HistoryRepository.java
  6. 3 1
      app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java
  7. 7 5
      app/src/main/java/com/atmob/voiceai/helper/ErrorHelper.java
  8. 46 0
      app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceFragment.java
  9. 135 0
      app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceViewModel.java
  10. 2 0
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIFragment.java
  11. 0 1
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java
  12. BIN
      app/src/main/res/drawable-xxhdpi/bg_clone_voice_head_background.webp
  13. BIN
      app/src/main/res/drawable-xxhdpi/icon_clone_setting.webp
  14. BIN
      app/src/main/res/drawable-xxhdpi/icon_clone_voice_logo.webp
  15. 2 2
      app/src/main/res/layout/activity_voice_generating.xml
  16. 3 3
      app/src/main/res/layout/activity_voice_result.xml
  17. 200 4
      app/src/main/res/layout/fragment_clone_voice.xml
  18. 2 2
      app/src/main/res/layout/fragment_history.xml
  19. 2 2
      app/src/main/res/layout/fragment_voice_ai.xml
  20. 1 0
      app/src/main/res/values/dimens.xml
  21. 4 0
      app/src/main/res/values/strings.xml

+ 0 - 3
app/src/main/java/com/atmob/voiceai/data/api/AtmobApi.java

@@ -36,9 +36,6 @@ public interface AtmobApi {
     @POST("/project/voice/v1/clone/list")
     Single<BaseResponse<VoiceCloneListResponse>> getCloneList(@Body BaseRequest request);
 
-    @POST("/project/voice/v1/voice/clone")
-    Single<BaseResponse<VoiceCloneResponse>> voiceClone(@Body VoiceCloneRequest request);
-
     @POST("/project/voice/v1/clone/delete")
     Single<BaseResponse<Object>> cloneDelete(@Body CloneDeleteRequest request);
 

+ 6 - 0
app/src/main/java/com/atmob/voiceai/data/api/GenerateApi.java

@@ -2,7 +2,9 @@ package com.atmob.voiceai.data.api;
 
 import com.atmob.app.lib.base.BaseResponse;
 import com.atmob.voiceai.data.api.request.TextTosSpeechRequest;
+import com.atmob.voiceai.data.api.request.VoiceCloneRequest;
 import com.atmob.voiceai.data.api.response.TextToSpeechResponse;
+import com.atmob.voiceai.data.api.response.VoiceCloneResponse;
 
 import atmob.reactivex.rxjava3.core.Single;
 import atmob.retrofit2.http.Body;
@@ -13,4 +15,8 @@ public interface GenerateApi {
 
     @POST("/project/voice/v1/voice/textToSpeech")
     Single<BaseResponse<TextToSpeechResponse>> textToSpeech(@Body TextTosSpeechRequest request);
+
+
+    @POST("/project/voice/v1/voice/clone")
+    Single<BaseResponse<VoiceCloneResponse>> voiceClone(@Body VoiceCloneRequest request);
 }

+ 1 - 1
app/src/main/java/com/atmob/voiceai/data/api/request/BaseRequest.java

@@ -13,7 +13,7 @@ public class BaseRequest extends AtmobParams {
 //            try {
 //                Field androidId = AtmobParams.class.getDeclaredField("androidId");
 //                androidId.setAccessible(true);
-//                androidId.set(this, "6713123wdq22e1232");
+//                androidId.set(this, "16713123wdq22e1232");
 //            } catch (NoSuchFieldException e) {
 //
 //            } catch (IllegalAccessException e) {

+ 62 - 0
app/src/main/java/com/atmob/voiceai/data/repositories/CloneRepository.java

@@ -0,0 +1,62 @@
+package com.atmob.voiceai.data.repositories;
+
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.atmob.app.lib.handler.RxHttpHandler;
+import com.atmob.app.lib.livedata.SingleLiveEvent;
+import com.atmob.voiceai.data.api.AtmobApi;
+import com.atmob.voiceai.data.api.GenerateApi;
+import com.atmob.voiceai.data.api.request.BaseRequest;
+import com.atmob.voiceai.data.api.request.CloneDeleteRequest;
+import com.atmob.voiceai.data.api.request.VoiceCloneRequest;
+import com.atmob.voiceai.data.api.response.VoiceCloneListResponse;
+import com.atmob.voiceai.data.api.response.VoiceCloneResponse;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import atmob.reactivex.rxjava3.core.Single;
+import atmob.rxjava.utils.RxJavaUtil;
+
+@Singleton
+public class CloneRepository {
+
+
+    private final AtmobApi atmobApi;
+    private final VoiceAIRepository voiceAIRepository;
+    private final GenerateApi generateApi;
+
+    @Inject
+    public CloneRepository(AtmobApi atmobApi, GenerateApi generateApi, VoiceAIRepository voiceAIRepository) {
+        this.atmobApi = atmobApi;
+        this.generateApi = generateApi;
+        this.voiceAIRepository = voiceAIRepository;
+    }
+
+    public Single<VoiceCloneListResponse> requestCloneList() {
+        return atmobApi.getCloneList(new BaseRequest())
+                .compose(RxHttpHandler.handle(true))
+                .retryWhen(RxJavaUtil.exponentialBackOff(null, 3, TimeUnit.SECONDS))
+                .compose(RxJavaUtil.SingleSchedule.io2Main());
+    }
+
+    public Single<Object> requestCloneDelete(int id) {
+        return atmobApi.cloneDelete(new CloneDeleteRequest(id))
+                .compose(RxHttpHandler.handle(true))
+                .compose(RxJavaUtil.SingleSchedule.io2Main())
+                .doOnSuccess(response -> voiceAIRepository.setCloneExecuteSuccessEvent());
+
+    }
+
+    public Single<VoiceCloneResponse> requestVoiceClone(int type, File file) {
+        return generateApi.voiceClone(new VoiceCloneRequest(type, file))
+                .compose(RxHttpHandler.handle(false))
+                .compose(RxJavaUtil.SingleSchedule.io2Main())
+                .doOnSuccess(response -> voiceAIRepository.setCloneExecuteSuccessEvent());
+    }
+}

+ 3 - 0
app/src/main/java/com/atmob/voiceai/data/repositories/HistoryRepository.java

@@ -8,6 +8,8 @@ import com.atmob.voiceai.data.api.AtmobApi;
 import com.atmob.voiceai.data.api.bean.UserVoiceBean;
 import com.atmob.voiceai.data.api.request.VoiceHistoryRequest;
 import com.atmob.voiceai.data.api.response.VoiceHistoryResponse;
+import com.atmob.voiceai.helper.ErrorHelper;
+import com.atmob.voiceai.utils.ToastUtil;
 
 import java.util.List;
 
@@ -87,6 +89,7 @@ public class HistoryRepository {
             public void onError(@NonNull Throwable e) {
                 refreshFlag = false;
                 refreshing.setValue(false);
+                ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
             }
         });
     }

+ 3 - 1
app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java

@@ -8,6 +8,7 @@ import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
 import com.atmob.app.lib.handler.RxHttpHandler;
+import com.atmob.app.lib.livedata.SingleLiveEvent;
 import com.atmob.common.runtime.ContextUtil;
 import com.atmob.voiceai.R;
 import com.atmob.voiceai.data.api.AtmobApi;
@@ -54,7 +55,7 @@ public class VoiceAIRepository {
 
     private final MutableLiveData<ThreePair<Integer, String, UserVoiceBean>> textToSpeechState = new MutableLiveData<>();
 
-    private final MutableLiveData<VoiceListBean> recommendClickBean = new MutableLiveData<>();
+    private final SingleLiveEvent<VoiceListBean> recommendClickBean = new SingleLiveEvent<>();
 
     private boolean isRequestFreeGenerate;
 
@@ -75,6 +76,7 @@ public class VoiceAIRepository {
         refreshVoiceInfo();
     }
 
+
     public LiveData<Integer> getAdFreeGenerateNumber() {
         return adFreeGenerateNumber;
     }

+ 7 - 5
app/src/main/java/com/atmob/voiceai/helper/ErrorHelper.java

@@ -11,19 +11,21 @@ public class ErrorHelper {
     public static void errorThrowableToast(Throwable throwable, @ToastUtil.Duration int duration) {
         if (throwable instanceof RxHttpHandler.ServerErrorException) {
             RxHttpHandler.ServerErrorException serverErrorException = (RxHttpHandler.ServerErrorException) throwable;
-            errorToast(serverErrorException.getCode(), duration);
+            int errorResId = getErrorResId(serverErrorException.getCode());
+            if (errorResId == -1) {
+                ToastUtil.show(serverErrorException.getMessage(), duration);
+            } else {
+                ToastUtil.show(errorResId, duration);
+            }
         } else {
             ToastUtil.show(R.string.net_error_message, duration);
         }
     }
 
-    public static void errorToast(int errorCode, @ToastUtil.Duration int duration) {
-        ToastUtil.show(getErrorResId(errorCode), duration);
-    }
 
     @StringRes
     public static int getErrorResId(int errorCode) {
-        int errorResId = R.string.net_error_message;
+        int errorResId = -1;
         switch (errorCode) {
 
         }

+ 46 - 0
app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceFragment.java

@@ -1,8 +1,14 @@
 package com.atmob.voiceai.module.clonevoice;
 
+import android.os.Bundle;
+import android.view.View;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.widget.NestedScrollView;
 
 import com.atmob.app.lib.base.BaseFragment;
+import com.atmob.common.ui.SizeUtil;
 import com.atmob.voiceai.databinding.FragmentCloneVoiceBinding;
 import com.gyf.immersionbar.ImmersionBar;
 
@@ -12,9 +18,49 @@ import dagger.hilt.android.AndroidEntryPoint;
 public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding> {
 
 
+    private CloneVoiceViewModel cloneVoiceViewModel;
+
+    private int maxScrollY;
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        initView();
+        initObserver();
+    }
+
+    private void initView() {
+        initScrollView();
+    }
+
+    private void initScrollView() {
+        maxScrollY = (int) SizeUtil.dp2px(89);
+        binding.scrollView.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) (v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+            if (scrollY > maxScrollY) {
+                binding.vStatusBar.setAlpha(1f);
+                binding.vMenu.setAlpha(1f);
+            } else {
+                binding.vStatusBar.setAlpha(scrollY * 1f / maxScrollY);
+                binding.vMenu.setAlpha(scrollY * 1f / maxScrollY);
+            }
+        });
+    }
+
+    private void initObserver() {
+
+    }
+
     @Override
     protected void configImmersion(@NonNull ImmersionBar immersionBar) {
         super.configImmersion(immersionBar);
         immersionBar.statusBarDarkFont(false);
     }
+
+
+    @Override
+    protected void initViewModel() {
+        super.initViewModel();
+        cloneVoiceViewModel = getViewModelProvider().get(CloneVoiceViewModel.class);
+        binding.setCloneVoiceViewModel(cloneVoiceViewModel);
+    }
 }

+ 135 - 0
app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceViewModel.java

@@ -0,0 +1,135 @@
+package com.atmob.voiceai.module.clonevoice;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.common.runtime.ActivityUtil;
+import com.atmob.common.runtime.ContextUtil;
+import com.atmob.voiceai.R;
+import com.atmob.voiceai.data.api.response.VoiceCloneListResponse;
+import com.atmob.voiceai.data.api.response.VoiceCloneResponse;
+import com.atmob.voiceai.data.repositories.CloneRepository;
+import com.atmob.voiceai.data.repositories.MemberRepository;
+import com.atmob.voiceai.helper.ErrorHelper;
+import com.atmob.voiceai.module.setting.SettingActivity;
+import com.atmob.voiceai.module.subscription.SubscriptionPageActivity;
+import com.atmob.voiceai.utils.SpannableUtil;
+import com.atmob.voiceai.utils.ToastUtil;
+
+import javax.inject.Inject;
+
+import atmob.reactivex.rxjava3.annotations.NonNull;
+import atmob.reactivex.rxjava3.core.SingleObserver;
+import atmob.reactivex.rxjava3.disposables.Disposable;
+import dagger.hilt.android.lifecycle.HiltViewModel;
+
+
+@HiltViewModel
+public class CloneVoiceViewModel extends BaseViewModel {
+
+
+    private final MutableLiveData<CharSequence> cloneTip = new MutableLiveData<>();
+
+
+    private final MemberRepository memberRepository;
+    private final CloneRepository cloneRepository;
+
+    private boolean requestCloneFlag = false;
+
+    @Inject
+    public CloneVoiceViewModel(MemberRepository memberRepository, CloneRepository cloneRepository) {
+        this.memberRepository = memberRepository;
+        this.cloneRepository = cloneRepository;
+        initCloneTipTxt();
+        refreshCloneList();
+    }
+
+
+    public LiveData<CharSequence> getCloneTip() {
+        return cloneTip;
+    }
+
+    public LiveData<Boolean> getIsMember() {
+        return memberRepository.getIsMember();
+    }
+
+
+    private void initCloneTipTxt() {
+        String allTxt = ContextUtil.getContext().getString(R.string.clone_voice_tip);
+        String targetTxt = ContextUtil.getContext().getString(R.string.clone_voice_tip_create);
+        cloneTip.setValue(SpannableUtil.getSpannableStringBuilder(allTxt, targetTxt, ContextUtil.getContext().getResources().getColor(R.color.colorPrimaryVariant), false));
+    }
+
+    public void refreshCloneList() {
+        if (requestCloneFlag) {
+            return;
+        }
+        cloneRepository.requestCloneList().subscribe(new SingleObserver<VoiceCloneListResponse>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                addDisposable(d);
+                requestCloneFlag = true;
+            }
+
+            @Override
+            public void onSuccess(@NonNull VoiceCloneListResponse voiceCloneListResponse) {
+                requestCloneFlag = false;
+
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                requestCloneFlag = false;
+                ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
+            }
+        });
+    }
+
+    public void requestCloneVoice() {
+
+//        cloneRepository.requestVoiceClone().subscribe(new SingleObserver<VoiceCloneResponse>() {
+//            @Override
+//            public void onSubscribe(@NonNull Disposable d) {
+//                addDisposable(d);
+//            }
+//
+//            @Override
+//            public void onSuccess(@NonNull VoiceCloneResponse voiceCloneResponse) {
+//                  refreshCloneList();
+//            }
+//
+//            @Override
+//            public void onError(@NonNull Throwable e) {
+//                  ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
+//            }
+//        });
+    }
+
+    public void deleteClone(int id) {
+        cloneRepository.requestCloneDelete(id).subscribe(new SingleObserver<Object>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                addDisposable(d);
+            }
+
+            @Override
+            public void onSuccess(@NonNull Object o) {
+                refreshCloneList();
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
+            }
+        });
+    }
+
+    public void onGoSubscriptionClick() {
+        SubscriptionPageActivity.start(ActivityUtil.getTopActivity());
+    }
+
+    public void onSettingClick() {
+        SettingActivity.start(ActivityUtil.getTopActivity());
+    }
+}

+ 2 - 0
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIFragment.java

@@ -72,6 +72,8 @@ public class VoiceAIFragment extends BaseFragment<FragmentVoiceAiBinding> {
         voiceViewModel.getVoiceDetailList().observe(getViewLifecycleOwner(), list -> voiceAIListAdapter.submit(list.first, () -> {
                     if (list.second != null) {
                         voiceViewModel.scrollToListPosition(list.second);
+                    } else {
+                        binding.ryVoiceView.scrollToPosition(0);
                     }
                 })
         );

+ 0 - 1
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java

@@ -192,7 +192,6 @@ public class VoiceAIViewModel extends BaseViewModel {
             if (bean == null) {
                 return;
             }
-            voiceAIRepository.setRecommendClickBean(null);
             voicePrintTxt.setValue(bean.getContent());
             List<TypeListBean> typeList = voiceTypeList.getValue();
             if (typeList == null || typeList.isEmpty()) {

BIN
app/src/main/res/drawable-xxhdpi/bg_clone_voice_head_background.webp


BIN
app/src/main/res/drawable-xxhdpi/icon_clone_setting.webp


BIN
app/src/main/res/drawable-xxhdpi/icon_clone_voice_logo.webp


+ 2 - 2
app/src/main/res/layout/activity_voice_generating.xml

@@ -34,7 +34,7 @@
             app:layout_constraintEnd_toEndOf="parent" />
 
         <Space
-            android:id="@+id/space_status_bar"
+            android:id="@+id/v_status_bar"
             android:layout_width="match_parent"
             android:layout_height="@{SizeUtil.getStatusBarHeight(), default=@dimen/app_status_bar_height}"
             app:layout_constraintTop_toTopOf="parent" />
@@ -44,7 +44,7 @@
             android:layout_width="match_parent"
             android:layout_height="0dp"
             app:layout_constraintDimensionRatio="360:74"
-            app:layout_constraintTop_toBottomOf="@+id/space_status_bar" />
+            app:layout_constraintTop_toBottomOf="@+id/v_status_bar" />
 
         <ImageView
             android:id="@+id/iv_avatar"

+ 3 - 3
app/src/main/res/layout/activity_voice_result.xml

@@ -26,7 +26,7 @@
             app:layout_constraintTop_toTopOf="parent" />
 
         <Space
-            android:id="@+id/space_status_bar"
+            android:id="@+id/v_status_bar"
             android:layout_width="match_parent"
             android:layout_height="@{SizeUtil.getStatusBarHeight(), default=@dimen/app_status_bar_height}"
             app:layout_constraintTop_toTopOf="parent" />
@@ -42,7 +42,7 @@
             android:layout_width="match_parent"
             android:layout_height="0dp"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/space_status_bar">
+            app:layout_constraintTop_toBottomOf="@+id/v_status_bar">
 
             <androidx.constraintlayout.widget.ConstraintLayout
                 android:layout_width="match_parent"
@@ -248,7 +248,7 @@
             android:layout_width="match_parent"
             android:layout_height="0dp"
             app:layout_constraintDimensionRatio="360:10"
-            app:layout_constraintTop_toBottomOf="@+id/space_status_bar" />
+            app:layout_constraintTop_toBottomOf="@+id/v_status_bar" />
 
         <ImageView
             android:id="@+id/iv_back"

+ 200 - 4
app/src/main/res/layout/fragment_clone_voice.xml

@@ -1,6 +1,202 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+    <data>
+
+        <variable
+            name="cloneVoiceViewModel"
+            type="com.atmob.voiceai.module.clonevoice.CloneVoiceViewModel" />
+
+
+        <import type="com.atmob.common.ui.SizeUtil" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+
+        <androidx.core.widget.NestedScrollView
+            android:id="@+id/scroll_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <Space
+                    android:id="@+id/space_status"
+                    android:layout_width="match_parent"
+                    android:layout_height="@{SizeUtil.getStatusBarHeight(), default=@dimen/app_status_bar_height}"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+                <Space
+                    android:id="@+id/space1"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="360:50"
+                    app:layout_constraintTop_toBottomOf="@id/space_status" />
+
+                <ImageView
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:src="@drawable/bg_clone_voice_head_background"
+                    app:layout_constraintDimensionRatio="1080:620"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+                <ImageView
+                    android:id="@+id/iv_logo"
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    android:src="@drawable/icon_clone_voice_logo"
+                    app:layout_constraintDimensionRatio="216:203"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@+id/space1"
+                    app:layout_constraintWidth_percent="0.1833333333333333" />
+
+                <Space
+                    android:id="@+id/space2"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="360:50"
+                    app:layout_constraintTop_toBottomOf="@+id/iv_logo" />
+
+                <TextView
+                    android:id="@+id/tv_clone_tip"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="@dimen/clone_voice_padding"
+                    android:text="@{cloneVoiceViewModel.cloneTip}"
+                    android:textColor="@color/white"
+                    android:textSize="26sp"
+                    android:textStyle="bold"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@+id/space2"
+                    tools:text="CREATE YOUR OWN \nAI VOICE" />
+
+                <TextView
+                    android:id="@+id/tv_clone_hint"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginHorizontal="@dimen/clone_voice_padding"
+                    android:layout_marginTop="12dp"
+                    android:text="@string/clone_voice_hint"
+                    android:textColor="@color/white80"
+                    android:textSize="14sp"
+                    app:layout_constraintTop_toBottomOf="@+id/tv_clone_tip" />
+
+
+                <Space
+                    android:id="@+id/space3"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="360:56"
+                    app:layout_constraintTop_toBottomOf="@+id/tv_clone_hint" />
+
+                <TextView
+                    android:id="@+id/tv_clone_hint2"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginHorizontal="@dimen/clone_voice_padding"
+                    android:text="@string/clone_voice_hint2"
+                    android:textColor="@color/white80"
+                    android:textSize="14sp"
+                    app:layout_constraintTop_toBottomOf="@+id/space3" />
+
+                <Space
+                    android:id="@+id/space4"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="360:12"
+                    app:layout_constraintTop_toBottomOf="@+id/tv_clone_hint2" />
+
+
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+        </androidx.core.widget.NestedScrollView>
+
+        <View
+            android:id="@+id/v_status_bar"
+            android:layout_width="match_parent"
+            android:layout_height="@{SizeUtil.getStatusBarHeight(), default=@dimen/app_status_bar_height}"
+            android:alpha="0"
+            android:background="@color/colorPrimary"
+            app:layout_constraintTop_toTopOf="parent" />
+
+
+        <View
+            android:id="@+id/v_menu"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:alpha="0"
+            android:background="@color/colorPrimary"
+            app:layout_constraintDimensionRatio="360:65"
+            app:layout_constraintTop_toBottomOf="@+id/v_status_bar" />
+
+
+        <View
+            android:id="@+id/v_vip_bg"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
+            android:background="@drawable/bg_voice_ai_vip"
+            android:onClick="@{()-> cloneVoiceViewModel.onGoSubscriptionClick()}"
+            app:layout_constraintBottom_toBottomOf="@+id/v_menu"
+            app:layout_constraintDimensionRatio="58:28"
+            app:layout_constraintStart_toStartOf="@+id/v_menu"
+            app:layout_constraintTop_toTopOf="@+id/v_menu"
+            app:layout_constraintWidth_percent="0.1611111111111111" />
+
+
+        <ImageView
+            android:id="@+id/iv_vip"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:src="@drawable/icon_vip"
+            app:layout_constraintBottom_toBottomOf="@+id/v_vip_bg"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintLeft_toLeftOf="@+id/v_vip_bg"
+            app:layout_constraintRight_toLeftOf="@+id/tv_vip_title"
+            app:layout_constraintTop_toTopOf="@+id/v_vip_bg"
+            app:layout_constraintWidth_percent="0.0444444444444444" />
+
+        <TextView
+            android:id="@+id/tv_vip_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="4dp"
+            android:text="@string/voice_pro"
+            android:textColor="@color/colorPrimaryVariant"
+            android:textSize="14dp"
+            app:layout_constraintBottom_toBottomOf="@+id/v_vip_bg"
+            app:layout_constraintLeft_toRightOf="@+id/iv_vip"
+            app:layout_constraintRight_toRightOf="@+id/v_vip_bg"
+            app:layout_constraintTop_toTopOf="@+id/v_vip_bg" />
+
+        <ImageView
+            android:id="@+id/iv_setting"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginEnd="16dp"
+            android:onClick="@{()->cloneVoiceViewModel.onSettingClick()}"
+            android:src="@drawable/icon_clone_setting"
+            app:layout_constraintBottom_toBottomOf="@+id/v_menu"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintEnd_toEndOf="@+id/v_menu"
+            app:layout_constraintTop_toTopOf="@+id/v_menu"
+            app:layout_constraintWidth_percent="0.0888888888888889" />
+
+        <androidx.constraintlayout.widget.Group
+            isGone="@{cloneVoiceViewModel.isMember}"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:constraint_referenced_ids="v_vip_bg,iv_vip,tv_vip_title" />
+
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 2 - 2
app/src/main/res/layout/fragment_history.xml

@@ -28,7 +28,7 @@
             app:layout_constraintWidth_percent="0.5083333333333333" />
 
         <Space
-            android:id="@+id/space_status_bar"
+            android:id="@+id/v_status_bar"
             android:layout_width="match_parent"
             android:layout_height="@{SizeUtil.getStatusBarHeight(), default=@dimen/app_status_bar_height}"
             app:layout_constraintTop_toTopOf="parent" />
@@ -38,7 +38,7 @@
             android:layout_width="match_parent"
             android:layout_height="0dp"
             app:layout_constraintDimensionRatio="360:44"
-            app:layout_constraintTop_toBottomOf="@+id/space_status_bar" />
+            app:layout_constraintTop_toBottomOf="@+id/v_status_bar" />
 
         <View
             android:id="@+id/v_vip_bg"

+ 2 - 2
app/src/main/res/layout/fragment_voice_ai.xml

@@ -34,7 +34,7 @@
 
 
             <Space
-                android:id="@+id/space_status_bar"
+                android:id="@+id/v_status_bar"
                 android:layout_width="match_parent"
                 android:layout_height="@{SizeUtil.getStatusBarHeight(), default=@dimen/app_status_bar_height}"
                 app:layout_constraintTop_toTopOf="parent" />
@@ -44,7 +44,7 @@
                 android:layout_width="match_parent"
                 android:layout_height="0dp"
                 app:layout_constraintDimensionRatio="360:44"
-                app:layout_constraintTop_toBottomOf="@+id/space_status_bar" />
+                app:layout_constraintTop_toBottomOf="@+id/v_status_bar" />
 
             <View
                 android:onClick="@{()-> voiceAIViewModel.onGoSubscriptionClick()}"

+ 1 - 0
app/src/main/res/values/dimens.xml

@@ -2,4 +2,5 @@
 <resources>
     <dimen name="app_common_page_horizontal_padding">12dp</dimen>
     <dimen name="app_status_bar_height">24dp</dimen>
+    <dimen name="clone_voice_padding">24dp</dimen>
 </resources>

+ 4 - 0
app/src/main/res/values/strings.xml

@@ -37,4 +37,8 @@
     <string name="google_play_not_found">Google Play not found</string>
     <string name="version_name">version%s</string>
     <string name="history_no_data">There is nothing here</string>
+    <string name="clone_voice_tip">CREATE YOUR OWN \nAI VOICE</string>
+    <string name="clone_voice_tip_create">CREATE</string>
+    <string name="clone_voice_hint">Record or upload an audio file to create AI model with your own voice</string>
+    <string name="clone_voice_hint2">#Clearing speech without background noise produces the best results.</string>
 </resources>