diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..bfd3c52 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/comment/CommentEditor.vue b/components/comment/CommentEditor.vue index a73530c..c46ed31 100644 --- a/components/comment/CommentEditor.vue +++ b/components/comment/CommentEditor.vue @@ -184,24 +184,24 @@ const handleEmojiSelect = (emoji: any) => { /** * 选择图片 */ -const chooseImage = () => { - if (uploadedImages.value.length >= 3) { - uni.showToast({ - title: '最多只能上传3张图片', - icon: 'none' - }) - return - } +// const chooseImage = () => { +// if (uploadedImages.value.length >= 3) { +// uni.showToast({ +// title: '最多只能上传3张图片', +// icon: 'none' +// }) +// return +// } - uni.chooseImage({ - count: 3 - uploadedImages.value.length, - sizeType: ['compressed'], - sourceType: ['album', 'camera'], - success: (res) => { - uploadImages(res.tempFilePaths) - } - }) -} +// uni.chooseImage({ +// count: 3 - uploadedImages.value.length, +// sizeType: ['compressed'], +// sourceType: ['album', 'camera'], +// success: (res) => { +// uploadImages(res.tempFilePaths) +// } +// }) +// } /** * 上传图片 diff --git a/manifest.json b/manifest.json index 2258c2c..42f17ee 100644 --- a/manifest.json +++ b/manifest.json @@ -2,8 +2,8 @@ "name" : "吴门国际", "appid" : "__UNI__1250B39", "description" : "吴门国际", - "versionName" : "1.0.7", - "versionCode" : 107, + "versionName" : "1.0.8", + "versionCode" : 108, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { @@ -23,7 +23,7 @@ "prompt" : "template", "template" : { "title" : "用户协议和隐私政策", - "message" : " 请你务必审慎阅读、充分理解“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。
  你可阅读《用户协议》《隐私协议》了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。", + "message" : "请你务必审慎阅读、充分理解“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。
你可阅读《用户协议》《隐私协议》了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。", "buttonAccept" : "同意", "buttonRefuse" : "暂不同意" } @@ -38,27 +38,22 @@ "distribute" : { /* android打包配置 */ "android" : { - "permissions" : [ - "", - "", - "", - "", - "", - "", - "", - "" - ], + "permissions" : [], "abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ], "minSdkVersion" : 23, - "targetSdkVersion" : 35 + "targetSdkVersion" : 35, + "excludePermissions" : [ + "", + "" + ] }, /* ios打包配置 */ "ios" : { "dSYMs" : false, "privacyDescription" : { - "NSPhotoLibraryUsageDescription" : "Ensure the normal use of your avatar modification, appeal feedback, image upload, and message upload functions in this app.", - "NSPhotoLibraryAddUsageDescription" : "Ensure the normal use of the functions of modifying avatars, uploading images for appeals and feedback, and uploading images for comments in this app.", - "NSCameraUsageDescription" : "Ensure the normal use of the functions of modifying avatars, uploading images for appeals and feedback, and uploading images for comments in this app." + "NSPhotoLibraryUsageDescription" : "保障您在此app中的订单售后问题描述上传图片、使用问题反馈上传图片、修改头像功能的正常使用", + "NSPhotoLibraryAddUsageDescription" : "保障您在此app中的订单售后问题描述上传图片、使用问题反馈上传图片、修改头像功能的正常使用", + "NSCameraUsageDescription" : "保障您在此app中的订单售后问题描述上传图片、使用问题反馈上传图片、修改头像功能的正常使用" }, "idfa" : false }, diff --git a/pages/book/listen/index.vue b/pages/book/listen/index.vue index b72efa8..1f650fd 100644 --- a/pages/book/listen/index.vue +++ b/pages/book/listen/index.vue @@ -2,35 +2,31 @@ - - - - - - - {{ bookInfo.name }} - {{ $t('bookDetails.authorName') }}{{ bookInfo.author?.authorName }} - - - {{ $t('bookDetails.buy') }} - + + + + + + {{ bookInfo.name }} + {{ $t('bookDetails.authorName') }}{{ bookInfo.author?.authorName }} - - - - - - {{ $t('book.zjContents') }} - + + {{ $t('bookDetails.buy') }} + + + + + + {{ $t('book.zjContents') }} + {{ nullText }} - - + + import { ref, computed } from 'vue' -import { onLoad } from '@dcloudio/uni-app' +import { onLoad, onShow } from '@dcloudio/uni-app' import { t } from '@/utils/i18n' import { bookApi } from '@/api/modules/book' import { useUserStore } from '@/stores/user' @@ -91,7 +87,6 @@ const bookInfo = ref({ const chapterList = ref([]) const activeIndex = ref(-1) const nullText = ref('') -const scrollHeight = ref(0) // 生命周期 onLoad((options: any) => { @@ -102,11 +97,12 @@ onLoad((options: any) => { fromIndex.value = Number(options.index) activeIndex.value = fromIndex.value } - - initScrollHeight() + loadGoodsInfo() +}) + +onShow(() => { loadBookInfo() loadChapterList() - loadGoodsInfo() }) // 购买弹窗状态 @@ -131,21 +127,6 @@ async function loadGoodsInfo() { goodsList.value = res.productList || [] } -// 初始化滚动区域高度 -function initScrollHeight() { - const systemInfo = uni.getSystemInfoSync() - const statusBarHeight = systemInfo.statusBarHeight || 0 - let navBarHeight = 44 - if (systemInfo.model.includes('iPhone')) { - const modelNumber = parseInt(systemInfo.model.match(/\d+/)?.[0] || '0') - if (modelNumber >= 11) { - navBarHeight = 48 - } - } - const totalNavHeight = statusBarHeight + navBarHeight - scrollHeight.value = systemInfo.windowHeight - totalNavHeight -} - // 加载书籍信息 async function loadBookInfo() { const res = await bookApi.getBookInfo(bookId.value) @@ -212,106 +193,99 @@ function goToPurchase() { .listen-page { background: #f7faf9; min-height: 100vh; +} + +.book-info { + margin: 20rpx; + padding: 30rpx; + background: #fff; + border-radius: 15rpx; + display: flex; + align-items: center; + position: relative; - .listen-scroll { - .book-info { - margin: 20rpx; - padding: 30rpx; - background: #fff; - border-radius: 15rpx; - display: flex; - align-items: center; - position: relative; - - .cover { - width: 180rpx; - height: 240rpx; - border-radius: 10rpx; - flex-shrink: 0; - } - - .info { - flex: 1; - padding: 0 20rpx; - - .title { - display: block; - font-size: 36rpx; - font-weight: bold; - color: #333; - line-height: 44rpx; - max-height: 88rpx; - overflow: hidden; - } - - .author { - display: block; - font-size: 30rpx; - padding-top: 15rpx; - color: #666; - } - } + .cover { + width: 180rpx; + height: 240rpx; + border-radius: 10rpx; + flex-shrink: 0; + } + + .info { + flex: 1; + padding: 0 20rpx; + + .title { + display: block; + font-size: 36rpx; + font-weight: bold; + color: #333; + line-height: 44rpx; + max-height: 88rpx; + overflow: hidden; } - .divider-line { - height: 20rpx; - background: #f7faf9; - } - - .chapter-section { - background: #fff; - padding: 30rpx; - min-height: 400rpx; - - .section-title { - display: block; - font-size: 34rpx; - line-height: 50rpx; - margin-bottom: 20rpx; - color: #333; - font-weight: 500; - } - - .chapter-list { - .chapter-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 18rpx 0; - border-bottom: 1rpx solid #dcdfe6; - - &.active .chapter-text { - color: #54a966; - } - - .chapter-text { - flex: 1; - font-size: 28rpx; - line-height: 50rpx; - color: #333; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - - &.locked { - color: #999; - } - } - - &:last-child { - border-bottom: none; - } - } - } - - .empty-text { - display: block; - text-align: center; - padding: 100rpx 0; - font-size: 28rpx; - color: #999; - } + .author { + display: block; + font-size: 30rpx; + padding-top: 15rpx; + color: #666; } } } + +.chapter-section { + background: #fff; + padding: 30rpx; + min-height: 400rpx; + + .section-title { + display: block; + font-size: 34rpx; + line-height: 50rpx; + margin-bottom: 20rpx; + color: #333; + font-weight: 500; + } + + .chapter-list { + .chapter-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18rpx 0; + border-bottom: 1rpx solid #dcdfe6; + + &.active .chapter-text { + color: #54a966; + } + + .chapter-text { + flex: 1; + font-size: 28rpx; + line-height: 50rpx; + color: #333; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &.locked { + color: #999; + } + } + + &:last-child { + border-bottom: none; + } + } + } + + .empty-text { + display: block; + text-align: center; + padding: 100rpx 0; + font-size: 28rpx; + color: #999; + } +} diff --git a/uni_modules/uni-chooseSystemImage/changelog.md b/uni_modules/uni-chooseSystemImage/changelog.md new file mode 100644 index 0000000..f297143 --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/changelog.md @@ -0,0 +1,10 @@ +## 1.1.4(2025-12-02) +修复部分设备上可能出现的选择图片之后不触发任何回调的bug +## 1.1.2(2025-04-18) +修复部分情况下选中了图片但是没有返回的问题。 +## 1.1.1(2024-11-28) +修复设置count为1时,选择图片提示失败的bug。 +## 1.1.0(2024-10-31) +新增chooseSystemMedia,支持选择图片和视频。 +## 1.0.0(2024-10-23) +新增插件 diff --git a/uni_modules/uni-chooseSystemImage/package.json b/uni_modules/uni-chooseSystemImage/package.json new file mode 100644 index 0000000..6d6aa32 --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/package.json @@ -0,0 +1,114 @@ +{ + "id": "uni-chooseSystemImage", + "displayName": "uni-chooseSystemMedia", + "version": "1.1.4", + "description": "从手机相册中选择图片或视频,解决google play新政策禁止添加媒体权限的问题", + "keywords": [ + "google", + "上架", + "图片选择" +], + "repository": "", + "engines": { + "HBuilderX": "^4.29", + "uni-app": "^3.99", + "uni-app-x": "^3.99" + }, + "dcloudext": { + "type": "uts", + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "本插件不会采集任何隐私信息,获取权限仅是为了兼容android12及以下版本的系统。", + "permissions": "" + }, + "npmurl": "", + "darkmode": "x", + "i18n": "x", + "widescreen": "x" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "√", + "aliyun": "√", + "alipay": "√" + }, + "client": { + "uni-app": { + "vue": { + "vue2": { + "extVersion": "1.0.0", + "minVersion": "" + }, + "vue3": { + "extVersion": "1.0.0", + "minVersion": "" + } + }, + "web": { + "safari": "x", + "chrome": "x" + }, + "app": { + "vue": { + "extVersion": "1.0.0", + "minVersion": "" + }, + "nvue": { + "extVersion": "1.1.0", + "minVersion": "" + }, + "android": { + "extVersion": "1.0.0", + "minVersion": "19" + }, + "ios": "x", + "harmony": "x" + }, + "mp": { + "weixin": "x", + "alipay": "x", + "toutiao": "x", + "baidu": "x", + "kuaishou": "x", + "jd": "x", + "harmony": "x", + "qq": "x", + "lark": "x" + }, + "quickapp": { + "huawei": "x", + "union": "x" + } + }, + "uni-app-x": { + "web": { + "safari": "-", + "chrome": "-" + }, + "app": { + "android": "-", + "ios": "-", + "harmony": "-" + }, + "mp": { + "weixin": "-" + } + } + } + } + } +} \ No newline at end of file diff --git a/uni_modules/uni-chooseSystemImage/readme.md b/uni_modules/uni-chooseSystemImage/readme.md new file mode 100644 index 0000000..518a7c3 --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/readme.md @@ -0,0 +1,89 @@ +## chooseSystemMedia + +chooseSystemMedia支持通过系统API选择图片,解决google play新政策要求[移除照片和视频访问权限权限](https://support.google.com/googleplay/android-developer/answer/14115180)。 + +### 引入插件 + +``` +import { + chooseSystemMedia +} from "@/uni_modules/uni-chooseSystemImage" +``` + +### 参数说明 +|参数名称 |类型 |描述 |取值 |默认值 | +|:-- |:-- |:-- |:-- |:-- | +|count |number |最多可以选择的文件个数 |最多支持100个 | | +|mediaType |Array |支持的文件类型 |image:只能选择图片
video:只能选择视频
mix:可以同时选择图片和视频 |['image'] | +|pageOrientation|string |图片选择的方向 |auto:跟随系统方向
landscape:横向显示
portrait:竖向显示 |portrait | +|success |function |成功回调 | | | +|fail |function |失败回调 | | | +|complete |function |完成回调 | | | + +图片选择成功回调: + +|参数名称 |类型 |描述 | +|:-- |:-- |:-- | +|filePaths | Array |选择的文件列表 | + + +图片选择失败回调错误码 + +|错误码 |描述 | +|:-- |:-- | +|2101001|用户取消 | +|2101002|传入的参数异常 | +|2101005|权限申请失败 | +|2101010|其他异常,如果遇到可以评论反馈 | + +### 调用APIK + +```javascript +chooseSystemMedia({ + count: 2, + mediaType: ['image'], + pageOrientation:"portrait", + success: (e) => { + console.log(e.filePaths) + }, + fail: (e) => { + console.log(e) + } +``` + +## chooseSystemImage + +`chooseSystemImage`已废弃,后续不在维护,建议切换成`chooseSystemMedia` + +### 引入插件 + +``` +import { + chooseSystemImage +} from "@/uni_modules/uni-chooseSystemImage" +``` + +### 调用API + +```javascript +chooseSystemImage({ + count: 3, + success: (e) => { + console.log(e.filePaths) + }, + fail: (e) => { + console.log(e) + } +}) +``` + +注意:在Android 11及以上的系统中,调用的是系统的照片选择器。低于android 11的系统中会调用系统的文件选择器。 + +目前android系统的图片选择仅支持选择图片数量,如果需要针对图片压缩,可以使用[uni.compressImage](https://uniapp.dcloud.net.cn/api/media/image.html#compressimage)。 + +引入当前插件时同时需要将照片和视频权限移除。将下面内容拷贝到项目的manifest.json->Android/iOS权限配置->强制移除的权限。 + +```xml + + +``` \ No newline at end of file diff --git a/uni_modules/uni-chooseSystemImage/utssdk/app-android/AndroidManifest.xml b/uni_modules/uni-chooseSystemImage/utssdk/app-android/AndroidManifest.xml new file mode 100644 index 0000000..3fb9e7f --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/utssdk/app-android/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/uni_modules/uni-chooseSystemImage/utssdk/app-android/ChooseSystemImageActivity.kt b/uni_modules/uni-chooseSystemImage/utssdk/app-android/ChooseSystemImageActivity.kt new file mode 100644 index 0000000..94cbc27 --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/utssdk/app-android/ChooseSystemImageActivity.kt @@ -0,0 +1,143 @@ +package uts.sdk.modules.uniChooseSystemImage + +import android.app.Activity +import android.content.Intent +import android.content.pm.ActivityInfo +import android.graphics.Color +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.MediaStore +import android.view.WindowManager +import android.widget.LinearLayout +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType +import androidx.fragment.app.FragmentActivity +import java.util.Locale + + +class ChooseSystemImageActivity : FragmentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStatusBarTransparent(this) + val layout = LinearLayout(this) + layout.setBackgroundColor(Color.TRANSPARENT) + setContentView(layout) + if (intent.hasExtra("page_orientation")) { + requestedOrientation = + intent.getIntExtra("page_orientation", ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + } + val count = intent.getIntExtra("count", 9) + val type = intent.getIntExtra("type", 1) + val mediaType: VisualMediaType = when (type) { + 1 -> { + ActivityResultContracts.PickVisualMedia.ImageOnly + } + + 2 -> { + ActivityResultContracts.PickVisualMedia.VideoOnly + } + + 3 -> { + ActivityResultContracts.PickVisualMedia.ImageAndVideo + } + + else -> { + ActivityResultContracts.PickVisualMedia.ImageOnly + } + } + val pickMultipleMedia = if (count == 1) { + this.registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + val intent = Intent() + if (uri != null) { + this.contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + var path = uri.toString() + val mediaT = this.contentResolver.getType(uri)?.lowercase(Locale.ENGLISH) + val m = Media( + if (mediaT?.startsWith("video/") == true) { + 2 + } else if (mediaT?.startsWith("image/") == true) { + 1 + } else { + 0 + }, path + ) + intent.putExtra("paths", arrayOf(m)) + this.setResult(RESULT_OK, intent) + this.finish() + } else { + this.setResult(RESULT_OK, intent) + this.finish() + } + } + } else + this.registerForActivityResult( + ActivityResultContracts.PickMultipleVisualMedia(count) + ) { result -> + val paths = mutableListOf() + for (uri in result) { + this.contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + var path = uri.toString() + val mediaT = this.contentResolver.getType(uri)?.lowercase(Locale.ENGLISH) + val m = Media( + if (mediaT?.startsWith("video/") == true) { + 2 + } else if (mediaT?.startsWith("image/") == true) { + 1 + } else { + 0 + }, path + ) + paths.add(m) + } + val intent = Intent() + intent.putExtra("paths", paths.toTypedArray()) + this.setResult(RESULT_OK, intent) + this.finish() + } + pickMultipleMedia.launch( + PickVisualMediaRequest.Builder() + .setMediaType(mediaType) + .build() + ) + } + + private fun getFilePathFromUri(uri: Uri): String? { + var filePath: String? = null + if (uri.scheme == "file") { + filePath = uri.path + } else if (uri.scheme == "content") { + val contentResolver = contentResolver + val cursor = + contentResolver.query(uri, arrayOf(MediaStore.Images.Media.DATA), null, null, null) + if (cursor != null && cursor.moveToFirst()) { + val columnIndex = cursor.getColumnIndex("_data") + filePath = cursor.getString(columnIndex) + cursor.close() + } + } + return filePath + } + + private fun setStatusBarTransparent(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + val window = activity.window + // 设置透明状态栏标志 + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.clearFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION + ) + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + window.statusBarColor = Color.TRANSPARENT + } + } +} \ No newline at end of file diff --git a/uni_modules/uni-chooseSystemImage/utssdk/app-android/FileUtils.kt b/uni_modules/uni-chooseSystemImage/utssdk/app-android/FileUtils.kt new file mode 100644 index 0000000..2200d7a --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/utssdk/app-android/FileUtils.kt @@ -0,0 +1,210 @@ +package uts.sdk.modules.uniChooseSystemImage + +import android.content.ContentResolver +import android.content.ContentUris +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import android.webkit.MimeTypeMap +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.net.URLConnection +import java.security.MessageDigest + + +object FileUtils { + fun getFilePathByUri(context: Context, uri: Uri): String? { + var path: String? = null + // 以 file:// 开头的 + if (ContentResolver.SCHEME_FILE == uri.scheme) { + path = uri.path + return path + } + // 以 content:// 开头的,比如 content://media/extenral/images/media/17766 + if (ContentResolver.SCHEME_CONTENT == uri.scheme && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + val cursor = context.contentResolver.query( + uri, + arrayOf(MediaStore.Images.Media.DATA), + null, + null, + null + ) + if (cursor != null) { + if (cursor.moveToFirst()) { + val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) + if (columnIndex > -1) { + path = cursor.getString(columnIndex) + } + } + cursor.close() + } + return path + } + // 4.4及之后的 是以 content:// 开头的,比如 content://com.android.providers.media.documents/document/image%3A235700 + if (ContentResolver.SCHEME_CONTENT == uri.scheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (DocumentsContract.isDocumentUri(context, uri)) { + if (isExternalStorageDocument(uri)) { + // ExternalStorageProvider + val docId = DocumentsContract.getDocumentId(uri) + val split = + docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + path = Environment.getExternalStorageDirectory().toString() + "/" + split[1] + return path + } + } else if (isDownloadsDocument(uri)) { + // DownloadsProvider + val id = DocumentsContract.getDocumentId(uri) + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + id.toLong() + ) + path = getDataColumn(context, contentUri, null, null) + return path + } else if (isMediaDocument(uri)) { + // MediaProvider + val docId = DocumentsContract.getDocumentId(uri) + val split = + docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val type = split[0] + var contentUri: Uri? = null + if ("image" == type) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } else if ("video" == type) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } else if ("audio" == type) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + path = getDataColumn(context, contentUri, selection, selectionArgs) + return path + } + } + } + return null + } + + // 新增:将 uri 拷贝到传入的父文件夹,文件名为 uri 的 MD5,后缀从头信息或原文件名推断。 + // 若目标文件已存在则直接返回已存在路径,不重复拷贝。 + // 返回目标文件的绝对路径,失败返回 null。 + fun copyUriToDir(context: Context, parentDirStr: String, uriString: String): String? { + try { + var uri = Uri.parse(uriString) + var parentDir = File(parentDirStr) + val resolver = context.contentResolver + // 读取全部数据到内存(用于判断 MIME 并写入目标文件) + val inputStream = resolver.openInputStream(uri) ?: return null + val baos = ByteArrayOutputStream() + inputStream.use { ins -> + val buf = ByteArray(8 * 1024) + var len: Int + while (ins.read(buf).also { len = it } != -1) { + baos.write(buf, 0, len) + } + } + val data = baos.toByteArray() + // 通过头信息猜 MIME + var mime: String? = null + try { + mime = URLConnection.guessContentTypeFromStream(ByteArrayInputStream(data)) + } catch (_: Exception) { + } + if (mime == null) { + try { + mime = resolver.getType(uri) + } catch (_: Exception) { + } + } + // 若仍为空,尝试从原始路径推断 + var originalPath: String? = null + try { + originalPath = getFilePathByUri(context, uri) + } catch (_: Exception) { + } + // 根据 mime 获取扩展名 + var ext: String? = null + if (mime != null) { + ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) + } + // 如果通过 mime 无法得到扩展名,尝试从原始路径取后缀 + if (ext.isNullOrEmpty() && !originalPath.isNullOrEmpty()) { + val idx = originalPath.lastIndexOf('.') + if (idx != -1 && idx + 1 < originalPath.length) { + ext = originalPath.substring(idx + 1).lowercase() + } + } + val extSuffix = if (!ext.isNullOrEmpty()) ".${ext}" else "" + // 计算 MD5 作为文件名(基于 uri.toString()) + val name = md5(uri.toString()) + extSuffix + // 确保父目录存在 + if (!parentDir.exists()) { + parentDir.mkdirs() + } + val destFile = File(parentDir, name) + // 若已存在,直接返回 + if (destFile.exists()) { + return destFile.absolutePath + } + // 写入文件 + FileOutputStream(destFile).use { fos -> + fos.write(data) + fos.flush() + } + return destFile.absolutePath + } catch (e: Exception) { + // 出错返回 null + return null + } + } + + // 辅助:计算字符串的 MD5(小写 hex) + private fun md5(input: String): String { + val md = MessageDigest.getInstance("MD5") + val bytes = md.digest(input.toByteArray(Charsets.UTF_8)) + return bytes.joinToString("") { "%02x".format(it) } + } + + private fun getDataColumn( + context: Context, + uri: Uri?, + selection: String?, + selectionArgs: Array?, + ): String? { + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf(column) + try { + cursor = + context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) + if (cursor != null && cursor.moveToFirst()) { + val column_index = cursor.getColumnIndexOrThrow(column) + return cursor.getString(column_index) + } + } finally { + cursor?.close() + } + return null + } + + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + + +} \ No newline at end of file diff --git a/uni_modules/uni-chooseSystemImage/utssdk/app-android/Media.kt b/uni_modules/uni-chooseSystemImage/utssdk/app-android/Media.kt new file mode 100644 index 0000000..4f1c463 --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/utssdk/app-android/Media.kt @@ -0,0 +1,42 @@ +package uts.sdk.modules.uniChooseSystemImage + +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.Creator + +class Media : Parcelable { + var type: Int + var path: String? + + constructor(type: Int, path: String?) { + this.type = type + this.path = path + } + + protected constructor(`in`: Parcel) { + type = `in`.readInt() + path = `in`.readString() + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeInt(type) + dest.writeString(path) + } + + companion object { + @JvmField + val CREATOR: Creator = object : Creator { + override fun createFromParcel(`in`: Parcel): Media { + return Media(`in`) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/uni_modules/uni-chooseSystemImage/utssdk/app-android/config.json b/uni_modules/uni-chooseSystemImage/utssdk/app-android/config.json new file mode 100644 index 0000000..a9b89eb --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/utssdk/app-android/config.json @@ -0,0 +1,7 @@ +{ + "dependencies": [ + "androidx.appcompat:appcompat:1.6.1", + "androidx.activity:activity-ktx:1.9.2" + ], + "minSdkVersion": "21" +} \ No newline at end of file diff --git a/uni_modules/uni-chooseSystemImage/utssdk/app-android/index.uts b/uni_modules/uni-chooseSystemImage/utssdk/app-android/index.uts new file mode 100644 index 0000000..69890d3 --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/utssdk/app-android/index.uts @@ -0,0 +1,260 @@ +/* 引入 interface.uts 文件中定义的变量 */ +import { ChooseSystemImage, ChooseSystemImageOptions, ChooseSystemImageSuccessResult, ChooseSystemMedia, ChooseSystemMediaOptions, ChooseSystemMediaSuccessResult, ChooseSystemVideo, ChooseSystemVideoOptions, ChooseSystemVideoSuccessResult } from '../interface.uts'; +import AppCompatActivity from 'androidx.appcompat.app.AppCompatActivity'; +import ActivityResultCallback from 'androidx.activity.result.ActivityResultCallback'; +import List from 'kotlin.collections.List'; +import Uri from 'android.net.Uri'; +import ActivityResultContracts from 'androidx.activity.result.contract.ActivityResultContracts'; +import ActivityResultLauncher from 'androidx.activity.result.ActivityResultLauncher'; +import PickVisualMediaRequest from "androidx.activity.result.PickVisualMediaRequest"; +import Builder from "androidx.activity.result.PickVisualMediaRequest.Builder"; +import Context from 'com.alibaba.fastjson.parser.deserializer.ASMDeserializerFactory.Context'; +import MediaStore from 'android.provider.MediaStore'; +import Activity from "android.app.Activity" +import Intent from 'android.content.Intent'; +import ChooseSystemImageActivity from "uts.sdk.modules.uniChooseSystemImage.ChooseSystemImageActivity" + +/* 引入 unierror.uts 文件中定义的变量 */ +import { ImageErrorImpl } from '../unierror'; +import ChooseVideoOptions from 'uts.sdk.modules.DCloudUniMedia.ChooseVideoOptions'; +import BitmapFactory from 'android.graphics.BitmapFactory'; +import File from 'java.io.File'; +import FileInputStream from 'java.io.FileInputStream'; +import FileOutputStream from 'java.io.FileOutputStream'; +import InputStream from 'java.io.InputStream'; +import Build from 'android.os.Build'; +import Parcelable from 'android.os.Parcelable'; +import Media from 'uts.sdk.modules.uniChooseSystemImage.Media'; +import FileUtils from "uts.sdk.modules.uniChooseSystemImage.FileUtils" + +var resultCallback : ((requestCode : Int, resultCode : Int, data ?: Intent) => void) | null = null + +export const chooseSystemImage : ChooseSystemImage = function (option : ChooseSystemImageOptions) { + if (option.count <= 0) { + var error = new ImageErrorImpl(2101002, "uni-chooseSystemImage") + option.fail?.(error) + option.complete?.(error) + return + } + if (Build.VERSION.SDK_INT > 32 || UTSAndroid.getUniActivity()!.applicationInfo.targetSdkVersion >= 33) { + __chooseSystemImage(option) + } else { + UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, [android.Manifest.permission.READ_EXTERNAL_STORAGE], (a : boolean, b : string[]) => { + __chooseSystemImage(option) + }, (a : boolean, b : string[]) => { + var error = new ImageErrorImpl(2101005, "uni-chooseSystemImage") + option.fail?.(error) + option.complete?.(error) + }) + } +} + +export const chooseSystemMedia : ChooseSystemMedia = function (option : ChooseSystemMediaOptions) { + if (option.count <= 0) { + var error = new ImageErrorImpl(2101002, "uni-chooseSystemMedia") + option.fail?.(error) + option.complete?.(error) + return + } + if (option.count > 100) { + option.count = 100 + } + if (Build.VERSION.SDK_INT > 32 || UTSAndroid.getUniActivity()!.applicationInfo.targetSdkVersion >= 33) { + __chooseSystemMedia(option) + } else { + UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, [android.Manifest.permission.READ_EXTERNAL_STORAGE], (a : boolean, b : string[]) => { + __chooseSystemMedia(option) + }, (a : boolean, b : string[]) => { + var error = new ImageErrorImpl(2101005, "uni-chooseSystemMedia") + option.fail?.(error) + option.complete?.(error) + }) + } +} + +function __chooseSystemMedia(option : ChooseSystemMediaOptions) { + try { + resultCallback = (requestCode : Int, resultCode : Int, data : Intent | null) => { + UTSAndroid.offAppActivityResult(resultCallback!) + if (10086 == requestCode && resultCode == -1) { + if (data != null) { + var result = data!.getParcelableArrayExtra("paths") + if (result != null && result!.size > 0) { + var paths : Array = [] + result.forEach((p : Parcelable) => { + if (p instanceof Media) + if (UTSAndroid.isUniAppX()) { + paths.push((p.path!)) + } else { + paths.push("file://" + copyResource(p.path!)) + } + }) + var success : ChooseSystemMediaSuccessResult = { + filePaths: paths + } + option.success?.(success) + option.complete?.(success) + } else { + var error = new ImageErrorImpl(2101001, "uni-chooseSystemMedia") + option.fail?.(error) + option.complete?.(error) + } + } else { + var error = new ImageErrorImpl(2101001, "uni-chooseSystemMedia") + option.fail?.(error) + option.complete?.(error) + } + } else { + var error = new ImageErrorImpl(2101001, "uni-chooseSystemMedia") + option.fail?.(error) + option.complete?.(error) + } + } + UTSAndroid.onAppActivityResult(resultCallback!) + var intent = new Intent(UTSAndroid.getUniActivity()!, Class.forName("uts.sdk.modules.uniChooseSystemImage.ChooseSystemImageActivity")) + intent.putExtra("count", option.count) + if (option.mediaType != null) { + if (option.mediaType!.indexOf("mix") >= 0) { + intent.putExtra("type", 3) + } else if (option.mediaType!.indexOf("image") >= 0) { + intent.putExtra("type", 1) + } else if (option.mediaType!.indexOf("video") >= 0) { + intent.putExtra("type", 2) + } else { + intent.putExtra("type", 1) + } + } + switch (option.pageOrientation) { + case "auto": { + intent.putExtra("page_orientation", 2) + break + } + case "portrait": { + intent.putExtra("page_orientation", 1) + break + } + case "landscape": { + intent.putExtra("page_orientation", 0) + break + } + default: { + intent.putExtra("page_orientation", 1) + break + } + } + UTSAndroid.getUniActivity()!.startActivityForResult(intent, 10086) + } catch (e) { + var error = new ImageErrorImpl(2101010, "uni-chooseSystemMedia") + option.fail?.(error) + option.complete?.(error) + } + +} + +function __chooseSystemImage(option : ChooseSystemImageOptions) { + try { + resultCallback = (requestCode : Int, resultCode : Int, data : Intent | null) => { + UTSAndroid.offAppActivityResult(resultCallback!) + if (10086 == requestCode && resultCode == -1) { + if (data != null) { + var result = data!.getParcelableArrayExtra("paths") + if (result != null && result!.size > 0) { + var paths : Array = [] + result.forEach((p : Parcelable) => { + if (p instanceof Media) + if (UTSAndroid.isUniAppX()) { + paths.push((p.path!)) + } else { + paths.push("file://" + copyResource(p.path!)) + } + }) + var success : ChooseSystemImageSuccessResult = { + filePaths: paths + } + option.success?.(success) + option.complete?.(success) + } else { + var error = new ImageErrorImpl(2101001, "uni-chooseSystemImage") + option.fail?.(error) + option.complete?.(error) + } + } else { + var error = new ImageErrorImpl(2101001, "uni-chooseSystemImage") + option.fail?.(error) + option.complete?.(error) + } + } else { + var error = new ImageErrorImpl(2101001, "uni-chooseSystemImage") + option.fail?.(error) + option.complete?.(error) + } + } + UTSAndroid.onAppActivityResult(resultCallback!) + var intent = new Intent(UTSAndroid.getUniActivity()!, Class.forName("uts.sdk.modules.uniChooseSystemImage.ChooseSystemImageActivity")) + intent.putExtra("count", option.count) + intent.putExtra("type", 1) + UTSAndroid.getUniActivity()!.startActivityForResult(intent, 10086) + } catch (e) { + var error = new ImageErrorImpl(2101010, "uni-chooseSystemImage") + option.fail?.(error) + option.complete?.(error) + } +} + +var CACHEPATH = UTSAndroid.getAppCachePath() +function copyResource(url : string) : string { + var path : String = CACHEPATH! + if (CACHEPATH?.endsWith("/") == true) { + path = CACHEPATH + "uni-getSystemMedia/" + } else { + path = CACHEPATH + "/uni-getSystemMedia/" + } + console.log(url) + var result = FileUtils.copyUriToDir(UTSAndroid.getAppContext()!,path,url) + // path = path + new File(url).getName() + // copyFile(url, path) + return result! +} + +function copyFile(fromFilePath : string, toFilePath : string) : boolean { + var fis : InputStream | null = null + try { + let fromFile = new File(fromFilePath) + if (!fromFile.exists()) { + return false; + } + if (!fromFile.isFile()) { + return false + } + if (!fromFile.canRead()) { + return false; + } + fis = new FileInputStream(fromFile); + if (fis == null) { + return false + } + } catch (e) { + return false; + } + let toFile = new File(toFilePath) + if (!toFile.getParentFile().exists()) { + toFile.getParentFile().mkdirs() + } + if (!toFile.exists()) { + toFile.createNewFile() + } + try { + let fos = new FileOutputStream(toFile) + let byteArrays = ByteArray(1024) + var c = fis!!.read(byteArrays) + while (c > 0) { + fos.write(byteArrays, 0, c) + c = fis!!.read(byteArrays) + } + fis!!.close() + fos.close() + return true + } catch (e) { + return false; + } +} \ No newline at end of file diff --git a/uni_modules/uni-chooseSystemImage/utssdk/interface.uts b/uni_modules/uni-chooseSystemImage/utssdk/interface.uts new file mode 100644 index 0000000..8aa962b --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/utssdk/interface.uts @@ -0,0 +1,38 @@ +export type ChooseSystemImageSuccessResult = { + filePaths : Array +} +export type ImageErrorCode = 2101001 | 2101010 | 2101002 | 2101005 +export interface ChooseSystemImageError extends IUniError { + errCode : ImageErrorCode +}; +export type ChooseSystemImageSuccessCallback = (result : ChooseSystemImageSuccessResult) => void +export type ChooseSystemImageFailResult = ChooseSystemImageError +export type ChooseSystemImageFailCallback = (result : ChooseSystemImageFailResult) => void +export type ChooseSystemImageCompleteCallback = (callback : any) => void + +export type ChooseSystemImageOptions = { + count : number, + success ?: ChooseSystemImageSuccessCallback | null, + fail ?: ChooseSystemImageFailCallback | null, + complete ?: ChooseSystemImageCompleteCallback | null +} + +export type ChooseSystemImage = (options : ChooseSystemImageOptions) => void + +export type ChooseSystemMediaSuccessResult = { + filePaths : Array +} +export type ChooseSystemMediaSuccessCallback = (result : ChooseSystemMediaSuccessResult) => void +export type ChooseSystemMediaFailResult = ChooseSystemImageError +export type ChooseSystemMediaFailCallback = (result : ChooseSystemMediaFailResult) => void +export type ChooseSystemMediaCompleteCallback = (callback : any) => void +export type ChooseSystemMediaOptions = { + count : number, + mediaType ?: Array | null, + pageOrientation ?: string | null, + success ?: ChooseSystemMediaSuccessCallback | null, + fail ?: ChooseSystemMediaFailCallback | null, + complete ?: ChooseSystemMediaCompleteCallback | null +} + +export type ChooseSystemMedia = (options : ChooseSystemMediaOptions) => void diff --git a/uni_modules/uni-chooseSystemImage/utssdk/unierror.uts b/uni_modules/uni-chooseSystemImage/utssdk/unierror.uts new file mode 100644 index 0000000..ea56c5d --- /dev/null +++ b/uni_modules/uni-chooseSystemImage/utssdk/unierror.uts @@ -0,0 +1,25 @@ +import { ImageErrorCode, ChooseSystemImageError } from "./interface.uts" +export const ImageUniErrors : Map = new Map([ + /** + * 用户取消 + */ + [2101001, 'user cancel'], + [2101002, 'fail parameter error'], + [2101005, "No Permission"], + /** + * 其他错误 + */ + [2101010, "unexpect error:"] +]); + +export class ImageErrorImpl extends UniError implements ChooseSystemImageError { + // #ifdef APP-ANDROID + override errCode : ImageErrorCode + // #endif + constructor(errCode : ImageErrorCode, uniErrorSubject : string) { + super() + this.errSubject = uniErrorSubject + this.errCode = errCode + this.errMsg = ImageUniErrors.get(errCode) ?? ""; + } +} \ No newline at end of file diff --git a/uni_modules/wot-design-uni/components/composables/useUpload.ts b/uni_modules/wot-design-uni/components/composables/useUpload.ts index 87cf54b..86d569d 100644 --- a/uni_modules/wot-design-uni/components/composables/useUpload.ts +++ b/uni_modules/wot-design-uni/components/composables/useUpload.ts @@ -1,5 +1,6 @@ import { isArray, isDef, isFunction } from '../common/util' import type { ChooseFile, ChooseFileOption, UploadFileItem, UploadMethod, UploadStatusType } from '../wd-upload/types' +import { chooseSystemMedia } from "@/uni_modules/uni-chooseSystemImage" export const UPLOAD_STATUS: Record = { PENDING: 'pending', @@ -244,15 +245,26 @@ export function useUpload(): UseUploadReturn { fail: reject }) // #endif - // #ifndef MP-WEIXIN - uni.chooseImage({ + // #ifdef H5 + uni.chooseImage({ + count: multiple ? maxCount : 1, + sizeType, + sourceType, + extension, + success: (res) => resolve(formatImage(res)), + fail: reject + }) + // #endif + // #ifdef APP-PLUS + chooseSystemMedia({ count: multiple ? maxCount : 1, - sizeType, - sourceType, - // #ifdef H5 - extension, - // #endif - success: (res) => resolve(formatImage(res)), + mediaType: ['image'], + success: (res) => { + const tempFiles = res.filePaths.map((item: any) => ({ + path: item + })) + resolve(formatImage({ tempFiles })) + }, fail: reject }) // #endif @@ -269,19 +281,30 @@ export function useUpload(): UseUploadReturn { fail: reject }) // #endif - // #ifndef MP-WEIXIN + // #ifdef H5 uni.chooseVideo({ sourceType, compressed, maxDuration, camera, - // #ifdef H5 extension, - // #endif success: (res) => resolve(formatVideo(res)), fail: reject }) // #endif + // #ifdef APP-PLUS + chooseSystemMedia({ + count: multiple ? maxCount : 1, + mediaType: ['video'], + success: (res) => { + const tempFiles = res.filePaths.map((item: any) => ({ + path: item + })) + resolve(formatImage({ tempFiles })) + }, + fail: reject + }) + // #endif break // #ifdef MP-WEIXIN case 'media': @@ -324,7 +347,6 @@ export function useUpload(): UseUploadReturn { fail: reject }) // #endif - break default: // #ifdef MP-WEIXIN @@ -338,14 +360,20 @@ export function useUpload(): UseUploadReturn { fail: reject }) // #endif - // #ifndef MP-WEIXIN + // #ifdef H5 uni.chooseImage({ count: multiple ? maxCount : 1, sizeType, sourceType, - // #ifdef H5 extension, - // #endif + success: (res) => resolve(formatImage(res)), + fail: reject + }) + // #endif + // #ifdef APP-PLUS + chooseSystemMedia({ + count: multiple ? maxCount : 1, + mediaType: ['image'], success: (res) => resolve(formatImage(res)), fail: reject })