From 1b9d2b6d8dccde52897ed9082e82eff1f3ed0b03 Mon Sep 17 00:00:00 2001 From: iSmartCoding Date: Sun, 19 May 2024 10:10:29 +0800 Subject: [PATCH] Enhance image previewer --- app/build.gradle.kts | 4 +- .../ismartcoding/plain/helpers/VideoHelper.kt | 22 ++++--- .../plain/ui/base/PMiniOutlineButton.kt | 10 ++-- .../plain/ui/base/coil/ImageLoader.kt | 2 +- .../ui/base/markdowntext/CoilImagesPlugin.kt | 23 ++------ .../plain/ui/components/CastDialog.kt | 7 +-- .../plain/ui/components/ChatListItem.kt | 6 +- .../plain/ui/components/ImageGridItem.kt | 21 +++++++ .../plain/ui/components/chat/ChatFiles.kt | 11 +--- .../plain/ui/components/chat/ChatImages.kt | 6 +- .../ui/components/mediaviewer/MediaGallery.kt | 3 +- .../mediaviewer/MediaNormalImage.kt | 3 +- .../ui/components/mediaviewer/MediaViewer.kt | 9 ++- .../previewer/ImagePreviewActions.kt | 58 +++++++++++++------ .../mediaviewer/previewer/MediaPreviewer.kt | 10 ++-- .../previewer/MediaViewerContainer.kt | 6 -- .../previewer/PreviewerTransformState.kt | 43 ++++---------- .../plain/ui/extensions/TextView.kt | 57 +++++++++++++----- .../plain/ui/models/CastViewModel.kt | 5 ++ .../plain/ui/models/FeedsViewModel.kt | 5 +- .../plain/ui/models/MediaPreviewData.kt | 16 ++++- .../plain/ui/preview/MediaItem.kt | 4 -- .../com/ismartcoding/plain/web/HttpModule.kt | 26 +++++++-- app/src/main/res/values-bn/strings.xml | 4 ++ app/src/main/res/values-de/strings.xml | 4 ++ app/src/main/res/values-es/strings.xml | 4 ++ app/src/main/res/values-fr/strings.xml | 4 ++ app/src/main/res/values-hi/strings.xml | 4 ++ app/src/main/res/values-it/strings.xml | 4 ++ app/src/main/res/values-ja/strings.xml | 4 ++ app/src/main/res/values-ko/strings.xml | 4 ++ app/src/main/res/values-nl/strings.xml | 4 ++ app/src/main/res/values-pt/strings.xml | 4 ++ app/src/main/res/values-ru/strings.xml | 4 ++ app/src/main/res/values-ta/strings.xml | 4 ++ app/src/main/res/values-tr/strings.xml | 4 ++ app/src/main/res/values-vi/strings.xml | 4 ++ app/src/main/res/values-zh-rCN/strings.xml | 4 ++ app/src/main/res/values-zh-rTW/strings.xml | 4 ++ app/src/main/res/values/strings.xml | 1 + .../com/ismartcoding/lib/extensions/String.kt | 11 ++++ .../lib/helpers/ValidateHelper.kt | 10 ---- .../ismartcoding/lib/upnp/UPnPController.kt | 38 +++++++++++- .../com/ismartcoding/lib/upnp/UPnPDevice.kt | 2 +- 44 files changed, 319 insertions(+), 164 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a3586b93..fb89d4d6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,9 +37,9 @@ android { else -> 0 } - val vCode = 283 + val vCode = 286 versionCode = vCode - singleAbiNum - versionName = "1.2.50" + versionName = "1.2.51" ndk { //noinspection ChromeOsAbiSupport diff --git a/app/src/main/java/com/ismartcoding/plain/helpers/VideoHelper.kt b/app/src/main/java/com/ismartcoding/plain/helpers/VideoHelper.kt index c8e950a6..8d61f1cf 100644 --- a/app/src/main/java/com/ismartcoding/plain/helpers/VideoHelper.kt +++ b/app/src/main/java/com/ismartcoding/plain/helpers/VideoHelper.kt @@ -4,16 +4,24 @@ import android.content.Context import android.media.MediaMetadataRetriever import android.net.Uri import androidx.compose.ui.unit.IntSize +import com.ismartcoding.lib.logcat.LogCat import java.io.File object VideoHelper { fun getIntrinsicSize(context: Context, path: String): IntSize { - val file = File(path) - val retriever = MediaMetadataRetriever() - retriever.setDataSource(context, Uri.fromFile(file)) - val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 0 - val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: 0 - retriever.release() - return IntSize(width, height) + try { + val file = File(path) + val retriever = MediaMetadataRetriever() + retriever.setDataSource(context, Uri.fromFile(file)) + val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 0 + val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: 0 + retriever.release() + return IntSize(width, height) + } catch (ex: Exception) { + LogCat.e(ex.toString()) + ex.printStackTrace() + } + + return IntSize.Zero } } \ No newline at end of file diff --git a/app/src/main/java/com/ismartcoding/plain/ui/base/PMiniOutlineButton.kt b/app/src/main/java/com/ismartcoding/plain/ui/base/PMiniOutlineButton.kt index 541f23b8..962b07f2 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/base/PMiniOutlineButton.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/base/PMiniOutlineButton.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -13,11 +14,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp - @Composable fun PMiniOutlineButton( text: String, modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.primary, onClick: () -> Unit, ) { Button( @@ -27,12 +28,11 @@ fun PMiniOutlineButton( .height(32.dp), contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp), shape = RoundedCornerShape(8.dp), - colors = - ButtonDefaults.buttonColors( + colors = ButtonDefaults.buttonColors( containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.primary, + contentColor = color, ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), + border = BorderStroke(1.dp, color), ) { Text(text, style = MaterialTheme.typography.labelSmall) } diff --git a/app/src/main/java/com/ismartcoding/plain/ui/base/coil/ImageLoader.kt b/app/src/main/java/com/ismartcoding/plain/ui/base/coil/ImageLoader.kt index c9e2712c..ac35ea2f 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/base/coil/ImageLoader.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/base/coil/ImageLoader.kt @@ -16,7 +16,7 @@ fun newImageLoader(context: PlatformContext): ImageLoader { val memoryPercent = if (activityManager.isLowRamDevice) 0.25 else 0.75 return ImageLoader.Builder(context) .components { - add(SvgDecoder.Factory(false)) + add(SvgDecoder.Factory(true)) add(AnimatedImageDecoder.Factory()) add(ThumbnailDecoder.Factory()) } diff --git a/app/src/main/java/com/ismartcoding/plain/ui/base/markdowntext/CoilImagesPlugin.kt b/app/src/main/java/com/ismartcoding/plain/ui/base/markdowntext/CoilImagesPlugin.kt index a63ab6cf..654c858a 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/base/markdowntext/CoilImagesPlugin.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/base/markdowntext/CoilImagesPlugin.kt @@ -48,8 +48,8 @@ class CoilImagesPlugin internal constructor(coilStore: CoilStore, imageLoader: I override fun load(drawable: AsyncDrawable) { val loaded = AtomicBoolean(false) - val target: Target = AsyncDrawableTarget(drawable, loaded) - val request: ImageRequest = coilStore.load(drawable).newBuilder() + val target = AsyncDrawableTarget(drawable, loaded) + val request = coilStore.load(drawable).newBuilder() .target(target) .build() // @since 4.5.1 execute can return result _before_ disposable is created, @@ -75,15 +75,7 @@ class CoilImagesPlugin internal constructor(coilStore: CoilStore, imageLoader: I return null } - private inner class AsyncDrawableTarget(drawable: AsyncDrawable, loaded: AtomicBoolean) : Target { - private val drawable: AsyncDrawable - private val loaded: AtomicBoolean - - init { - this.drawable = drawable - this.loaded = loaded - } - + private inner class AsyncDrawableTarget(val drawable: AsyncDrawable,val loaded: AtomicBoolean) : Target { fun onSuccess(loadedDrawable: Drawable) { // @since 4.5.1 check finished flag (result can be delivered _before_ disposable is created) if (cache.remove(drawable) != null @@ -121,7 +113,7 @@ class CoilImagesPlugin internal constructor(coilStore: CoilStore, imageLoader: I context: Context, imageLoader: ImageLoader ): CoilImagesPlugin { - return create(object : CoilStore { + return CoilImagesPlugin(object : CoilStore { override fun load(drawable: AsyncDrawable): ImageRequest { return ImageRequest.Builder(context) .data(drawable.destination) @@ -133,12 +125,5 @@ class CoilImagesPlugin internal constructor(coilStore: CoilStore, imageLoader: I } }, imageLoader) } - - fun create( - coilStore: CoilStore, - imageLoader: ImageLoader - ): CoilImagesPlugin { - return CoilImagesPlugin(coilStore, imageLoader) - } } } \ No newline at end of file diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/CastDialog.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/CastDialog.kt index 73bc67f8..9b434191 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/CastDialog.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/CastDialog.kt @@ -22,9 +22,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.ismartcoding.lib.channel.sendEvent -import com.ismartcoding.lib.helpers.CoroutinesHelper -import com.ismartcoding.lib.helpers.CoroutinesHelper.coIO -import com.ismartcoding.lib.helpers.CoroutinesHelper.withIO import com.ismartcoding.plain.R import com.ismartcoding.plain.TempData import com.ismartcoding.plain.features.StartHttpServerEvent @@ -53,7 +50,7 @@ fun CastDialog(viewModel: CastViewModel) { scope.launch(Dispatchers.IO) { viewModel.searchAsync(context) } - coIO { + scope.launch(Dispatchers.IO) { delay(5000) if (itemsState.isEmpty()) { loadingTextId = R.string.no_devices_found @@ -77,7 +74,7 @@ fun CastDialog(viewModel: CastViewModel) { viewModel.selectDevice(m) viewModel.enterCastMode() scope.launch(Dispatchers.IO) { - if(!TempData.webEnabled) { + if (!TempData.webEnabled) { WebPreference.putAsync(context, true) sendEvent(StartHttpServerEvent()) } diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/ChatListItem.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/ChatListItem.kt index 880131a7..feebc890 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/ChatListItem.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/ChatListItem.kt @@ -108,11 +108,11 @@ fun ChatListItem( ) { when (m.type) { DMessageType.IMAGES.value -> { - ChatImages(context, m, imageWidthDp, previewerState) + ChatImages(context, items, m, imageWidthDp, previewerState) } DMessageType.FILES.value -> { - ChatFiles(context, navController, m, previewerState) + ChatFiles(context, items, navController, m, previewerState) } DMessageType.TEXT.value -> { @@ -140,7 +140,7 @@ fun ChatListItem( onDismissRequest = { viewModel.selectedItem.value = null showContextMenu.value = false - }, + }, ) { PDropdownMenuItem( text = { Text(stringResource(id = R.string.select)) }, diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/ImageGridItem.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/ImageGridItem.kt index 7858fe7e..605adf25 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/ImageGridItem.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/ImageGridItem.kt @@ -8,8 +8,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.PlayCircleOutline import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -116,6 +120,23 @@ fun ImageGridItem( .background(MaterialTheme.colorScheme.lightMask()) .aspectRatio(1f) ) + } else if (castViewModel.castMode.value) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.darkMask()) + .aspectRatio(1f) + ) { + Icon( + modifier = + Modifier + .align(Alignment.Center) + .size(48.dp), + imageVector = Icons.Outlined.PlayCircleOutline, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } } if (inSelectionMode) { diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/chat/ChatFiles.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/chat/ChatFiles.kt index b5858c1c..31ee707e 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/chat/ChatFiles.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/chat/ChatFiles.kt @@ -1,7 +1,6 @@ package com.ismartcoding.plain.ui.components.chat import android.content.Context -import androidx.activity.ComponentActivity import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -16,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -55,12 +53,12 @@ import java.io.File @Composable fun ChatFiles( context: Context, + items: List, navController: NavHostController, m: VChat, previewerState: MediaPreviewerState, ) { val fileItems = (m.value as DMessageFiles).items - val context = LocalContext.current as ComponentActivity val keyboardController = LocalSoftwareKeyboardController.current Column { fileItems.forEachIndexed { index, item -> @@ -74,12 +72,7 @@ fun ChatFiles( if (path.isImageFast() || path.isVideoFast()) { coMain { keyboardController?.hide() - withIO { - MediaPreviewData.setDataAsync( - context, itemState, fileItems - .filter { it.uri.isVideoFast() || it.uri.isImageFast() }, item - ) - } + withIO { MediaPreviewData.setDataAsync(context, itemState, items.reversed(), item) } previewerState.openTransform( index = MediaPreviewData.items.indexOfFirst { it.id == item.id }, itemState = itemState, diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/chat/ChatImages.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/chat/ChatImages.kt index 2dfaf53d..7d16d717 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/chat/ChatImages.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/chat/ChatImages.kt @@ -1,7 +1,6 @@ package com.ismartcoding.plain.ui.components.chat import android.content.Context -import androidx.activity.ComponentActivity import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -19,7 +18,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp @@ -39,12 +37,12 @@ import com.ismartcoding.plain.ui.models.VChat @Composable fun ChatImages( context: Context, + items: List, m: VChat, imageWidthDp: Dp, previewerState: MediaPreviewerState, ) { val imageItems = (m.value as DMessageImages).items - val context = LocalContext.current as ComponentActivity val keyboardController = LocalSoftwareKeyboardController.current FlowRow( @@ -63,7 +61,7 @@ fun ChatImages( Modifier.clickable { coMain { keyboardController?.hide() - withIO { MediaPreviewData.setDataAsync(context, itemState, imageItems, item) } + withIO { MediaPreviewData.setDataAsync(context, itemState, items.reversed(), item) } previewerState.openTransform( index = MediaPreviewData.items.indexOfFirst { it.id == item.id }, itemState = itemState, diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaGallery.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaGallery.kt index 7f4a57ad..ede35f9a 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaGallery.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaGallery.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import com.ismartcoding.lib.extensions.isUrl import com.ismartcoding.lib.extensions.isVideoFast import com.ismartcoding.lib.logcat.LogCat import com.ismartcoding.plain.enums.ImageType @@ -100,7 +101,7 @@ fun MediaGallery( key(page) { val item = getItem(page) val model: Any? - if (item.path.isVideoFast()) { + if (item.path.isVideoFast() || item.path.isUrl()) { model = item } else if (item.size <= 2000 * 1000) { // If the image size is less than 2MB, load the image directly diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaNormalImage.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaNormalImage.kt index 81d4cd2e..2b49619b 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaNormalImage.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaNormalImage.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.IntSize import androidx.media3.common.util.UnstableApi import coil3.compose.AsyncImage import coil3.imageLoader +import com.ismartcoding.lib.extensions.isUrl import com.ismartcoding.plain.ui.components.mediaviewer.previewer.DEFAULT_CROSS_FADE_ANIMATE_SPEC import com.ismartcoding.plain.ui.preview.PreviewItem import kotlinx.coroutines.launch @@ -151,7 +152,7 @@ fun MediaNormalImage( var painter by remember { mutableStateOf(null) } - if (model.isWebUrl()) { + if (model.path.isUrl()) { painter = rememberCoilImagePainter(model.path) var isMounted by remember { mutableStateOf(false) } imageSpecified = painter!!.intrinsicSize.isSpecified diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaViewer.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaViewer.kt index ecb76464..31377b41 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaViewer.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaViewer.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach +import com.ismartcoding.lib.extensions.isUrl import com.ismartcoding.lib.extensions.isVideoFast import com.ismartcoding.lib.logcat.LogCat import com.ismartcoding.plain.ui.components.mediaviewer.hugeimage.ImageDecoder @@ -499,7 +500,7 @@ fun MediaViewer( state.scale.snapTo(desScale) state.offsetX.snapTo(desX) state.offsetY.snapTo(desY) - state.rotation.snapTo(desRotation) + // state.rotation.snapTo(desRotation) } // 这里判断是否已运动到边界,如果到了边界,就不消费事件,让上层界面获取到事件 @@ -539,13 +540,11 @@ fun MediaViewer( state.mountedFlow.emit(true) } } - /** - * 根据不同类型的model进行不同的渲染 - */ + when (model) { is PreviewItem, -> { - if (model.path.isVideoFast() && !model.isWebUrl()) { + if (model.path.isVideoFast() && !model.path.isUrl()) { MediaVideo( pagerState = pagerState, page = page, diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/ImagePreviewActions.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/ImagePreviewActions.kt index 235c4af4..5db62593 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/ImagePreviewActions.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/ImagePreviewActions.kt @@ -33,10 +33,9 @@ import androidx.compose.ui.unit.dp import coil3.imageLoader import com.ismartcoding.lib.extensions.getFilenameExtension import com.ismartcoding.lib.extensions.getFilenameFromPath -import com.ismartcoding.lib.helpers.CoroutinesHelper +import com.ismartcoding.lib.extensions.isUrl import com.ismartcoding.lib.helpers.CoroutinesHelper.withIO import com.ismartcoding.lib.helpers.ValidateHelper -import com.ismartcoding.lib.logcat.LogCat import com.ismartcoding.plain.R import com.ismartcoding.plain.features.ImageMediaStoreHelper import com.ismartcoding.plain.features.locale.LocaleHelper @@ -45,8 +44,12 @@ import com.ismartcoding.plain.helpers.FileHelper import com.ismartcoding.plain.helpers.PathHelper import com.ismartcoding.plain.helpers.ShareHelper import com.ismartcoding.plain.ui.base.HorizontalSpace +import com.ismartcoding.plain.ui.base.PMiniButton +import com.ismartcoding.plain.ui.base.PMiniOutlineButton +import com.ismartcoding.plain.ui.components.CastDialog import com.ismartcoding.plain.ui.components.mediaviewer.MediaViewerState import com.ismartcoding.plain.ui.helpers.DialogHelper +import com.ismartcoding.plain.ui.models.CastViewModel import com.ismartcoding.plain.ui.preview.PreviewItem import com.ismartcoding.plain.ui.theme.darkMask import com.ismartcoding.plain.ui.theme.lightMask @@ -54,17 +57,38 @@ import kotlinx.coroutines.launch import java.io.File @Composable -fun ImagePreviewActions(context: Context, uiAlpha: Float, item: PreviewItem, getViewerState: () -> MediaViewerState?, state: MediaPreviewerState) { +fun ImagePreviewActions(context: Context, castViewModel: CastViewModel, m: PreviewItem, getViewerState: () -> MediaViewerState?, state: MediaPreviewerState) { val scope = rememberCoroutineScope() + + CastDialog(castViewModel) + Box( modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp, vertical = 48.dp) - .alpha(uiAlpha) + .alpha(state.uiAlpha.value) ) { if (!state.showActions) { return } + if (castViewModel.castMode.value) { + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.darkMask()) + .padding(horizontal = 20.dp, vertical = 8.dp), + ) { + PMiniButton(text = stringResource(id = R.string.cast)) { + castViewModel.cast(m.path) + } + HorizontalSpace(dp = 20.dp) + PMiniOutlineButton(text = stringResource(id = R.string.exit_cast_mode), color = Color.LightGray) { + castViewModel.exitCastMode() + } + } + return + } Row( modifier = Modifier .align(Alignment.BottomCenter) @@ -76,19 +100,19 @@ fun ImagePreviewActions(context: Context, uiAlpha: Float, item: PreviewItem, get icon = Icons.Rounded.Share, contentDescription = stringResource(R.string.share), ) { - if (item.mediaId.isNotEmpty()) { - ShareHelper.shareUris(context, listOf(ImageMediaStoreHelper.getItemUri(item.mediaId))) - } else if (ValidateHelper.isUrl(item.path)) { + if (m.mediaId.isNotEmpty()) { + ShareHelper.shareUris(context, listOf(ImageMediaStoreHelper.getItemUri(m.mediaId))) + } else if (m.path.isUrl()) { scope.launch { val cachedPath = context.imageLoader - .diskCache?.openSnapshot(item.path)?.data - val tempFile = File.createTempFile("imagePreviewShare", "." + item.path.getFilenameExtension(), File(context.cacheDir, "/image_cache")) + .diskCache?.openSnapshot(m.path)?.data + val tempFile = File.createTempFile("imagePreviewShare", "." + m.path.getFilenameExtension(), File(context.cacheDir, "/image_cache")) if (cachedPath != null) { cachedPath.toFile().copyTo(tempFile, true) ShareHelper.shareFile(context, tempFile) } else { DialogHelper.showLoading() - val r = withIO { DownloadHelper.downloadToTempAsync(item.path, tempFile) } + val r = withIO { DownloadHelper.downloadToTempAsync(m.path, tempFile) } DialogHelper.hideLoading() if (r.success) { ShareHelper.shareFile(context, File(r.path)) @@ -98,7 +122,7 @@ fun ImagePreviewActions(context: Context, uiAlpha: Float, item: PreviewItem, get } } } else { - ShareHelper.shareFile(context, File(item.path)) + ShareHelper.shareFile(context, File(m.path)) } } HorizontalSpace(dp = 20.dp) @@ -106,7 +130,7 @@ fun ImagePreviewActions(context: Context, uiAlpha: Float, item: PreviewItem, get icon = Icons.Rounded.Cast, contentDescription = stringResource(R.string.cast), ) { - + castViewModel.showCastDialog.value = true } HorizontalSpace(dp = 20.dp) ActionIconButton( @@ -125,12 +149,12 @@ fun ImagePreviewActions(context: Context, uiAlpha: Float, item: PreviewItem, get contentDescription = stringResource(R.string.save), ) { scope.launch { - if (ValidateHelper.isUrl(item.path)) { + if (m.path.isUrl()) { DialogHelper.showLoading() val cachedPath = context.imageLoader - .diskCache?.openSnapshot(item.path)?.data + .diskCache?.openSnapshot(m.path)?.data if (cachedPath != null) { - val r = withIO { FileHelper.copyFileToPublicDir(cachedPath.toString(), Environment.DIRECTORY_PICTURES, newName = item.path.getFilenameFromPath()) } + val r = withIO { FileHelper.copyFileToPublicDir(cachedPath.toString(), Environment.DIRECTORY_PICTURES, newName = m.path.getFilenameFromPath()) } DialogHelper.hideLoading() if (r.isNotEmpty()) { DialogHelper.showMessage(LocaleHelper.getStringF(R.string.image_save_to, "path", r)) @@ -140,7 +164,7 @@ fun ImagePreviewActions(context: Context, uiAlpha: Float, item: PreviewItem, get return@launch } val dir = PathHelper.getPlainPublicDir(Environment.DIRECTORY_PICTURES) - val r = withIO { DownloadHelper.downloadAsync(item.path, dir.absolutePath) } + val r = withIO { DownloadHelper.downloadAsync(m.path, dir.absolutePath) } DialogHelper.hideLoading() if (r.success) { DialogHelper.showMessage(LocaleHelper.getStringF(R.string.image_save_to, "path", r.path)) @@ -148,7 +172,7 @@ fun ImagePreviewActions(context: Context, uiAlpha: Float, item: PreviewItem, get DialogHelper.showMessage(r.message) } } else { - val r = withIO { FileHelper.copyFileToPublicDir(item.path, Environment.DIRECTORY_PICTURES) } + val r = withIO { FileHelper.copyFileToPublicDir(m.path, Environment.DIRECTORY_PICTURES) } if (r.isNotEmpty()) { DialogHelper.showMessage(LocaleHelper.getStringF(R.string.image_save_to, "path", r)) } else { diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaPreviewer.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaPreviewer.kt index 797dfc6a..179652f4 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaPreviewer.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaPreviewer.kt @@ -20,26 +20,25 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.viewmodel.compose.viewModel import com.ismartcoding.lib.extensions.isVideoFast import com.ismartcoding.plain.db.DTag import com.ismartcoding.plain.db.DTagRelation import com.ismartcoding.plain.ui.components.mediaviewer.MediaGallery import com.ismartcoding.plain.ui.components.mediaviewer.MediaGalleryState import com.ismartcoding.plain.ui.components.mediaviewer.ViewImageBottomSheet +import com.ismartcoding.plain.ui.models.CastViewModel import com.ismartcoding.plain.ui.models.MediaPreviewData import com.ismartcoding.plain.ui.models.TagsViewModel import com.ismartcoding.plain.ui.preview.PreviewItem @@ -55,8 +54,6 @@ class MediaPreviewerState( galleryState: MediaGalleryState, ) : PreviewerVerticalDragState(scope, galleryState = galleryState) { - var showActions by mutableStateOf(true) - var showMediaInfo by mutableStateOf(false) companion object { fun getSaver(galleryState: MediaGalleryState): Saver { @@ -151,6 +148,7 @@ fun ImagePreviewer( getItem: @Composable (Int) -> PreviewItem = { index -> MediaPreviewData.items[index] }, + castViewModel: CastViewModel = viewModel(), tagsViewModel: TagsViewModel? = null, tagsMap: Map>? = null, tagsState: List = emptyList(), @@ -215,7 +213,7 @@ fun ImagePreviewer( this.foreground = { page -> val m = getItem(page) if (!m.path.isVideoFast()) { - ImagePreviewActions(context = context, uiAlpha = state.uiAlpha.value, item = m, getViewerState = { state.currentViewerState }, state) + ImagePreviewActions(context = context, castViewModel = castViewModel, m = m, getViewerState = { state.currentViewerState }, state) } } }, diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaViewerContainer.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaViewerContainer.kt index 738d2b9a..d3bb8af2 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaViewerContainer.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaViewerContainer.kt @@ -29,12 +29,6 @@ internal class ViewerContainerState( var mediaViewerState: MediaViewerState = MediaViewerState(), ) { - /** - * +-------------------+ - * INTERNAL - * +-------------------+ - */ - // 转换图层transformContent透明度 internal var transformContentAlpha = Animatable(0F) diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/PreviewerTransformState.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/PreviewerTransformState.kt index a36aae19..552ae395 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/PreviewerTransformState.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/PreviewerTransformState.kt @@ -26,13 +26,6 @@ open class PreviewerTransformState( val galleryState: MediaGalleryState, var currentViewerState: MediaViewerState? = null, ) { - - /** - * +-------------------+ - * PRIVATE - * +-------------------+ - */ - // 锁对象 private var mutex = Mutex() @@ -60,12 +53,6 @@ open class PreviewerTransformState( } } - /** - * +-------------------+ - * INTERNAL - * +-------------------+ - */ - // 等待界面刷新的ticket internal val ticket = Ticket() @@ -90,11 +77,11 @@ open class PreviewerTransformState( get() = (viewerContainerState?.openTransformJob != null) || (mediaViewerState?.mountedFlow?.value == true) // 标记打开动作,执行开始 - internal suspend fun stateOpenStart() = + private suspend fun stateOpenStart() = updateState(animating = true, visible = false, visibleTarget = true) // 标记打开动作,执行结束 - internal suspend fun stateOpenEnd() = + private suspend fun stateOpenEnd() = updateState(animating = false, visible = true, visibleTarget = null) // 标记关闭动作,执行开始 @@ -126,11 +113,8 @@ open class PreviewerTransformState( } } - /** - * +-------------------+ - * PUBLIC - * +-------------------+ - */ + var showActions by mutableStateOf(true) + var showMediaInfo by mutableStateOf(false) // 是否正在进行动画 var animating by mutableStateOf(false) @@ -141,8 +125,7 @@ open class PreviewerTransformState( internal set // 是否可见的目标值 - var visibleTarget by mutableStateOf(null) - internal set + private var visibleTarget by mutableStateOf(null) // 是否允许执行open操作 val canOpen: Boolean @@ -175,11 +158,6 @@ open class PreviewerTransformState( // 清除全部transformItems fun clearTransformItems() = transformItemStateMap.clear() - /** - * 打开previewer - * @param index Int - * @param itemState TransformItemState? - */ suspend fun open( index: Int = 0, itemState: TransformItemState? = null, @@ -196,6 +174,7 @@ open class PreviewerTransformState( } } scope.launch { + showActions = true // 标记开始 stateOpenStart() // 开启UI @@ -254,6 +233,7 @@ open class PreviewerTransformState( animateContainerVisibleState = MutableTransitionState(false) } ).awaitAll() + showActions = true ticket.awaitNextTicket() transformState?.setExitState() } @@ -263,11 +243,8 @@ open class PreviewerTransformState( index: Int, itemState: TransformItemState ) { - // 动画开始 stateOpenStart() - // 关闭UI uiAlpha.snapTo(0F) - // 关闭viewer viewerAlpha.snapTo(0F) // 设置新的container状态立刻设置为true animateContainerVisibleState = MutableTransitionState(true) @@ -301,7 +278,6 @@ open class PreviewerTransformState( } suspend fun closeTransform() { - // 标记开始 stateCloseStart() // 判断当前状态是否允许transform结束 // 需要在cancel前获取该值 @@ -351,9 +327,10 @@ open class PreviewerTransformState( // container动画退出 animateContainerVisibleState.targetState = false } - // 允许使用loading + viewerContainerState?.allowLoading = true - // 标记结束 + showActions = true + stateCloseEnd() } diff --git a/app/src/main/java/com/ismartcoding/plain/ui/extensions/TextView.kt b/app/src/main/java/com/ismartcoding/plain/ui/extensions/TextView.kt index 915e880e..9230e382 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/extensions/TextView.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/extensions/TextView.kt @@ -8,6 +8,7 @@ import android.view.GestureDetector import android.view.MotionEvent import android.widget.TextView import androidx.core.view.GestureDetectorCompat +import coil3.imageLoader import com.ismartcoding.lib.extensions.dp2px import com.ismartcoding.lib.extensions.getFinalPath import com.ismartcoding.lib.helpers.CoroutinesHelper.coMain @@ -39,6 +40,9 @@ import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin import io.noties.markwon.linkify.LinkifyPlugin import org.commonmark.node.Image import org.commonmark.node.SoftLineBreak +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.jsoup.Jsoup @SuppressLint("ClickableViewAccessibility") fun TextView.setSelectableTextClickable(click: () -> Unit) { @@ -96,9 +100,14 @@ fun TextView.setDoubleCLick(click: () -> Unit) { fun TextView.markdown(content: String, previewerState: MediaPreviewerState) { this.movementMethod = LinkMovementMethod.getInstance() - Markwon.builder(context) - .usePlugin(ImagesPlugin.create()) - .usePlugin(CoilImagesPlugin.create(context, newImageLoader(context))) + val markdown = Markwon.builder(context) + .usePlugin(CoilImagesPlugin.create(context, context.imageLoader)) + .usePlugin( + ImagesPlugin.create { plugin -> + plugin.addSchemeHandler(AppImageSchemeHandler(context)) + plugin.addSchemeHandler(NetworkSchemeHandler()) + }, + ) .usePlugin( object : AbstractMarkwonPlugin() { override fun configureTheme(builder: MarkwonTheme.Builder) { @@ -107,12 +116,6 @@ fun TextView.markdown(content: String, previewerState: MediaPreviewerState) { } }, ) - .usePlugin( - ImagesPlugin.create { plugin -> - plugin.addSchemeHandler(AppImageSchemeHandler(context)) - plugin.addSchemeHandler(NetworkSchemeHandler()) - }, - ) .usePlugin(HtmlPlugin.create { plugin -> plugin.addHandler(FontTagHandler()).addHandler(AppImageHandler.create()) }) @@ -140,10 +143,9 @@ fun TextView.markdown(content: String, previewerState: MediaPreviewerState) { ImageProps.DESTINATION.require(props), ) { _, link -> coMain { - MediaPreviewData.items = listOf(PreviewItem(link, Uri.EMPTY, link.getFinalPath(context))) - previewerState.open( - index = 0, - ) + val links = extractImageLinksFromHtml(convertMarkdownToHtml(content)).map { PreviewItem(it, Uri.EMPTY, it.getFinalPath(context)) } + MediaPreviewData.items = links + previewerState.open(index = links.indexOfFirst { it.id == link }) } } } @@ -157,5 +159,32 @@ fun TextView.markdown(content: String, previewerState: MediaPreviewerState) { } }, ) - .build().setMarkdown(this, content) + .build() + + markdown.setMarkdown(this, content) +} + +fun extractImageLinksFromHtml(htmlContent: String): List { + val imageLinks = mutableListOf() + + // Parse the HTML content using Jsoup + val doc = Jsoup.parse(htmlContent) + + // Select all tags + val imgTags = doc.select("img") + + // Extract src attributes from tags + for (imgTag in imgTags) { + val imageUrl = imgTag.attr("src") + imageLinks.add(imageUrl) + } + + return imageLinks } + +fun convertMarkdownToHtml(markdownText: String): String { + val parser = Parser.builder().build() + val renderer = HtmlRenderer.builder().build() + val document = parser.parse(markdownText) + return renderer.render(document) +} \ No newline at end of file diff --git a/app/src/main/java/com/ismartcoding/plain/ui/models/CastViewModel.kt b/app/src/main/java/com/ismartcoding/plain/ui/models/CastViewModel.kt index 2de76001..ba5a56c2 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/models/CastViewModel.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/models/CastViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ismartcoding.lib.extensions.isUrl import com.ismartcoding.lib.helpers.CoroutinesHelper import com.ismartcoding.lib.logcat.LogCat import com.ismartcoding.lib.upnp.UPnPController @@ -42,6 +43,10 @@ class CastViewModel : ViewModel() { fun exitCastMode() { castMode.value = false + val device = CastPlayer.currentDevice ?: return + viewModelScope.launch(Dispatchers.IO) { + UPnPController.stopAVTransportAsync(device) + } } fun cast(path: String) { diff --git a/app/src/main/java/com/ismartcoding/plain/ui/models/FeedsViewModel.kt b/app/src/main/java/com/ismartcoding/plain/ui/models/FeedsViewModel.kt index 1b705c25..8983d75c 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/models/FeedsViewModel.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/models/FeedsViewModel.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.toMutableStateList import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ismartcoding.lib.extensions.isUrl import com.ismartcoding.lib.helpers.ValidateHelper import com.ismartcoding.lib.rss.model.RssChannel import com.ismartcoding.plain.R @@ -69,7 +70,7 @@ class FeedsViewModel(private val savedStateHandle: SavedStateHandle) : ISelectab fun fetchChannel() { editUrlError.value = "" - if (!ValidateHelper.isUrl(editUrl.value)) { + if (!editUrl.value.isUrl()) { editUrlError.value = getString(R.string.invalid_url) return } @@ -99,7 +100,7 @@ class FeedsViewModel(private val savedStateHandle: SavedStateHandle) : ISelectab fun edit() { editUrlError.value = "" - if (!ValidateHelper.isUrl(editUrl.value)) { + if (!editUrl.value.isUrl()) { editUrlError.value = getString(R.string.invalid_url) return } diff --git a/app/src/main/java/com/ismartcoding/plain/ui/models/MediaPreviewData.kt b/app/src/main/java/com/ismartcoding/plain/ui/models/MediaPreviewData.kt index 6c6d806c..c7a7169f 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/models/MediaPreviewData.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/models/MediaPreviewData.kt @@ -4,8 +4,12 @@ import android.content.Context import android.net.Uri import androidx.compose.ui.unit.toSize import com.ismartcoding.lib.extensions.getFinalPath +import com.ismartcoding.lib.extensions.isImageFast +import com.ismartcoding.lib.extensions.isVideoFast import com.ismartcoding.plain.data.DImage import com.ismartcoding.plain.db.DMessageFile +import com.ismartcoding.plain.db.DMessageFiles +import com.ismartcoding.plain.db.DMessageImages import com.ismartcoding.plain.ui.components.mediaviewer.previewer.TransformItemState import com.ismartcoding.plain.ui.preview.PreviewItem @@ -16,10 +20,18 @@ object MediaPreviewData { fun setDataAsync( context: Context, itemState: TransformItemState, - files: List, + chatItems: List, m: DMessageFile ) { - items = files.map { f -> + val newItems = mutableListOf() + chatItems.forEach { item -> + if (item.value is DMessageImages) { + newItems.addAll((item.value as DMessageImages).items) + } else if (item.value is DMessageFiles) { + newItems.addAll((item.value as DMessageFiles).items.filter { it.uri.isVideoFast() || it.uri.isImageFast() }) + } + } + items = newItems.map { f -> PreviewItem(f.id, Uri.EMPTY, f.uri.getFinalPath(context), f.size, data = f) } items.find { it.id == m.id }?.let { diff --git a/app/src/main/java/com/ismartcoding/plain/ui/preview/MediaItem.kt b/app/src/main/java/com/ismartcoding/plain/ui/preview/MediaItem.kt index 23f62f40..83ec17f5 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/preview/MediaItem.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/preview/MediaItem.kt @@ -23,10 +23,6 @@ data class PreviewItem( var intrinsicSize: IntSize = IntSize.Zero var rotation: Int = -1 - fun isWebUrl(): Boolean { - return ValidateHelper.isUrl(path) - } - fun initAsync(context: Context, width: Int, height: Int) { if (path.isImageFast()) { rotation = ImageHelper.getRotation(path) diff --git a/app/src/main/java/com/ismartcoding/plain/web/HttpModule.kt b/app/src/main/java/com/ismartcoding/plain/web/HttpModule.kt index 381827be..e7e16a27 100644 --- a/app/src/main/java/com/ismartcoding/plain/web/HttpModule.kt +++ b/app/src/main/java/com/ismartcoding/plain/web/HttpModule.kt @@ -10,6 +10,7 @@ import com.ismartcoding.lib.channel.sendEvent import com.ismartcoding.lib.extensions.compress import com.ismartcoding.lib.extensions.getFinalPath import com.ismartcoding.lib.extensions.isImageFast +import com.ismartcoding.lib.extensions.isUrl import com.ismartcoding.lib.extensions.newFile import com.ismartcoding.lib.extensions.parse import com.ismartcoding.lib.extensions.scanFileByConnection @@ -26,26 +27,29 @@ import com.ismartcoding.lib.upnp.UPnPController import com.ismartcoding.plain.BuildConfig import com.ismartcoding.plain.MainApp import com.ismartcoding.plain.TempData +import com.ismartcoding.plain.api.HttpClientManager import com.ismartcoding.plain.data.DownloadFileItem import com.ismartcoding.plain.data.DownloadFileItemWrap import com.ismartcoding.plain.data.UploadInfo import com.ismartcoding.plain.enums.DataType import com.ismartcoding.plain.enums.ImageType import com.ismartcoding.plain.enums.PasswordType -import com.ismartcoding.plain.preference.AuthTwoFactorPreference -import com.ismartcoding.plain.preference.PasswordPreference -import com.ismartcoding.plain.preference.PasswordTypePreference import com.ismartcoding.plain.features.ConfirmToAcceptLoginEvent +import com.ismartcoding.plain.features.ImageMediaStoreHelper +import com.ismartcoding.plain.features.PackageHelper import com.ismartcoding.plain.features.audio.AudioMediaStoreHelper import com.ismartcoding.plain.features.file.FileSortBy -import com.ismartcoding.plain.features.ImageMediaStoreHelper import com.ismartcoding.plain.features.media.CastPlayer -import com.ismartcoding.plain.features.PackageHelper import com.ismartcoding.plain.features.video.VideoMediaStoreHelper import com.ismartcoding.plain.helpers.ImageHelper import com.ismartcoding.plain.helpers.TempHelper import com.ismartcoding.plain.helpers.UrlHelper +import com.ismartcoding.plain.preference.AuthTwoFactorPreference +import com.ismartcoding.plain.preference.PasswordPreference +import com.ismartcoding.plain.preference.PasswordTypePreference import com.ismartcoding.plain.web.websocket.WebSocketSession +import io.ktor.client.request.get +import io.ktor.client.statement.readBytes import io.ktor.http.CacheControl import io.ktor.http.ContentType import io.ktor.http.HttpHeaders @@ -57,6 +61,7 @@ import io.ktor.http.content.LastModifiedVersion import io.ktor.http.content.PartData import io.ktor.http.content.forEachPart import io.ktor.http.content.streamProvider +import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.Application import io.ktor.server.application.ApplicationCallPipeline @@ -101,6 +106,7 @@ import org.json.JSONArray import org.json.JSONObject import java.io.ByteArrayOutputStream import java.io.File +import java.io.IOException import java.util.Date import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @@ -213,7 +219,15 @@ object HttpModule { return@get } - if (path.startsWith("content://")) { + if (path.isUrl()) { + try { + val client = HttpClientManager.browserClient() + val r = client.get(path) + call.respondBytes(r.readBytes(), r.contentType() ?: ContentType.Application.OctetStream) + } catch (e: IOException) { + call.respondText("Failed to fetch data from URL: $path", status = HttpStatusCode.InternalServerError) + } + } else if (path.startsWith("content://")) { val bytes = MainApp.instance.contentResolver.openInputStream(Uri.parse(path))?.buffered()?.use { it.readBytes() } call.respondBytes(bytes!!) } else if (path.isImageFast()) { diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index a1c2d26f..e3de112c 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -2,6 +2,10 @@ PlainApp আপনার ডিভাইসে সক্রিয় ভিপিএন আছে, যা একটি পিসি থেকে অ্যাক্সেস করার সময় ওয়েব সংযোগের সমস্যা উত্পন্ন করতে পারে। এটি সমাধান করতে, আমরা আপনাকে আপনার ভিপিএন সেটিংস পরিবর্তন করে এলএএন ট্র্যাফিক অনুমতি দেওয়া বা আপনার ভিপিএন নিষ্ক্রিয় করতে প্রস্তাবনা দিচ্ছি। সতর্কবার্তা + কাস্ট মোড থেকে বের হোন + ঘুরান + চিত্র {{path}}-এ সংরক্ষিত হয়েছে + চিত্র সংরক্ষণ করা ব্যর্থ হয়েছে সমাধান মাত্রা একটি ডিভাইস নির্বাচন করুন diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5d32c801..b1459d42 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2,6 +2,10 @@ PlainApp Ihr VPN ist derzeit auf Ihrem Gerät aktiviert, was potenziell zu Webverbindungsproblemen führen kann, wenn es von Ihrem PC aus zugegriffen wird. Um dies zu lösen, empfehlen wir Ihnen, Ihre VPN-Einstellungen anzupassen, um LAN-Verkehr zuzulassen oder Ihr VPN zu deaktivieren. Warnung + Cast-Modus beenden + Drehen + Bild gespeichert unter {{path}} + Speichern des Bildes fehlgeschlagen Auflösung Abmessungen Bitte ein Gerät auswählen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ddc789ef..d264431a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2,6 +2,10 @@ PlainApp Su VPN está actualmente habilitado en su dispositivo, lo que podría provocar problemas de conexión web cuando se accede desde su PC. Para resolver esto, le recomendamos ajustar la configuración de su VPN para permitir el tráfico de LAN o desactivar su VPN. Advertencia + Salir del modo de transmisión + Rotar + Imagen guardada en {{path}} + Error al guardar la imagen Resolución Dimensiones Por favor, selecciona un dispositivo diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 51e380e4..fe0da4a0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2,6 +2,10 @@ PlainApp Votre VPN est actuellement activé sur votre appareil, ce qui pourrait potentiellement entraîner des problèmes de connexion Web lorsqu\'il est accédé depuis un PC. Pour résoudre cela, nous vous recommandons d\'ajuster les paramètres de votre VPN pour autoriser le trafic LAN ou de désactiver votre VPN. Avertissement + Quitter le mode de diffusion + Faire pivoter + Image enregistrée dans {{path}} + Échec de l\'enregistrement de l\'image Résolution Dimensions Sélectionnez un appareil diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index de878da9..ebcd8357 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -2,6 +2,10 @@ PlainApp आपके डिवाइस पर वर्तमान में आपका VPN सक्रिय है, जिससे PC से एक्सेस करते समay वेब कनेक्शन समस्याएँ उत्पन्न हो सकती हैं। इसे हल करने के लिए, हम सुझाव देते हैं कि आप अपने VPN सेटिंग्स को LAN ट्रैफ़िक की अनुमति देने या अपने VPN को अक्षम करने के लिए समायोजित करें। चेतावनी + कास्ट मोड से बाहर निकलें + घुमाएं + छवि {{path}} में सहेजी गई + छवि सहेजने में विफल रहा संकल्प आयाम एक उपकरण चुनें diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 3218acc5..05f90a75 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2,6 +2,10 @@ PlainApp Attualmente, il tuo VPN è attivo sul tuo dispositivo, il che potrebbe portare a problemi di connessione web quando si accede da un PC. Per risolvere questo problema, ti consigliamo di regolare le impostazioni del tuo VPN per consentire il traffico LAN o disattivare il tuo VPN. Attenzione + Esci dalla modalità di trasmissione + Ruota + Immagine salvata in {{path}} + Salvataggio dell\'immagine non riuscito Risoluzione Dimensioni Seleziona un dispositivo diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5137dcf4..83c595f3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -2,6 +2,10 @@ PlainApp お使いのデバイスでは現在VPNが有効になっており、PCからアクセスした際にWeb接続の問題が発生する可能性があります。これを解決するために、LANトラフィックを許可するようにVPNの設定を調整するか、VPNを無効にすることをお勧めします。 警告 + キャストモードを終了 + 回転 + {{path}} に画像が保存されました + 画像の保存に失敗しました 解像度 寸法 デバイスを選択してください diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 7a65c97a..286d669d 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2,6 +2,10 @@ PlainApp 현재 기기에서 VPN이 활성화되어 있어 PC에서 액세스할 때 웹 연결 문제가 발생할 수 있습니다. 이를 해결하기 위해 LAN 트래픽을 허용하거나 VPN을 비활성화하는 것을 권장합니다. 경고 + 캐스트 모드 종료 + 회전 + 이미지가 {{path}}에 저장되었습니다 + 이미지 저장 실패 해상도 크기 기기 선택 diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index be482e69..ef120692 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -2,6 +2,10 @@ PlainApp Uw VPN is momenteel ingeschakeld op uw apparaat, wat mogelijk kan leiden tot webverbinding problemen bij toegang vanaf uw pc. Om dit op te lossen, raden we aan om uw VPN-instellingen aan te passen om LAN-verkeer toe te staan of uw VPN uit te schakelen. Waarschuwing + Verlaat castmodus + Roterend + Afbeelding opgeslagen in {{path}} + Afbeelding opslaan mislukt Resolutie Afmetingen Selecteer een apparaat diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index a6e86f6b..25f27134 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -2,6 +2,10 @@ PlainApp O seu VPN está atualmente ativado no seu dispositivo, o que pode potencialmente levar a problemas de conexão web quando acessado a partir do PC. Para resolver isso, recomendamos ajustar as configurações do seu VPN para permitir o tráfego de LAN ou desativar o seu VPN. Aviso + Sair do modo de transmissão + Girar + Imagem salva em {{path}} + Falha ao salvar a imagem Resolução Dimensões Selecione um dispositivo diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index de92aa37..55ba98fa 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2,6 +2,10 @@ PlainApp Ваш VPN в настоящее время включен на вашем устройстве, что может потенциально привести к проблемам с веб-подключением при доступе с ПК. Для устранения этой проблемы мы рекомендуем настроить параметры вашего VPN для разрешения локального сетевого трафика или отключить ваш VPN. Предупреждение + Выйти из режима передачи + Повернуть + Изображение сохранено в {{path}} + Не удалось сохранить изображение Разрешение Размеры Выберите устройство diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index eb54182c..6151cc0c 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -2,6 +2,10 @@ PlainApp உங்கள் உலாவியில் தற்போது VPN செல்லுபடி உள்ளது, இது பிரச்சனைகளைக் கொண்டுவரலாம் பாராட்டும் நேரத்தில். இதை தீர்மானிக்க, உங்கள் VPN அமைப்புகளை LAN பரிமாறத்தை அனுமதிக்க அல்லது உங்கள் VPN முடக்குதலை கொண்டுவிடுவதை பரிந்துரைக்கிறோம். எச்சரிக்கை + திருத்த முறையிலிருந்து வெளியேறு + சுழற்சி + படம் {{path}} இல் சேமிக்கப்பட்டது + படத்தை சேமிக்க தோல்வி தீர்வு அளவுகள் ஒரு சாதனத்தைத் தேர்ந்தெடு diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a2f581f0..1d103745 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -2,6 +2,10 @@ PlainApp VPN\'iniz şu anda cihazınızda etkinleştirilmiştir, bu da PC\'den erişildiğinde potansiyel olarak web bağlantısı sorunlarına yol açabilir. Bu sorunu çözmek için VPN ayarlarınızı lan trafiğine izin verecek şekilde ayarlamanızı veya VPN\'inizi devre dışı bırakmanızı öneririz. Uyarı + Yansıtma modundan çık + Döndür + {{path}} yoluna resim kaydedildi + Resim kaydetme başarısız oldu Çözünürlük Boyutlar Lütfen bir cihaz seçin diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 8bb6435a..33cff224 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -2,6 +2,10 @@ PlainApp VPN hiện đang được kích hoạt trên thiết bị của bạn, điều này có thể dẫn đến vấn đề kết nối web khi truy cập từ PC. Để giải quyết vấn đề này, chúng tôi khuyên bạn nên điều chỉnh cài đặt VPN của mình để cho phép lưu lượng LAN hoặc tắt VPN của bạn. Cảnh báo + Thoát chế độ trình chiếu + Xoay + Hình ảnh đã được lưu vào {{path}} + Không thể lưu hình ảnh Độ phân giải Kích thước Vui lòng chọn một thiết bị diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index da0aba71..0ea3970d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2,6 +2,10 @@ 简朴 您的手机启用了VPN,这可能会导致从电脑访问网页时出现连接问题。我们建议您调整VPN设置允许局域网流量或关闭VPN。 警告 + 退出投屏模式 + 旋转 + 图像已保存至{{path}} + 保存图像失败 分辨率 尺寸 选择一个设备 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d991180a..2e93720b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2,6 +2,10 @@ 簡樸 您的手機啟用了VPN,這可能會導致從電腦訪問網頁時出現連接問題。我們建議您調整VPN設置允許區域網路流量或關閉VPN。 警告 + 退出投影模式 + 旋轉 + 圖片已保存至{{path}} + 無法儲存圖片 分辨率 尺寸 請選擇一個設備 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9f773a83..27b8bf6a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ PlainApp Your VPN is currently enabled on your device, which could potentially lead to web connection issues when accessed from PC. To resolve this, we recommend adjusting your VPN settings to allow LAN traffic or disabling your VPN. Warning + Exit cast mode Rotate Image saved to {{path}} Failed to save image diff --git a/lib/src/main/java/com/ismartcoding/lib/extensions/String.kt b/lib/src/main/java/com/ismartcoding/lib/extensions/String.kt index 1e40203f..ee5ac7fc 100644 --- a/lib/src/main/java/com/ismartcoding/lib/extensions/String.kt +++ b/lib/src/main/java/com/ismartcoding/lib/extensions/String.kt @@ -5,7 +5,9 @@ import android.net.Uri import android.provider.MediaStore import android.telephony.PhoneNumberUtils import com.ismartcoding.lib.Constants +import com.ismartcoding.lib.helpers.ValidateHelper import java.io.File +import java.net.URL import java.net.URLDecoder import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -35,6 +37,15 @@ fun String.pathToUri(): Uri { return Uri.parse(this) } +fun String.isUrl(): Boolean { + return try { + URL(this) + true + } catch (e: Exception) { + false + } +} + fun String.toAppUrl(context: Context): String { val prefix = context.getExternalFilesDir(null)?.path?.removeSuffix("/") + "/" return this.replace(prefix, "app://") diff --git a/lib/src/main/java/com/ismartcoding/lib/helpers/ValidateHelper.kt b/lib/src/main/java/com/ismartcoding/lib/helpers/ValidateHelper.kt index d915dcd4..a1b4d460 100644 --- a/lib/src/main/java/com/ismartcoding/lib/helpers/ValidateHelper.kt +++ b/lib/src/main/java/com/ismartcoding/lib/helpers/ValidateHelper.kt @@ -1,6 +1,5 @@ package com.ismartcoding.lib.helpers -import java.net.URL object ValidateHelper { fun isEmail(email: String): Boolean { @@ -10,13 +9,4 @@ object ValidateHelper { fun isPhone(phone: String): Boolean { return android.util.Patterns.PHONE.matcher(phone).matches() } - - fun isUrl(url: String): Boolean { - return try { - URL(url) - true - } catch (e: Exception) { - false - } - } } \ No newline at end of file diff --git a/lib/src/main/java/com/ismartcoding/lib/upnp/UPnPController.kt b/lib/src/main/java/com/ismartcoding/lib/upnp/UPnPController.kt index 6c000f6d..ddbfca82 100644 --- a/lib/src/main/java/com/ismartcoding/lib/upnp/UPnPController.kt +++ b/lib/src/main/java/com/ismartcoding/lib/upnp/UPnPController.kt @@ -54,7 +54,43 @@ object UPnPController { return "" } - public suspend inline fun HttpClient.subscribe( + suspend fun stopAVTransportAsync( + device: UPnPDevice, + ): String { + val service = device.getAVTransportService() ?: return "" + try { + val client = HttpClient(CIO) + val response = + withIO { + client.post(device.getBaseUrl() + "/" + service.controlURL.trimStart('/')) { + headers { + set("Content-Type", "text/xml") + set("SOAPAction", "\"${service.serviceType}#Stop\"") + } + setBody( + getRequestBody( + """ + + 0 + + """.trimIndent(), + ), + ) + } + } + LogCat.e(response.toString()) + val xml = response.body() + LogCat.e(xml) + if (response.status == HttpStatusCode.OK) { + return xml + } + } catch (ex: Exception) { + ex.printStackTrace() + } + return "" + } + + suspend inline fun HttpClient.subscribe( urlString: String, block: HttpRequestBuilder.() -> Unit = {}, ): HttpResponse { diff --git a/lib/src/main/java/com/ismartcoding/lib/upnp/UPnPDevice.kt b/lib/src/main/java/com/ismartcoding/lib/upnp/UPnPDevice.kt index a21a4087..b3256c82 100644 --- a/lib/src/main/java/com/ismartcoding/lib/upnp/UPnPDevice.kt +++ b/lib/src/main/java/com/ismartcoding/lib/upnp/UPnPDevice.kt @@ -14,7 +14,7 @@ class UPnPDevice( val uSN = parseHeader(header, "USN: ") val sT = parseHeader(header, "ST: ") - var descriptionXML: String = "" + private var descriptionXML: String = "" var description: DescriptionModel? = null fun isAVTransport(): Boolean {