瀏覽代碼

增加历史记录

zk 2 年之前
父節點
當前提交
8e5e4c0fba
共有 28 個文件被更改,包括 873 次插入153 次删除
  1. 0 86
      app/src/main/java/com/atmob/voiceai/data/api/bean/CloneHistoryBean.java
  2. 50 1
      app/src/main/java/com/atmob/voiceai/data/api/bean/UserVoiceBean.java
  3. 11 11
      app/src/main/java/com/atmob/voiceai/data/api/request/BaseRequest.java
  4. 4 4
      app/src/main/java/com/atmob/voiceai/data/api/response/VoiceHistoryResponse.java
  5. 4 8
      app/src/main/java/com/atmob/voiceai/data/repositories/AccountRepository.java
  6. 100 0
      app/src/main/java/com/atmob/voiceai/data/repositories/HistoryRepository.java
  7. 28 0
      app/src/main/java/com/atmob/voiceai/data/repositories/MemberRepository.java
  8. 19 20
      app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java
  9. 2 2
      app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingActivity.java
  10. 6 4
      app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingViewModel.java
  11. 61 1
      app/src/main/java/com/atmob/voiceai/module/history/HistoryFragment.java
  12. 99 0
      app/src/main/java/com/atmob/voiceai/module/history/HistoryItemAdapter.java
  13. 68 0
      app/src/main/java/com/atmob/voiceai/module/history/HistoryViewModel.java
  14. 2 1
      app/src/main/java/com/atmob/voiceai/module/main/MainViewModel.java
  15. 11 1
      app/src/main/java/com/atmob/voiceai/module/result/VoiceResultActivity.java
  16. 8 1
      app/src/main/java/com/atmob/voiceai/module/result/VoiceResultViewModel.java
  17. 1 0
      app/src/main/java/com/atmob/voiceai/module/setting/SettingActivity.java
  18. 6 8
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java
  19. 8 0
      app/src/main/java/com/atmob/voiceai/utils/DateUtil.java
  20. 70 0
      app/src/main/java/com/atmob/voiceai/utils/LinearEndLessOnScrollListener.java
  21. 41 0
      app/src/main/java/com/atmob/voiceai/utils/ThreePair.java
  22. 二進制
      app/src/main/res/drawable-xxhdpi/icon_history_create_time.webp
  23. 二進制
      app/src/main/res/drawable-xxhdpi/icon_history_no_data.webp
  24. 5 0
      app/src/main/res/drawable/bg_history_item.xml
  25. 13 1
      app/src/main/res/layout/activity_voice_result.xml
  26. 155 4
      app/src/main/res/layout/fragment_history.xml
  27. 100 0
      app/src/main/res/layout/item_history_clone_voice.xml
  28. 1 0
      app/src/main/res/values/strings.xml

+ 0 - 86
app/src/main/java/com/atmob/voiceai/data/api/bean/CloneHistoryBean.java

@@ -1,86 +0,0 @@
-package com.atmob.voiceai.data.api.bean;
-
-import com.google.gson.annotations.SerializedName;
-
-public class CloneHistoryBean {
-    @SerializedName("id")
-    private int id;
-    @SerializedName("ssid")
-    private String ssid;
-    @SerializedName("content")
-    private String content;
-    @SerializedName("voiceId")
-    private int voiceId;
-    @SerializedName("voiceName")
-    private String voiceName;
-    @SerializedName("voiceAvatar")
-    private String voiceAvatar;
-    @SerializedName("voiceUrl")
-    private String voiceUrl;
-    @SerializedName("createTime")
-    private long createTime;
-
-    public int getId() {
-        return id;
-    }
-
-    public void setId(int id) {
-        this.id = id;
-    }
-
-    public String getSsid() {
-        return ssid;
-    }
-
-    public void setSsid(String ssid) {
-        this.ssid = ssid;
-    }
-
-    public String getContent() {
-        return content;
-    }
-
-    public void setContent(String content) {
-        this.content = content;
-    }
-
-    public int getVoiceId() {
-        return voiceId;
-    }
-
-    public void setVoiceId(int voiceId) {
-        this.voiceId = voiceId;
-    }
-
-    public String getVoiceName() {
-        return voiceName;
-    }
-
-    public void setVoiceName(String voiceName) {
-        this.voiceName = voiceName;
-    }
-
-    public String getVoiceAvatar() {
-        return voiceAvatar;
-    }
-
-    public void setVoiceAvatar(String voiceAvatar) {
-        this.voiceAvatar = voiceAvatar;
-    }
-
-    public String getVoiceUrl() {
-        return voiceUrl;
-    }
-
-    public void setVoiceUrl(String voiceUrl) {
-        this.voiceUrl = voiceUrl;
-    }
-
-    public long getCreateTime() {
-        return createTime;
-    }
-
-    public void setCreateTime(long createTime) {
-        this.createTime = createTime;
-    }
-}

+ 50 - 1
app/src/main/java/com/atmob/voiceai/data/api/bean/UserVoiceBean.java

@@ -1,8 +1,13 @@
 package com.atmob.voiceai.data.api.bean;
 
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
 import com.google.gson.annotations.SerializedName;
 
