package com.gh.base import android.app.Application import android.content.Intent import android.graphics.Bitmap import android.media.ThumbnailUtils import android.provider.MediaStore import android.text.TextUtils import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import com.gh.gamecenter.common.base.fragment.WaitingDialogFragment import com.gh.gamecenter.core.runOnUiThread import com.gh.gamecenter.R import com.gh.gamecenter.common.utils.* import com.gh.gamecenter.core.utils.* import com.gh.gamecenter.entity.ErrorEntity import com.gh.gamecenter.entity.LocalVideoEntity import com.gh.gamecenter.entity.QuoteCountEntity import com.gh.gamecenter.qa.BbsType import com.gh.gamecenter.common.retrofit.Response import com.gh.gamecenter.retrofit.RetrofitManager import com.gh.gamecenter.retrofit.service.ApiService import com.gh.gamecenter.video.upload.OnUploadListener import com.gh.gamecenter.video.upload.UploadManager import com.google.gson.JsonObject import com.lightgame.utils.Utils import com.zhihu.matisse.Matisse import com.zhihu.matisse.internal.utils.PathUtils import io.reactivex.disposables.Disposable import okhttp3.ResponseBody import retrofit2.HttpException import java.io.File import java.io.FileOutputStream import java.util.* import kotlin.collections.HashMap import kotlin.collections.LinkedHashMap import kotlin.collections.List import kotlin.collections.Map import kotlin.collections.find import kotlin.collections.forEach import kotlin.collections.set // TODO: 移动到module_bbs模块 abstract class BaseRichEditorViewModel(application: Application) : AndroidViewModel(application) { val mApi: ApiService = RetrofitManager.getInstance().api val processDialog = MediatorLiveData() val uploadingImage = ArrayList>() val chooseImagesUpload = MutableLiveData>() val chooseImagesUploadSuccess = MutableLiveData>() var uploadImageSubscription: Disposable? = null val mapImages = HashMap() val localVideoList = ArrayList() val uploadVideoErrorList = ArrayList() var currentUploadingVideo: LocalVideoEntity? = null var type: String = "" //游戏论坛:game_bbs 官方论坛:official_bbs private var mUploadVideoListener: UploadVideoListener? = null val TITLE_MIN_LENGTH = 6 val MIN_TEXT_LENGTH = 6 val MAX_TEXT_LENGTH = 10000 val FILE_HOST = "file:///" var id = ""//视频标记 var videoId = ""//更改封面视频id val quoteCountEntity = QuoteCountEntity()//数据上报用 fun setUploadVideoListener(uploadVideoListener: UploadVideoListener) { this.mUploadVideoListener = uploadVideoListener } //检查图片是否符合规则并上传图片 fun uploadPic(data: Intent) { val uris = Matisse.obtainResult(data) val pictureList = ArrayList() for (uri in uris) { val picturePath = PathUtils.getPath(getApplication(), uri) if (picturePath != null) { if (File(picturePath).length() > ImageUtils.getUploadFileMaxSize()) { val count = ImageUtils.getUploadFileMaxSize() / 1024 / 1024 val application: Application = getApplication() Utils.toast( getApplication(), application.getString(R.string.pic_max_hint, count) ) continue } Utils.log("picturePath = $picturePath") pictureList.add(picturePath) } else { Utils.log("picturePath is null") } } if (pictureList.size == 0) return val imageType = when (getRichType()) { RichType.ARTICLE -> UploadImageUtils.UploadType.community_article RichType.QUESTION -> UploadImageUtils.UploadType.question else -> UploadImageUtils.UploadType.poster } uploadImageSubscription = UploadImageUtils.compressAndUploadImageList( imageType, pictureList, false, object : UploadImageUtils.OnUploadImageListListener { override fun onProgress(total: Long, progress: Long) {} override fun onCompressSuccess(imageUrls: List) { val chooseImageMd5Map = LinkedHashMap() imageUrls.forEach { chooseImageMd5Map[MD5Utils.getUrlMD5(it)] = "" } uploadingImage.add(chooseImageMd5Map) chooseImagesUpload.postValue(chooseImageMd5Map) } override fun onSingleSuccess(imageUrl: Map) { val map = LinkedHashMap() for (key in imageUrl.keys) { map[MD5Utils.getUrlMD5(key)] = FILE_HOST + key.decodeURI() mapImages[TextUtils.htmlEncode(key).decodeURI()] = imageUrl[key] ?: "" } chooseImagesUploadSuccess.postValue(map) } override fun onSuccess( imageUrl: LinkedHashMap, errorMap: Map ) { val uploadMap = uploadingImage.find { it.containsKey( MD5Utils.getUrlMD5( imageUrl.entries.iterator().next().key ) ) } uploadMap?.let { uploadingImage.remove(uploadMap) } val errorSize = pictureList.size - imageUrl.size if (errorSize > 0) { val map = LinkedHashMap() for (key in errorMap.keys) { map[MD5Utils.getUrlMD5(key)] = "" } //value为空会删除PlaceholderImage chooseImagesUploadSuccess.postValue(map) for (error in errorMap.values) { if (error is HttpException && error.code() == 403) { Utils.toast(getApplication(), errorSize.toString() + "张违规图片上传失败") return } } Utils.toast(getApplication(), errorSize.toString() + "张图片上传失败") } } override fun onError(errorMap: Map) { val errorSize = errorMap.size if (errorSize > 0) { val map = LinkedHashMap() for (key in errorMap.keys) { map[MD5Utils.getUrlMD5(key)] = "" } //value为空会删除PlaceholderImage chooseImagesUploadSuccess.postValue(map) } for (error in errorMap.values) { if (error is HttpException && error.code() == 403) { val e = error.response()?.errorBody()?.string()?.toObject() if (e != null && e.code == 403017) { Utils.toast( getApplication(), errorSize.toString() + "张图片的宽或高超过限制,请裁剪后上传" ) } else { Utils.toast(getApplication(), errorSize.toString() + "张违规图片上传失败") } return } } if (errorSize == 1) { Utils.toast(getApplication(), "图片上传失败") } else { Utils.toast(getApplication(), errorSize.toString() + "张图片上传失败") } } }) } fun uploadPoster(picturePath: String) { processDialog.postValue(WaitingDialogFragment.WaitingDialogData("封面上传中...", true)) uploadImageSubscription = UploadImageUtils.compressAndUploadImage( UploadImageUtils.UploadType.poster, picturePath, false, object : UploadImageUtils.OnUploadImageListener { override fun onSuccess(imageUrl: String) { patchVideoPoster(imageUrl) } override fun onError(e: Throwable?) { handleUploadPosterResult(true) } override fun onProgress(total: Long, progress: Long) { } }) } private fun patchVideoPoster(poster: String) { if (id.isEmpty() || videoId.isEmpty()) return val map = hashMapOf("poster" to poster, "type" to getVideoType()) mApi.patchInsertVideo(videoId, map.toRequestBody()) .compose(observableToMain()) .subscribe(object : Response() { override fun onResponse(response: ResponseBody?) { super.onResponse(response) mUploadVideoListener?.changePoster(id, poster) handleUploadPosterResult(false) } override fun onFailure(e: HttpException?) { super.onFailure(e) handleUploadPosterResult(true) } }) } private fun handleUploadPosterResult(isFailure: Boolean = false) { processDialog.postValue(WaitingDialogFragment.WaitingDialogData("封面上传中...", false)) if (isFailure) { ToastUtils.showToast("封面更改失败") } id = "" videoId = "" } fun deleteVideo(id: String) { if (localVideoList.isNotEmpty()) { val video = localVideoList.find { it.id == id } if (video != null) { if (UploadManager.isUploading(video.filePath)) { UploadManager.cancelTask(video.filePath) } localVideoList.remove(video) } } if (uploadVideoErrorList.isNotEmpty()) { val video = uploadVideoErrorList.find { it.id == id } if (video != null) { uploadVideoErrorList.remove(video) } } if (currentUploadingVideo?.id == id) { currentUploadingVideo = null uploadVideo() } } fun uploadVideo() { if (currentUploadingVideo != null) return if (localVideoList.isEmpty()) return currentUploadingVideo = localVideoList[0] UploadManager.createUploadTask(currentUploadingVideo?.filePath ?: "", object : OnUploadListener { override fun onProgressChanged( uploadFilePath: String, currentSize: Long, totalSize: Long, speed: Long ) { runOnUiThread { val percent = (currentSize * 100 / totalSize.toFloat()).roundTo(1) currentUploadingVideo?.id?.let { mUploadVideoListener?.updateVideoProgress(it, percent.toString()) } } } override fun onUploadSuccess(uploadFilePath: String, url: String) { if (currentUploadingVideo != null) { postVideoPosterAndInfo(uploadFilePath, url) } } override fun onUploadFailure(uploadFilePath: String, errorMsg: String) { uploadVideoFailure() } }) } private fun postVideoPosterAndInfo(uploadFilePath: String, url: String) { val localVideoPoster = getApplication().cacheDir.absolutePath + File.separator + System.currentTimeMillis() + ".jpg" try { val bmp = ThumbnailUtils.createVideoThumbnail( uploadFilePath, MediaStore.Images.Thumbnails.MINI_KIND ) // bmp 可能为空 FileOutputStream(localVideoPoster).use { out -> bmp?.compress(Bitmap.CompressFormat.PNG, 100, out) } } catch (e: java.lang.Exception) { e.printStackTrace() ToastUtils.showToast("视频封面操作失败") uploadVideoFailure() return } uploadImageSubscription = UploadImageUtils.compressAndUploadImage( UploadImageUtils.UploadType.poster, localVideoPoster, false, object : UploadImageUtils.OnUploadImageListener { override fun onSuccess(imageUrl: String) { postVideoInfo(url, imageUrl) } override fun onError(e: Throwable?) { uploadVideoFailure() } override fun onProgress(total: Long, progress: Long) { } }) } private fun postVideoInfo(url: String, poster: String) { val map = HashMap().apply { put("poster", poster) put("url", url) put("format", currentUploadingVideo?.format ?: "") put("size", currentUploadingVideo?.size ?: 0) put("length", (currentUploadingVideo?.duration ?: 0) / 1000) put("type", getVideoType()) } val requestBody = map.toRequestBody() mApi.insertVideo(requestBody) .compose(observableToMain()) .subscribe(object : Response() { override fun onResponse(response: JsonObject?) { super.onResponse(response) if (response != null) { uploadVideoSuccess(poster, url, response) } } override fun onFailure(e: HttpException?) { super.onFailure(e) uploadVideoFailure() } }) } private fun uploadVideoSuccess(poster: String, url: String, data: JsonObject) { processDialog.postValue(WaitingDialogFragment.WaitingDialogData("封面上传中...", false)) currentUploadingVideo?.let { mUploadVideoListener?.changePoster(it.id, poster) mUploadVideoListener?.videoUploadFinished(it.id, url, data) UploadManager.cancelTask(it.filePath) localVideoList.remove(it) } currentUploadingVideo = null uploadVideo() } private fun uploadVideoFailure() { processDialog.postValue(WaitingDialogFragment.WaitingDialogData("封面上传中...", false)) currentUploadingVideo?.let { runOnUiThread { mUploadVideoListener?.videoUploadFailed(it.id) } uploadVideoErrorList.add(it) localVideoList.remove(it) UploadManager.cancelTask(it.filePath) } currentUploadingVideo = null uploadVideo() } fun checkIsAllUploadedAndToast(): Boolean { if (localVideoList.isNotEmpty() || uploadVideoErrorList.isNotEmpty()) { ToastUtils.showToast("视频未上传完成,视频内容保存失败") return false } return true } private fun getVideoType(): String { return when (type) { BbsType.GAME_BBS.value -> { when (getRichType()) { RichType.ARTICLE -> BbsType.GAME_BBS_ARTICLE_INSERT.value RichType.QUESTION -> BbsType.GAME_BBS_QUESTION_INSERT.value else -> "" } } BbsType.OFFICIAL_BBS.value -> { when (getRichType()) { RichType.ARTICLE -> BbsType.OFFICIAL_BBS_ARTICLE_INSERT.value RichType.QUESTION -> BbsType.OFFICIAL_BBS_QUESTION_INSERT.value else -> "" } } else -> "" } } abstract fun getRichType(): RichType } interface UploadVideoListener { /** * 插入视频占位图 */ fun insertPlaceholderVideo(id: String, poster: String) /** * 更新视频进度条 */ fun updateVideoProgress(id: String, progress: String) /** * 上传视频完成 */ fun videoUploadFinished(id: String, url: String, msg: JsonObject) /** * 更换封面图 */ fun changePoster(id: String, poster: String) /** * 上传失败 */ fun videoUploadFailed(id: String) } enum class RichType { ARTICLE, QUESTION, ANSWER }