-public class UserVoiceBean {
+public class UserVoiceBean implements Parcelable {
     @SerializedName("id")
     private int id;
     @SerializedName("ssid")
@@ -20,6 +25,33 @@ public class UserVoiceBean {
     @SerializedName("createTime")
     private long createTime;
 
+    public UserVoiceBean() {
+
+    }
+
+    protected UserVoiceBean(Parcel in) {
+        id = in.readInt();
+        ssid = in.readString();
+        content = in.readString();
+        voiceId = in.readInt();
+        voiceName = in.readString();
+        voiceAvatar = in.readString();
+        voiceUrl = in.readString();
+        createTime = in.readLong();
+    }
+
+    public static final Creator<UserVoiceBean> CREATOR = new Creator<UserVoiceBean>() {
+        @Override
+        public UserVoiceBean createFromParcel(Parcel in) {
+            return new UserVoiceBean(in);
+        }
+
+        @Override
+        public UserVoiceBean[] newArray(int size) {
+            return new UserVoiceBean[size];
+        }
+    };
+
     public int getId() {
         return id;
     }
@@ -83,4 +115,21 @@ public class UserVoiceBean {
     public void setCreateTime(long createTime) {
         this.createTime = createTime;
     }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int i) {
+        parcel.writeInt(id);
+        parcel.writeString(ssid);
+        parcel.writeString(content);
+        parcel.writeInt(voiceId);
+        parcel.writeString(voiceName);
+        parcel.writeString(voiceAvatar);
+        parcel.writeString(voiceUrl);
+        parcel.writeLong(createTime);
+    }
 }

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

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

+ 4 - 4
app/src/main/java/com/atmob/voiceai/data/api/response/VoiceHistoryResponse.java

@@ -1,6 +1,6 @@
 package com.atmob.voiceai.data.api.response;
 
-import com.atmob.voiceai.data.api.bean.CloneHistoryBean;
+import com.atmob.voiceai.data.api.bean.UserVoiceBean;
 import com.google.gson.annotations.SerializedName;
 
 import java.util.List;
@@ -11,7 +11,7 @@ public class VoiceHistoryResponse {
     @SerializedName("count")
     private int count;
     @SerializedName("list")
-    private List<CloneHistoryBean> list;
+    private List<UserVoiceBean> list;
 
     public int getCount() {
         return count;
@@ -21,11 +21,11 @@ public class VoiceHistoryResponse {
         this.count = count;
     }
 
-    public List<CloneHistoryBean> getList() {
+    public List<UserVoiceBean> getList() {
         return list;
     }
 
-    public void setList(List<CloneHistoryBean> list) {
+    public void setList(List<UserVoiceBean> list) {
         this.list = list;
     }
 

+ 4 - 8
app/src/main/java/com/atmob/voiceai/data/repositories/AccountRepository.java

@@ -25,20 +25,16 @@ import atmob.rxjava.utils.RxJavaUtil;
 public class AccountRepository {
 
     private final AtmobApi atmobApi;
+    private final MemberRepository memberRepository;
     private boolean requestUserInfoDisabled;
 
-    private final MutableLiveData<Boolean> isMember = new MutableLiveData<>();
-
-
     @Inject
-    public AccountRepository(AtmobApi atmobApi) {
+    public AccountRepository(AtmobApi atmobApi, MemberRepository memberRepository) {
         this.atmobApi = atmobApi;
+        this.memberRepository = memberRepository;
         refreshUserData();
     }
 
-    public LiveData<Boolean> getIsMember() {
-        return isMember;
-    }
 
 
     private void refreshUserData() {
@@ -73,7 +69,7 @@ public class AccountRepository {
                 .compose(RxJavaUtil.SingleSchedule.io2Main())
                 .doOnSuccess(userInfoResponse -> {
                     MemberInfoBean memberInfo = userInfoResponse.getMemberInfo();
-                    isMember.setValue(memberInfo != null && memberInfo.isIsMember());
+                    memberRepository.setIsMember(memberInfo != null && memberInfo.isIsMember());
                 });
     }
 }

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

@@ -0,0 +1,100 @@
+package com.atmob.voiceai.data.repositories;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.atmob.app.lib.handler.RxHttpHandler;
+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 java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import atmob.reactivex.rxjava3.annotations.NonNull;
+import atmob.reactivex.rxjava3.core.Single;
+import atmob.reactivex.rxjava3.core.SingleObserver;
+import atmob.reactivex.rxjava3.disposables.Disposable;
+import atmob.rxjava.utils.RxJavaUtil;
+
+@Singleton
+public class HistoryRepository {
+
+
+    private final AtmobApi atmobApi;
+
+    private final MutableLiveData<Boolean> refreshing = new MutableLiveData<>();
+
+    private final int pageSize = 10;
+    private boolean smoothScrollToTop = false;
+
+    private boolean refreshFlag;
+
+    private int page = 1;
+
+    private final MutableLiveData<List<UserVoiceBean>> voiceHistoryList = new MutableLiveData<>();
+
+    @Inject
+    public HistoryRepository(AtmobApi atmobApi) {
+        this.atmobApi = atmobApi;
+        refreshCloneHistory(page);
+    }
+
+    public boolean isSmoothScrollToTop() {
+        return smoothScrollToTop;
+    }
+
+    public LiveData<Boolean> getRefreshing() {
+        return refreshing;
+    }
+
+    public LiveData<List<UserVoiceBean>> getVoiceHistoryList() {
+        return voiceHistoryList;
+    }
+
+    public void refreshCloneHistory(int page) {
+        if (refreshFlag) {
+            return;
+        }
+        voiceHistory(page, pageSize).subscribe(new SingleObserver<VoiceHistoryResponse>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                refreshFlag = true;
+                refreshing.setValue(true);
+            }
+
+            @Override
+            public void onSuccess(@NonNull VoiceHistoryResponse voiceHistoryResponse) {
+                refreshFlag = false;
+                refreshing.setValue(false);
+                if (page == 1) {
+                    smoothScrollToTop = true;
+                    voiceHistoryList.setValue(voiceHistoryResponse.getList());
+                } else {
+                    smoothScrollToTop = false;
+                    List<UserVoiceBean> list = voiceHistoryList.getValue();
+                    if (list != null) {
+                        list.addAll(voiceHistoryResponse.getList());
+                        voiceHistoryList.setValue(list);
+                    }
+                }
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                refreshFlag = false;
+                refreshing.setValue(false);
+            }
+        });
+    }
+
+
+    private Single<VoiceHistoryResponse> voiceHistory(int page, int pageSize) {
+        return atmobApi.voiceHistory(new VoiceHistoryRequest(page, pageSize))
+                .compose(RxHttpHandler.handle(true))
+                .compose(RxJavaUtil.SingleSchedule.io2Main());
+    }
+}

+ 28 - 0
app/src/main/java/com/atmob/voiceai/data/repositories/MemberRepository.java

@@ -0,0 +1,28 @@
+package com.atmob.voiceai.data.repositories;
+
+
+import androidx.lifecycle.MutableLiveData;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+public class MemberRepository {
+
+
+    private final MutableLiveData<Boolean> isMember = new MutableLiveData<>();
+
+
+    @Inject
+    public MemberRepository() {
+    }
+
+
+    public MutableLiveData<Boolean> getIsMember() {
+        return isMember;
+    }
+
+    public void setIsMember(Boolean isMember) {
+        this.isMember.setValue(isMember);
+    }
+}

+ 19 - 20
app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java

@@ -22,6 +22,7 @@ import com.atmob.voiceai.data.api.response.VoiceInfoResponse;
 import com.atmob.voiceai.data.api.response.VoiceListResponse;
 import com.atmob.voiceai.data.api.response.VoiceTypeResponse;
 import com.atmob.voiceai.utils.BoxingUtil;
+import com.atmob.voiceai.utils.ThreePair;
 import com.atmob.voiceai.utils.ToastUtil;
 
 import java.util.ArrayList;
@@ -42,7 +43,8 @@ public class VoiceAIRepository {
 
     private final AtmobApi atmobApi;
     private final GenerateApi generateApi;
-    private final AccountRepository accountRepository;
+    private final HistoryRepository historyRepository;
+    private final MemberRepository memberRepository;
 
     private boolean textToSpeechRequest;
     private final MutableLiveData<Integer> adFreeGenerateNumber = new MutableLiveData<>();
@@ -50,9 +52,7 @@ public class VoiceAIRepository {
     private VoiceListBean requestCloneBean;
     private String textToSpeechTxt;
 
-    private final MutableLiveData<Pair<Integer, String>> textToSpeechState = new MutableLiveData<>();
-
-    private final MutableLiveData<UserVoiceBean> resultBean = new MutableLiveData<>();
+    private final MutableLiveData<ThreePair<Integer, String, UserVoiceBean>> textToSpeechState = new MutableLiveData<>();
 
     private final MutableLiveData<VoiceListBean> recommendClickBean = new MutableLiveData<>();
 
@@ -67,10 +67,11 @@ public class VoiceAIRepository {
     }
 
     @Inject
-    public VoiceAIRepository(AtmobApi atmobApi, GenerateApi generateApi, AccountRepository accountRepository) {
+    public VoiceAIRepository(AtmobApi atmobApi, GenerateApi generateApi, MemberRepository memberRepository, HistoryRepository historyRepository) {
         this.atmobApi = atmobApi;
         this.generateApi = generateApi;
-        this.accountRepository = accountRepository;
+        this.memberRepository = memberRepository;
+        this.historyRepository = historyRepository;
         refreshVoiceInfo();
     }
 
@@ -86,10 +87,6 @@ public class VoiceAIRepository {
         return recommendClickBean;
     }
 
-    public LiveData<UserVoiceBean> getResultBean() {
-        return resultBean;
-    }
-
     public VoiceListBean getRequestCloneBean() {
         return requestCloneBean;
     }
@@ -98,13 +95,13 @@ public class VoiceAIRepository {
         return textToSpeechTxt;
     }
 
-    public LiveData<Pair<Integer, String>> getTextToSpeechState() {
+    public LiveData<ThreePair<Integer, String, UserVoiceBean>> getTextToSpeechState() {
         return textToSpeechState;
     }
 
 
     private void refreshVoiceInfo() {
-        if (isRequestFreeGenerate || BoxingUtil.boxing(accountRepository.getIsMember().getValue())) {
+        if (isRequestFreeGenerate || BoxingUtil.boxing(memberRepository.getIsMember().getValue())) {
             return;
         }
         requestVoiceInfo().subscribe(new SingleObserver<VoiceInfoResponse>() {
@@ -145,6 +142,7 @@ public class VoiceAIRepository {
                 .compose(RxJavaUtil.SingleSchedule.io2Main());
     }
 
+
     public void requestTextToSpeech(VoiceListBean bean, String content) {
         if (bean == null || textToSpeechRequest) {
             return;
@@ -152,27 +150,25 @@ public class VoiceAIRepository {
         requestCloneBean = bean;
         textToSpeechTxt = content;
 //        /********仅测试使用******/
-//        textToSpeechState.setValue(new Pair<>(TextToSpeechState.GENERATED, null));
 //        UserVoiceBean userVoiceBean = new UserVoiceBean();
 //        userVoiceBean.setVoiceAvatar(bean.getAvatarUrl());
 //        userVoiceBean.setVoiceId(bean.getId());
 //        userVoiceBean.setVoiceName(bean.getName());
 //        userVoiceBean.setContent(content);
 //        userVoiceBean.setVoiceUrl("http://cdn.atmob.com/upload/project_voice/text_to_speech/20240510/986334800900366336.mp3");
-//        resultBean.setValue(userVoiceBean);
+//        textToSpeechState.setValue(new ThreePair<>(TextToSpeechState.GENERATED, null, userVoiceBean));
 //        /********仅测试使用******/
         textToSpeech(bean.getId(), bean.getVoiceType(), content).subscribe(new SingleObserver<TextToSpeechResponse>() {
             @Override
             public void onSubscribe(@NonNull Disposable d) {
                 textToSpeechRequest = true;
-                textToSpeechState.setValue(new Pair<>(TextToSpeechState.GENERATING, null));
+                textToSpeechState.setValue(new ThreePair<>(TextToSpeechState.GENERATING, null, null));
             }
 
             @Override
             public void onSuccess(@NonNull TextToSpeechResponse textToSpeechResponse) {
                 textToSpeechRequest = false;
-                resultBean.setValue(textToSpeechResponse.getUserVoice());
-                textToSpeechState.setValue(new Pair<>(TextToSpeechState.GENERATED, null));
+                textToSpeechState.setValue(new ThreePair<>(TextToSpeechState.GENERATED, null, textToSpeechResponse.getUserVoice()));
             }
 
             @Override
@@ -180,9 +176,9 @@ public class VoiceAIRepository {
                 textToSpeechRequest = false;
                 if (throwable instanceof RxHttpHandler.ServerErrorException) {
                     RxHttpHandler.ServerErrorException serverErrorException = (RxHttpHandler.ServerErrorException) throwable;
-                    textToSpeechState.setValue(new Pair<>(TextToSpeechState.ERROR, serverErrorException.getMsg()));
+                    textToSpeechState.setValue(new ThreePair<>(TextToSpeechState.ERROR, serverErrorException.getMsg(), null));
                 } else {
-                    textToSpeechState.setValue(new Pair<>(TextToSpeechState.ERROR, ContextUtil.getContext().getString(R.string.generate_error)));
+                    textToSpeechState.setValue(new ThreePair<>(TextToSpeechState.ERROR, ContextUtil.getContext().getString(R.string.generate_error), null));
                 }
             }
         });
@@ -192,7 +188,10 @@ public class VoiceAIRepository {
         return generateApi.textToSpeech(new TextTosSpeechRequest(id, voiceType, content))
                 .compose(RxHttpHandler.handle(true))
                 .compose(RxJavaUtil.SingleSchedule.io2Main())
-                .doOnSuccess(data -> refreshVoiceInfo());
+                .doOnSuccess(data -> {
+                    refreshVoiceInfo();
+                    historyRepository.refreshCloneHistory(1);
+                });
     }
 
 

+ 2 - 2
app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingActivity.java

@@ -39,8 +39,8 @@ public class VoiceGeneratingActivity extends BaseActivity<ActivityVoiceGeneratin
     }
 
     private void initObserver() {
-        voiceGeneratingViewModel.getGenerateSuccess().observe(this, o -> {
-            VoiceResultActivity.start(this);
+        voiceGeneratingViewModel.getGenerateSuccess().observe(this, userVoiceBean -> {
+            VoiceResultActivity.start(this, userVoiceBean);
             finish();
         });
         voiceGeneratingViewModel.getGenerateError().observe(this, txt -> {

+ 6 - 4
app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingViewModel.java

@@ -12,8 +12,10 @@ import com.atmob.app.lib.base.BaseViewModel;
 import com.atmob.app.lib.livedata.SingleLiveEvent;
 import com.atmob.common.runtime.ContextUtil;
 import com.atmob.voiceai.R;
+import com.atmob.voiceai.data.api.bean.UserVoiceBean;
 import com.atmob.voiceai.data.api.bean.VoiceListBean;
 import com.atmob.voiceai.data.repositories.VoiceAIRepository;
+import com.atmob.voiceai.utils.ThreePair;
 
 import java.util.concurrent.TimeUnit;
 
@@ -27,12 +29,12 @@ public class VoiceGeneratingViewModel extends BaseViewModel {
 
     private final MutableLiveData<String> generatingTime = new MutableLiveData<>();
 
-    private final SingleLiveEvent<?> generateSuccess = new SingleLiveEvent<>();
+    private final SingleLiveEvent<UserVoiceBean> generateSuccess = new SingleLiveEvent<>();
     private final SingleLiveEvent<String> generateError = new SingleLiveEvent<>();
 
     private final VoiceAIRepository voiceAIRepository;
 
-    private final Observer<Pair<Integer, String>> stateObserver;
+    private final Observer<ThreePair<Integer, String, UserVoiceBean>> stateObserver;
 
     @Inject
     public VoiceGeneratingViewModel(VoiceAIRepository voiceAIRepository) {
@@ -45,13 +47,13 @@ public class VoiceGeneratingViewModel extends BaseViewModel {
             if (state.first == VoiceAIRepository.TextToSpeechState.ERROR) {
                 generateError.setValue(state.second);
             } else if (state.first == VoiceAIRepository.TextToSpeechState.GENERATED) {
-                generateSuccess.call();
+                generateSuccess.setValue(state.third);
             }
         };
         voiceAIRepository.getTextToSpeechState().observeForever(stateObserver);
     }
 
-    public LiveData<?> getGenerateSuccess() {
+    public LiveData<UserVoiceBean> getGenerateSuccess() {
         return generateSuccess;
     }
 

+ 61 - 1
app/src/main/java/com/atmob/voiceai/module/history/HistoryFragment.java

@@ -1,21 +1,81 @@
 package com.atmob.voiceai.module.history;
 
+import android.os.Bundle;
+import android.view.View;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 
 import com.atmob.app.lib.base.BaseFragment;
 import com.atmob.voiceai.databinding.FragmentHistoryBinding;
+import com.atmob.voiceai.utils.BoxingUtil;
+import com.atmob.voiceai.utils.LinearEndLessOnScrollListener;
 import com.gyf.immersionbar.ImmersionBar;
 
 import dagger.hilt.android.AndroidEntryPoint;
-import dagger.hilt.android.HiltAndroidApp;
 
 @AndroidEntryPoint
 public class HistoryFragment extends BaseFragment<FragmentHistoryBinding> {
 
 
+    private HistoryViewModel historyViewModel;
+
+    private HistoryItemAdapter historyItemAdapter;
+    private LinearEndLessOnScrollListener linearEndLessOnScrollListener;
+
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        initView();
+        initObserver();
+    }
+
+    private void initView() {
+        historyItemAdapter = new HistoryItemAdapter(this);
+        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
+        binding.rvHistory.setLayoutManager(linearLayoutManager);
+        binding.rvHistory.setAdapter(historyItemAdapter);
+        historyItemAdapter.setActionHandler(cloneHistoryBean -> historyViewModel.onHistoryItemClick(cloneHistoryBean));
+        linearEndLessOnScrollListener = new LinearEndLessOnScrollListener(linearLayoutManager) {
+            @Override
+            public void onLoadMore(int currentPage) {
+                historyViewModel.refreshHistoryList(currentPage);
+            }
+        };
+        binding.rvHistory.addOnScrollListener(linearEndLessOnScrollListener);
+        binding.swipeHistoryLayout.setOnRefreshListener(() -> {
+            linearEndLessOnScrollListener.reset();
+            historyViewModel.refreshHistoryList(1);
+        });
+    }
+
+    private void initObserver() {
+        historyViewModel.getVoiceHistoryList().observe(getViewLifecycleOwner(), list -> historyItemAdapter.submitList(list, () -> {
+            if (historyViewModel.isSmoothScrollToTop()) {
+                binding.rvHistory.scrollToPosition(0);
+            }
+
+        }));
+        historyViewModel.getRefreshing().observe(getViewLifecycleOwner(), refreshing -> {
+            if (!BoxingUtil.boxing(refreshing)) {
+                binding.swipeHistoryLayout.setRefreshing(false);
+            }
+        });
+    }
+
     @Override
     protected void configImmersion(@NonNull ImmersionBar immersionBar) {
         super.configImmersion(immersionBar);
         immersionBar.statusBarDarkFont(false);
     }
+
+    @Override
+    protected void initViewModel() {
+        super.initViewModel();
+        historyViewModel = getViewModelProvider().get(HistoryViewModel.class);
+        binding.setHistoryViewModel(historyViewModel);
+    }
 }

+ 99 - 0
app/src/main/java/com/atmob/voiceai/module/history/HistoryItemAdapter.java

@@ -0,0 +1,99 @@
+package com.atmob.voiceai.module.history;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.atmob.voiceai.data.api.bean.UserVoiceBean;
+import com.atmob.voiceai.databinding.ItemHistoryCloneVoiceBinding;
+
+import java.util.List;
+import java.util.Objects;
+
+public class HistoryItemAdapter extends RecyclerView.Adapter<HistoryItemAdapter.ViewHolder> {
+
+
+    private final AsyncListDiffer<UserVoiceBean> listDiffer;
+
+    private final LifecycleOwner lifecycleOwner;
+
+    private ActionHandler actionHandler;
+
+    public HistoryItemAdapter(LifecycleOwner lifecycleOwner) {
+        this.lifecycleOwner = lifecycleOwner;
+        listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<UserVoiceBean>() {
+            @Override
+            public boolean areItemsTheSame(@NonNull UserVoiceBean oldItem, @NonNull UserVoiceBean newItem) {
+                return oldItem.getId() == newItem.getId();
+            }
+
+            @Override
+            public boolean areContentsTheSame(@NonNull UserVoiceBean oldItem, @NonNull UserVoiceBean newItem) {
+                return Objects.equals(oldItem.getVoiceAvatar(), newItem.getVoiceAvatar())
+                        && Objects.equals(oldItem.getVoiceName(), newItem.getVoiceName())
+                        && Objects.equals(oldItem.getContent(), newItem.getContent())
+                        && Objects.equals(oldItem.getCreateTime(), newItem.getCreateTime());
+            }
+        });
+    }
+
+    public void setActionHandler(ActionHandler actionHandler) {
+        this.actionHandler = actionHandler;
+    }
+
+    @NonNull
+    @Override
+    public HistoryItemAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        Context context = parent.getContext();
+        LayoutInflater layoutInflater = LayoutInflater.from(context);
+        return new ViewHolder(ItemHistoryCloneVoiceBinding.inflate(layoutInflater, parent, false));
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull HistoryItemAdapter.ViewHolder holder, int position) {
+        holder.bind(listDiffer.getCurrentList().get(position));
+    }
+
+    @Override
+    public int getItemCount() {
+        return listDiffer.getCurrentList().size();
+    }
+
+    public void submitList(List<UserVoiceBean> list, Runnable runnable) {
+        listDiffer.submitList(list, runnable);
+    }
+
+    public class ViewHolder extends RecyclerView.ViewHolder {
+
+        private final ItemHistoryCloneVoiceBinding binding;
+
+        public ViewHolder(@NonNull ItemHistoryCloneVoiceBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+            binding.setLifecycleOwner(lifecycleOwner);
+            binding.setItemClick(v -> {
+                if (actionHandler != null) {
+                    UserVoiceBean bean = binding.getHistoryBean();
+                    if (bean != null) {
+                        actionHandler.onItemClicked(bean);
+                    }
+                }
+            });
+        }
+
+        public void bind(UserVoiceBean cloneHistoryBean) {
+            binding.setHistoryBean(cloneHistoryBean);
+        }
+    }
+
+
+    public interface ActionHandler {
+        void onItemClicked(@NonNull UserVoiceBean cloneHistoryBean);
+    }
+}

+ 68 - 0
app/src/main/java/com/atmob/voiceai/module/history/HistoryViewModel.java

@@ -0,0 +1,68 @@
+package com.atmob.voiceai.module.history;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.common.runtime.ActivityUtil;
+import com.atmob.voiceai.data.api.bean.UserVoiceBean;
+import com.atmob.voiceai.data.repositories.HistoryRepository;
+import com.atmob.voiceai.data.repositories.MemberRepository;
+import com.atmob.voiceai.module.result.VoiceResultActivity;
+import com.atmob.voiceai.module.setting.SettingActivity;
+import com.atmob.voiceai.module.subscription.SubscriptionPageActivity;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+import dagger.hilt.android.lifecycle.HiltViewModel;
+
+@HiltViewModel
+public class HistoryViewModel extends BaseViewModel {
+
+
+    private final HistoryRepository historyRepository;
+    private final MemberRepository memberRepository;
+
+
+    @Inject
+    public HistoryViewModel(HistoryRepository historyRepository, MemberRepository memberRepository) {
+        this.historyRepository = historyRepository;
+        this.memberRepository = memberRepository;
+    }
+
+    public boolean isSmoothScrollToTop() {
+        return historyRepository.isSmoothScrollToTop();
+    }
+
+
+    public LiveData<List<UserVoiceBean>> getVoiceHistoryList() {
+        return historyRepository.getVoiceHistoryList();
+    }
+
+    public LiveData<Boolean> isMember() {
+        return memberRepository.getIsMember();
+    }
+
+    public void onSettingClick() {
+        SettingActivity.start(ActivityUtil.getTopActivity());
+    }
+
+
+    public void onGoSubscriptionClick() {
+        SubscriptionPageActivity.start(ActivityUtil.getTopActivity());
+    }
+
+    public void onHistoryItemClick(@NonNull UserVoiceBean cloneHistoryBean) {
+        VoiceResultActivity.start(ActivityUtil.getTopActivity(), cloneHistoryBean);
+    }
+
+    public void refreshHistoryList(int page) {
+        historyRepository.refreshCloneHistory(page);
+    }
+
+    public LiveData<Boolean> getRefreshing() {
+        return historyRepository.getRefreshing();
+    }
+}

+ 2 - 1
app/src/main/java/com/atmob/voiceai/module/main/MainViewModel.java

@@ -1,6 +1,7 @@
 package com.atmob.voiceai.module.main;
 
 import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.voiceai.data.repositories.AccountRepository;
 
 import javax.inject.Inject;
 
@@ -12,6 +13,6 @@ public class MainViewModel extends BaseViewModel {
 
 
     @Inject
-    public MainViewModel() {
+    public MainViewModel(AccountRepository accountRepository) {
     }
 }

+ 11 - 1
app/src/main/java/com/atmob/voiceai/module/result/VoiceResultActivity.java

@@ -33,16 +33,18 @@ import dagger.hilt.android.AndroidEntryPoint;
 public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding> {
 
 
+    private static final String USER_VOICE_BEAN = "user_voice_bean";
     private VoiceResultViewModel voiceResultViewModel;
     private ExoPlayer player;
 
     private VoiceRecommendAdapter voiceRecommendAdapter;
 
-    public static void start(Context context) {
+    public static void start(Context context, UserVoiceBean userVoiceBean) {
         Intent intent = new Intent(context, VoiceResultActivity.class);
         if (!(context instanceof Activity)) {
             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         }
+        intent.putExtra(USER_VOICE_BEAN, userVoiceBean);
         context.startActivity(intent);
     }
 
@@ -50,10 +52,18 @@ public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        initData();
         initView();
         initObserver();
     }
 
+    private void initData() {
+        Intent intent = getIntent();
+        if (intent != null) {
+            voiceResultViewModel.setVoiceBean(intent.getParcelableExtra(USER_VOICE_BEAN));
+        }
+    }
+
     private void initView() {
         intVoiceRecommendList();
         initMedia3ExoPlayer();

+ 8 - 1
app/src/main/java/com/atmob/voiceai/module/result/VoiceResultViewModel.java

@@ -1,5 +1,7 @@
 package com.atmob.voiceai.module.result;
 
+import android.os.Parcelable;
+
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
@@ -47,6 +49,8 @@ public class VoiceResultViewModel extends BaseViewModel {
     private final MutableLiveData<Long> currentDuration = new MutableLiveData<>();
     private final SingleLiveEvent<?> refreshAudioCurrentProgress = new SingleLiveEvent<>();
     private final MutableLiveData<List<VoiceListBean>> voiceList = new MutableLiveData<>();
+
+    private final MutableLiveData<UserVoiceBean> userVoiceBean = new MutableLiveData<>();
     private final OkHttpClient okHttpClient;
     private boolean isSeekbarChanging;
     private Timer timer;
@@ -81,7 +85,7 @@ public class VoiceResultViewModel extends BaseViewModel {
     }
 
     public LiveData<UserVoiceBean> getResultBean() {
-        return voiceAIRepository.getResultBean();
+        return userVoiceBean;
     }
 
 
@@ -230,4 +234,7 @@ public class VoiceResultViewModel extends BaseViewModel {
     }
 
 
+    public void setVoiceBean(UserVoiceBean userVoiceBean) {
+        this.userVoiceBean.setValue(userVoiceBean);
+    }
 }

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

@@ -40,6 +40,7 @@ public class SettingActivity extends BaseActivity<SettingActivityBinding> {
 
     private void initView() {
         addTopStatusBarHeight(binding.toolbar);
+        binding.toolbar.setNavigationOnClickListener(v -> finish());
     }
 
     @Override

+ 6 - 8
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java

@@ -5,7 +5,6 @@ import android.util.Pair;
 
 import androidx.annotation.ColorInt;
 import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MediatorLiveData;
 import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.Observer;
 import androidx.lifecycle.Transformations;
@@ -18,10 +17,9 @@ import com.atmob.common.runtime.ContextUtil;
 import com.atmob.voiceai.R;
 import com.atmob.voiceai.data.api.bean.TypeListBean;
 import com.atmob.voiceai.data.api.bean.VoiceListBean;
-import com.atmob.voiceai.data.api.response.VoiceInfoResponse;
 import com.atmob.voiceai.data.api.response.VoiceListResponse;
 import com.atmob.voiceai.data.api.response.VoiceTypeResponse;
-import com.atmob.voiceai.data.repositories.AccountRepository;
+import com.atmob.voiceai.data.repositories.MemberRepository;
 import com.atmob.voiceai.data.repositories.VoiceAIRepository;
 import com.atmob.voiceai.helper.ErrorHelper;
 import com.atmob.voiceai.module.generating.VoiceGeneratingActivity;
@@ -50,7 +48,6 @@ public class VoiceAIViewModel extends BaseViewModel {
     private final LiveData<CharSequence> voiceProbationTxt;
     private final MutableLiveData<String> voicePrintTxt = new MutableLiveData<>();
     private final LiveData<String> voicePrintLimitTxt;
-    private final AccountRepository accountRepository;
 
     @ColorInt
     private final int freeTxtColor;
@@ -59,6 +56,7 @@ public class VoiceAIViewModel extends BaseViewModel {
 
     private final MutableLiveData<List<TypeListBean>> voiceTypeList = new MutableLiveData<>();
     private final MutableLiveData<Pair<List<VoiceListBean>, VoiceListBean>> voiceDetailList = new MutableLiveData<>();
+    private final MemberRepository memberRepository;
 
     private VoiceListBean choiceVoiceBean;
 
@@ -75,8 +73,8 @@ public class VoiceAIViewModel extends BaseViewModel {
     Observer<VoiceListBean> recommendObserver;
 
     @Inject
-    public VoiceAIViewModel(AccountRepository accountRepository, VoiceAIRepository voiceAIRepository) {
-        this.accountRepository = accountRepository;
+    public VoiceAIViewModel(MemberRepository memberRepository, VoiceAIRepository voiceAIRepository) {
+        this.memberRepository = memberRepository;
         this.voiceAIRepository = voiceAIRepository;
         voicePrintLimitTxt = Transformations.map(voicePrintTxt, txt -> {
             int length = 0;
@@ -88,7 +86,7 @@ public class VoiceAIViewModel extends BaseViewModel {
         freeTxtColor = ContextUtil.getApplication().getResources().getColor(R.color.voice_free_crux_txt_color);
         voicePrintTxt.setValue(KVUtils.getDefault().getString(LAST_GENERATE_TEXT, ""));
         voiceProbationTxt = Transformations.map(voiceAIRepository.getAdFreeGenerateNumber(), number -> {
-            if (number == null || BoxingUtil.boxing(accountRepository.getIsMember().getValue())) {
+            if (number == null || BoxingUtil.boxing(memberRepository.getIsMember().getValue())) {
                 return "";
             }
             return SpannableUtil.getSpannableStringBuilder(ContextUtil.getContext().getString(R.string.voice_ai_get_free_number, number), String.valueOf(number), freeTxtColor, false);
@@ -130,7 +128,7 @@ public class VoiceAIViewModel extends BaseViewModel {
     }
 
     public LiveData<Boolean> isMember() {
-        return accountRepository.getIsMember();
+        return memberRepository.getIsMember();
     }
 
     public LiveData<CharSequence> getVoiceProbationTxt() {

+ 8 - 0
app/src/main/java/com/atmob/voiceai/utils/DateUtil.java

@@ -1,6 +1,7 @@
 package com.atmob.voiceai.utils;
 
 import java.text.SimpleDateFormat;
+import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
 
@@ -26,4 +27,11 @@ public class DateUtil {
         SimpleDateFormat dateFormat = new SimpleDateFormat(format, Locale.getDefault());
         return dateFormat.format(date);
     }
+
+    public static String getUSTime(long timestamp) {
+        //Apr 12,2024 10:10 AM
+        SimpleDateFormat sdf = new SimpleDateFormat("MMM dd,yyyy hh:mm a", Locale.US);
+        return sdf.format(new Date(timestamp));
+
+    }
 }

+ 70 - 0
app/src/main/java/com/atmob/voiceai/utils/LinearEndLessOnScrollListener.java

@@ -0,0 +1,70 @@
+package com.atmob.voiceai.utils;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.atmob.common.logging.AtmobLog;
+
+public abstract class LinearEndLessOnScrollListener extends RecyclerView.OnScrollListener {
+
+
+    private final LinearLayoutManager mLinearLayoutManager;
+    //当前页,从1开始
+    private int currentPage = 1;
+
+    //已经加载出来的Item的数量
+    private int totalItemCount;
+
+    //主要用来存储上一个totalItemCount
+    private int previousTotal = 0;
+
+    //在屏幕上可见的item数量
+    private int visibleItemCount;
+
+    //在屏幕可见的Item中的第一个
+    private int firstVisibleItem;
+
+    //是否正在上拉数据
+    private boolean loading = true;
+
+    public LinearEndLessOnScrollListener(LinearLayoutManager linearLayoutManager) {
+        this.mLinearLayoutManager = linearLayoutManager;
+    }
+
+    @Override
+    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+        super.onScrollStateChanged(recyclerView, newState);
+    }
+
+    @Override
+    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+        super.onScrolled(recyclerView, dx, dy);
+        visibleItemCount = recyclerView.getChildCount();
+        totalItemCount = mLinearLayoutManager.getItemCount();
+        firstVisibleItem = mLinearLayoutManager.findFirstVisibleItemPosition();
+        if (loading) {
+//            AtmobLog.d("zk","firstVisibleItem: " +firstVisibleItem);
+//            AtmobLog.d("zk","totalPageCount:" +totalItemCount);
+//            AtmobLog.d("zk", "visibleItemCount:" + visibleItemCount);
+
+            if (totalItemCount > previousTotal) {
+                loading = false;
+                previousTotal = totalItemCount;
+            }
+        }
+        //这里需要好好理解
+        if (!loading && totalItemCount - visibleItemCount <= firstVisibleItem) {
+            currentPage++;
+            onLoadMore(currentPage);
+            loading = true;
+        }
+    }
+
+    public abstract void onLoadMore(int currentPage);
+
+    public void reset() {
+        currentPage = 1;
+        loading = false;
+    }
+}

+ 41 - 0
app/src/main/java/com/atmob/voiceai/utils/ThreePair.java

@@ -0,0 +1,41 @@
+package com.atmob.voiceai.utils;
+
+import android.util.Pair;
+
+import java.util.Objects;
+
+public class ThreePair<F, S, T> {
+
+    public final F first;
+    public final S second;
+    public final T third;
+
+    public ThreePair(F first, S second, T third) {
+        this.first = first;
+        this.second = second;
+        this.third = third;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof Pair)) {
+            return false;
+        }
+        Pair<?, ?> p = (Pair<?, ?>) o;
+        return Objects.equals(p.first, first) && Objects.equals(p.second, second) && Objects.equals(p.second, third);
+    }
+
+    @Override
+    public int hashCode() {
+        return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode()) ^ (third == null ? 0 : third.hashCode());
+    }
+
+    @Override
+    public String toString() {
+        return "Pair{" + String.valueOf(first) + " " + String.valueOf(second) + " " + String.valueOf(third) + "}";
+    }
+
+    public static <A, B, C> ThreePair<A, B, C> create(A a, B b, C c) {
+        return new ThreePair<A, B, C>(a, b, c);
+    }
+}

二進制
app/src/main/res/drawable-xxhdpi/icon_history_create_time.webp


二進制
app/src/main/res/drawable-xxhdpi/icon_history_no_data.webp


+ 5 - 0
app/src/main/res/drawable/bg_history_item.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="12dp" />
+    <solid android:color="#32333C" />
+</shape>

+ 13 - 1
app/src/main/res/layout/activity_voice_result.xml

@@ -256,13 +256,25 @@
             android:layout_width="0dp"
             android:layout_height="0dp"
             android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
-            android:onClick="@{()-> voiceResultViewModel.onBackClick()}"
             android:src="@drawable/icon_result_back"
             app:layout_constraintDimensionRatio="1:1"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@+id/sapce1"
             app:layout_constraintWidth_percent="0.0666666666666667" />
 
+        <View
+            android:background="@drawable/bg_ripple_common_oval_mask"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:onClick="@{()-> voiceResultViewModel.onBackClick()}"
+            android:padding="12dp"
+            app:layout_constraintBottom_toBottomOf="@id/iv_back"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintEnd_toEndOf="@+id/iv_back"
+            app:layout_constraintStart_toStartOf="@+id/iv_back"
+            app:layout_constraintTop_toTopOf="@id/iv_back"
+            app:layout_constraintWidth_percent="0.1111111111111111" />
+
 
         <TextView
             android:id="@+id/tv_save"

+ 155 - 4
app/src/main/res/layout/fragment_history.xml

@@ -1,6 +1,157 @@
 <?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="historyViewModel"
+            type="com.atmob.voiceai.module.history.HistoryViewModel" />
+
+        <import type="com.atmob.common.ui.SizeUtil" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/colorPrimary">
+
+        <ImageView
+            android:id="@+id/iv_header_bg"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:src="@drawable/bg_voice_ai_header_top_bg"
+            app:layout_constraintDimensionRatio="836:556"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintWidth_percent="0.5083333333333333" />
+
+        <Space
+            android:id="@+id/space_status_bar"
+            android:layout_width="match_parent"
+            android:layout_height="@{SizeUtil.getStatusBarHeight(), default=@dimen/app_status_bar_height}"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <View
+            android:id="@+id/v_tool_bar"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:44"
+            app:layout_constraintTop_toBottomOf="@+id/space_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="@{()-> historyViewModel.onGoSubscriptionClick()}"
+            app:layout_constraintBottom_toBottomOf="@+id/v_tool_bar"
+            app:layout_constraintDimensionRatio="58:28"
+            app:layout_constraintStart_toStartOf="@+id/v_tool_bar"
+            app:layout_constraintTop_toTopOf="@+id/v_tool_bar"
+            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" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/voice_ai"
+            android:textColor="@color/white"
+            android:textSize="18sp"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="@+id/v_tool_bar"
+            app:layout_constraintEnd_toEndOf="@+id/v_tool_bar"
+            app:layout_constraintStart_toStartOf="@+id/v_tool_bar"
+            app:layout_constraintTop_toTopOf="@+id/v_tool_bar" />
+
+        <ImageView
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginEnd="@dimen/app_common_page_horizontal_padding"
+            android:onClick="@{()-> historyViewModel.onSettingClick()}"
+            android:src="@drawable/icon_voice_ai_menu"
+            app:layout_constraintBottom_toBottomOf="@+id/v_tool_bar"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintEnd_toEndOf="@+id/v_tool_bar"
+            app:layout_constraintTop_toTopOf="@+id/v_tool_bar"
+            app:layout_constraintWidth_percent="0.0666666666666667" />
+
+        <androidx.constraintlayout.widget.Group
+            isGone="@{historyViewModel.isMember}"
+            app:constraint_referenced_ids="v_vip_bg,iv_vip,tv_vip_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+
+        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+            android:id="@+id/swipe_history_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:layout_constrainedHeight="true"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/v_tool_bar">
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/rv_history"
+                isGone="@{historyViewModel.voiceHistoryList.size() == 0}"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+
+        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+
+        <ImageView
+            android:id="@+id/iv_no_data"
+            isGone="@{historyViewModel.voiceHistoryList.size() > 0}"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:src="@drawable/icon_history_no_data"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintDimensionRatio="504:301"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/v_tool_bar"
+            app:layout_constraintVertical_bias="0.3197115384615385"
+            app:layout_constraintWidth_percent="0.4666666666666667" />
+
+        <TextView
+            isGone="@{historyViewModel.voiceHistoryList.size() > 0}"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:text="@string/history_no_data"
+            android:textColor="@color/white80"
+            android:textSize="14sp"
+            app:layout_constraintEnd_toEndOf="@+id/iv_no_data"
+            app:layout_constraintStart_toStartOf="@+id/iv_no_data"
+            app:layout_constraintTop_toBottomOf="@+id/iv_no_data" />
+
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 100 - 0
app/src/main/res/layout/item_history_clone_voice.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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">
+
+    <data>
+
+        <variable
+            name="historyBean"
+            type="com.atmob.voiceai.data.api.bean.UserVoiceBean" />
+
+        <variable
+            name="itemClick"
+            type="android.view.View.OnClickListener" />
+
+        <import type="com.atmob.voiceai.utils.DateUtil" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="8dp">
+
+
+        <View
+            android:id="@+id/bg_item_history"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginHorizontal="@dimen/app_common_page_horizontal_padding"
+            android:background="@drawable/bg_history_item"
+            android:onClick="@{itemClick}"
+            app:layout_constraintDimensionRatio="336:84"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <ImageView
+            android:id="@+id/iv_item_history_voice_avatar"
+            imageUrl="@{historyBean.voiceAvatar}"
+            radius="@{8}"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
+            app:layout_constraintBottom_toBottomOf="@+id/bg_item_history"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintStart_toStartOf="@+id/bg_item_history"
+            app:layout_constraintTop_toTopOf="@+id/bg_item_history"
+            app:layout_constraintWidth_percent="0.1666666666666667"
+            tools:src="@drawable/icon_setting_logo" />
+
+        <TextView
+            android:id="@+id/tv_item_history_voice_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
+            android:text="@{historyBean.voiceName}"
+            android:textColor="@color/white"
+            android:textSize="17sp"
+            android:textStyle="bold"
+            app:layout_constraintStart_toEndOf="@+id/iv_item_history_voice_avatar"
+            app:layout_constraintTop_toTopOf="@+id/iv_item_history_voice_avatar"
+            tools:text="Taylor Swift" />
+
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="2dp"
+            android:layout_marginEnd="18dp"
+            android:singleLine="true"
+            android:text="@{historyBean.content}"
+            android:textColor="@color/white80"
+            android:textSize="14sp"
+            app:layout_constraintEnd_toEndOf="@+id/bg_item_history"
+            app:layout_constraintStart_toStartOf="@+id/tv_item_history_voice_name"
+            app:layout_constraintTop_toBottomOf="@+id/tv_item_history_voice_name"
+            tools:text="Record or upload an audio file to creasdasdadasd" />
+
+        <ImageView
+            android:id="@+id/icon_create_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/icon_history_create_time"
+            app:layout_constraintBottom_toBottomOf="@+id/tv_time"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintStart_toEndOf="@+id/iv_item_history_voice_avatar"
+            app:layout_constraintStart_toStartOf="@+id/tv_item_history_voice_name"
+            app:layout_constraintTop_toTopOf="@+id/tv_time" />
+
+        <TextView
+            android:id="@+id/tv_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="3dp"
+            android:text="@{DateUtil.getUSTime(historyBean.createTime)}"
+            android:textColor="@color/white50"
+            android:textSize="12sp"
+            app:layout_constraintBottom_toBottomOf="@+id/iv_item_history_voice_avatar"
+            app:layout_constraintStart_toEndOf="@+id/icon_create_time"
+            tools:text="Apr 12,2024 10:10 AM" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

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

@@ -36,4 +36,5 @@
     <string name="setting_terms_of_service">Terms Of Service</string>
     <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>
 </resources>