From d2ed0c65ca712ef4a6ec9314773cbdfe3b43c322 Mon Sep 17 00:00:00 2001 From: ajp-dev <48263871+ajp-dev@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:05:49 +0800 Subject: [PATCH 001/330] fix link typo for Script Signing typo causes HTTP 400 error when accessing link to Script Signing.md --- plugin-development.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin-development.md b/plugin-development.md index 6f49269e..de9fc70c 100644 --- a/plugin-development.md +++ b/plugin-development.md @@ -138,7 +138,7 @@ See [Pagers.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/doc When you deploy your plugin, you'll need to add code signing for security. -See [Script Signing.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Script%Signing.md) +See [Script Signing.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Script%20Signing.md) ## Plugin Deployment @@ -186,4 +186,4 @@ Make sure the signature is correctly generated and added. Also, ensure the versi If you have any issues or need further assistance, feel free to reach out to us at: -https://chat.futo.org/login/ \ No newline at end of file +https://chat.futo.org/login/ From a1d460385d644735a999e7b477a8e62ee240bfe4 Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 20 Jan 2025 15:38:26 -0600 Subject: [PATCH 002/330] prevent the user from needing to tap update on system dialog when self updating Changelog: added --- app/src/main/AndroidManifest.xml | 1 + .../java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index afd659a7..7a5f764b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ + = Build.VERSION_CODES.S) { + params.setRequireUserAction(USER_ACTION_NOT_REQUIRED) + } val sessionId = packageInstaller.createSession(params); session = packageInstaller.openSession(sessionId) From daf1d42a0f1398d4b01798c23c297c14bac15c84 Mon Sep 17 00:00:00 2001 From: Alexandre Picavet Date: Thu, 9 Jan 2025 17:44:11 +0100 Subject: [PATCH 003/330] feat(player): Add a setting to adjust player seek duration Create a seekOffset dropdown setting defaulting to 10 seconds. Update the fastForwardTick method of the GestureControlView to take the seekOffset setting into account and update the view accordingly. --- .../java/com/futo/platformplayer/Settings.kt | 18 +++++++++++++++++- .../views/behavior/GestureControlView.kt | 8 ++++---- app/src/main/res/values-fr/strings.xml | 8 ++++++++ app/src/main/res/values/strings.xml | 12 +++++++++++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 270158fd..4cdbf764 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -486,6 +486,22 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22) var deleteFromWatchLaterAuto: Boolean = true; + + @FormField(R.string.seek_offset, FieldForm.DROPDOWN, R.string.seek_offset_description, 23) + @DropdownFieldOptionsId(R.array.seek_offset_duration) + var seekOffset: Int = 2; + + fun getSeekOffset(): Long { + return when(seekOffset) { + 0 -> 3_000L; + 1 -> 5_000L; + 2 -> 10_000L; + 3 -> 20_000L; + 4 -> 30_000L; + 5 -> 60_000L; + else -> 10_000L; + } + } } @FormField(R.string.comments, "group", R.string.comments_description, 6) @@ -981,4 +997,4 @@ class Settings : FragmentedStorageFileJson() { } } //endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt index 5f4d2b33..3719e85e 100644 --- a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt @@ -628,12 +628,12 @@ class GestureControlView : LinearLayout { private fun fastForwardTick() { _fastForwardCounter++; - val seekOffset: Long = 10000; + val seekOffset: Long = Settings.instance.playback.getSeekOffset(); if (_rewinding) { - _textRewind.text = "${_fastForwardCounter * 10} " + context.getString(R.string.seconds); + _textRewind.text = "${_fastForwardCounter * seekOffset / 1_000} " + context.getString(R.string.seconds); onSeek.emit(-seekOffset); } else { - _textFastForward.text = "${_fastForwardCounter * 10} " + context.getString(R.string.seconds); + _textFastForward.text = "${_fastForwardCounter * seekOffset / 1_000} " + context.getString(R.string.seconds); onSeek.emit(seekOffset); } } @@ -807,4 +807,4 @@ class GestureControlView : LinearLayout { const val EXIT_DURATION_FAST_FORWARD: Long = 600; const val TAG = "GestureControlView"; } -} \ No newline at end of file +} diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 733eacc2..438020da 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -688,6 +688,14 @@ Continuer la lecture Superposition du lecteur + + 3 secondes + 5 secondes + 10 secondes + 20 secondes + 30 secondes + 60 secondes + Reprendre depuis le début Reprendre après 10s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa903f2f..3cff24ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,6 +403,8 @@ Allow full-screen portrait when watching horizontal videos Delete from WatchLater when watched After you leave a video that you mostly watched, it will be removed from watch later. + Seek duration + Fast-Forward / Fast-Rewind duration Switch to Audio in Background Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter Groups @@ -1026,6 +1028,14 @@ Within 30 seconds of loss Always + + 3 seconds + 5 seconds + 10 seconds + 20 seconds + 30 seconds + 60 seconds + 15 30 @@ -1039,4 +1049,4 @@ 1500 2000 - \ No newline at end of file + From d4ccf232c1251241a416ee895a14415759856540 Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 10 Feb 2025 17:57:27 -0600 Subject: [PATCH 004/330] fix HLS download odysee nebula peertube Changelog: changed --- .../main/java/com/futo/platformplayer/UISlideOverlays.kt | 8 +++++--- .../com/futo/platformplayer/downloads/VideoDownload.kt | 8 ++------ app/src/main/java/com/futo/platformplayer/parsers/HLS.kt | 6 ++++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 582a74f7..36f0b8e1 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -280,6 +280,8 @@ class UISlideOverlays { val masterPlaylistContent = masterPlaylistResponse.body?.string() ?: throw Exception("Master playlist content is empty") + val resolvedPlaylistUrl = masterPlaylistResponse.url + val videoButtons = arrayListOf() val audioButtons = arrayListOf() //TODO: Implement subtitles @@ -292,7 +294,7 @@ class UISlideOverlays { val masterPlaylist: HLS.MasterPlaylist try { - masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl) + masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl) masterPlaylist.getAudioSources().forEach { it -> @@ -366,11 +368,11 @@ class UISlideOverlays { if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) { withContext(Dispatchers.Main) { if (source is IHLSManifestSource) { - StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null) + StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null) UIDialogs.toast(container.context, "Variant video HLS playlist download started") slideUpMenuOverlay.hide() } else if (source is IHLSManifestAudioSource) { - StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null) + StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, resolvedPlaylistUrl), null) UIDialogs.toast(container.context, "Variant audio HLS playlist download started") slideUpMenuOverlay.hide() } else { diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 8ba2814f..a9a4e2bb 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -620,10 +620,8 @@ class VideoDownload { private suspend fun combineSegments(context: Context, segmentFiles: List, targetFile: File) = withContext(Dispatchers.IO) { suspendCancellableCoroutine { continuation -> - val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt") - fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" }) - - val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\"" + val cmd = + "-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\"" val statisticsCallback = StatisticsCallback { _ -> //TODO: Show progress? @@ -633,7 +631,6 @@ class VideoDownload { val session = FFmpegKit.executeAsync(cmd, { session -> if (ReturnCode.isSuccess(session.returnCode)) { - fileList.delete() continuation.resumeWith(Result.success(Unit)) } else { val errorMessage = if (ReturnCode.isCancel(session.returnCode)) { @@ -641,7 +638,6 @@ class VideoDownload { } else { "Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}" } - fileList.delete() continuation.resumeWithException(RuntimeException(errorMessage)) } }, diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index 9d1a3faa..eacdf546 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -61,7 +61,13 @@ class HLS { val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":") val streamInfo = lines.find { it.startsWith("#EXT-X-STREAM-INF:") }?.let { parseStreamInfo(it) } + val initSegment = + lines.find { it.startsWith("#EXT-X-MAP:") }?.substringAfter(":")?.split(",")?.get(0) + ?.substringAfter("=")?.trim('"') val segments = mutableListOf() + if (initSegment != null) { + segments.add(MediaSegment(0.0, resolveUrl(sourceUrl, initSegment))) + } var currentSegment: MediaSegment? = null lines.forEach { line -> when { From d5cab0910e9dbe7925c09a6266d71b3f21a78044 Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 10 Feb 2025 22:21:06 -0600 Subject: [PATCH 005/330] fix HLS audio download and download audio only Changelog: changed --- .../futo/platformplayer/UISlideOverlays.kt | 2 +- .../platformplayer/downloads/VideoDownload.kt | 14 ++-- .../platformplayer/downloads/VideoExport.kt | 2 +- .../com/futo/platformplayer/parsers/HLS.kt | 78 +++++++++++++------ 4 files changed, 63 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 582a74f7..a78cd611 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -292,7 +292,7 @@ class UISlideOverlays { val masterPlaylist: HLS.MasterPlaylist try { - masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl) + masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl, source is IHLSManifestAudioSource) masterPlaylist.getAudioSources().forEach { it -> diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 8ba2814f..011db8de 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -414,7 +414,7 @@ class VideoDownload { videoFilePath = File(downloadDir, videoFileName!!).absolutePath; } if(actualAudioSource != null) { - audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName(); + audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(if (actualAudioSource.container == "application/vnd.apple.mpegurl") actualAudioSource.codec else actualAudioSource.container)}".sanitizeFileName(); audioFilePath = File(downloadDir, audioFileName!!).absolutePath; } if(subtitleSource != null) { @@ -620,10 +620,8 @@ class VideoDownload { private suspend fun combineSegments(context: Context, segmentFiles: List, targetFile: File) = withContext(Dispatchers.IO) { suspendCancellableCoroutine { continuation -> - val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt") - fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" }) - - val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\"" + val cmd = + "-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\"" val statisticsCallback = StatisticsCallback { _ -> //TODO: Show progress? @@ -633,7 +631,6 @@ class VideoDownload { val session = FFmpegKit.executeAsync(cmd, { session -> if (ReturnCode.isSuccess(session.returnCode)) { - fileList.delete() continuation.resumeWith(Result.success(Unit)) } else { val errorMessage = if (ReturnCode.isCancel(session.returnCode)) { @@ -641,7 +638,6 @@ class VideoDownload { } else { "Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}" } - fileList.delete() continuation.resumeWithException(RuntimeException(errorMessage)) } }, @@ -1150,8 +1146,10 @@ class VideoDownload { fun audioContainerToExtension(container: String): String { if (container.contains("audio/mp4")) return "mp4a"; + else if (container.contains("video/mp4")) + return "mp4"; else if (container.contains("audio/mpeg")) - return "mpga"; + return "mp3"; else if (container.contains("audio/mp3")) return "mp3"; else if (container.contains("audio/webm")) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt index 7c1c4e09..7ebb70ff 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt @@ -81,7 +81,7 @@ class VideoExport { outputFile = f; } else if (a != null) { val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container); - val f = downloadRoot.createFile(a.container, outputFileName) + val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") a.codec else a.container, outputFileName) ?: throw Exception("Failed to create file in external directory."); Logger.i(TAG, "Copying audio."); diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index 9d1a3faa..bc01b4a6 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -1,5 +1,11 @@ package com.futo.platformplayer.parsers +import android.net.Uri +import androidx.annotation.OptIn +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory +import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist +import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource @@ -7,13 +13,20 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.toYesNo import com.futo.platformplayer.yesNoToBoolean +import java.io.ByteArrayInputStream import java.net.URI +import java.net.URLConnection import java.time.ZonedDateTime import java.time.format.DateTimeFormatter class HLS { companion object { - fun parseMasterPlaylist(masterPlaylistContent: String, sourceUrl: String): MasterPlaylist { + @OptIn(UnstableApi::class) + fun parseMasterPlaylist(masterPlaylistContent: String, sourceUrl: String, isAudioSource: Boolean? = null): MasterPlaylist { + val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray()) + val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser() + .parse(Uri.parse(sourceUrl), inputStream) + val baseUrl = URI(sourceUrl).resolve("./").toString() val variantPlaylists = mutableListOf() @@ -21,27 +34,39 @@ class HLS { val sessionDataList = mutableListOf() var independentSegments = false - masterPlaylistContent.lines().forEachIndexed { index, line -> - when { - line.startsWith("#EXT-X-STREAM-INF") -> { - val nextLine = masterPlaylistContent.lines().getOrNull(index + 1) - ?: throw Exception("Expected URI following #EXT-X-STREAM-INF, found none") - val url = resolveUrl(baseUrl, nextLine) + if (playlist is HlsMediaPlaylist) { + independentSegments = playlist.hasIndependentSegments + if (isAudioSource == true) { + val firstSegmentUrlFile = + Uri.parse(playlist.segments[0].initializationSegment?.url ?: playlist.segments[0].url).buildUpon().clearQuery().fragment(null) + .build().toString() + mediaRenditions.add(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null, URLConnection.guessContentTypeFromName(firstSegmentUrlFile))) + } else { + variantPlaylists.add(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null))) + } + } else if (playlist is HlsMultivariantPlaylist) { + masterPlaylistContent.lines().forEachIndexed { index, line -> + when { + line.startsWith("#EXT-X-STREAM-INF") -> { + val nextLine = masterPlaylistContent.lines().getOrNull(index + 1) + ?: throw Exception("Expected URI following #EXT-X-STREAM-INF, found none") + val url = resolveUrl(baseUrl, nextLine) - variantPlaylists.add(VariantPlaylistReference(url, parseStreamInfo(line))) - } + variantPlaylists.add(VariantPlaylistReference(url, parseStreamInfo(line))) + } - line.startsWith("#EXT-X-MEDIA") -> { - mediaRenditions.add(parseMediaRendition(line, baseUrl)) - } + line.startsWith("#EXT-X-MEDIA") -> { + mediaRenditions.add(parseMediaRendition(line, baseUrl)) + } - line == "#EXT-X-INDEPENDENT-SEGMENTS" -> { - independentSegments = true - } + line == "#EXT-X-INDEPENDENT-SEGMENTS" -> { + independentSegments = true + } - line.startsWith("#EXT-X-SESSION-DATA") -> { - val sessionData = parseSessionData(line) - sessionDataList.add(sessionData) + line.startsWith("#EXT-X-SESSION-DATA") -> { + val sessionData = parseSessionData(line) + sessionDataList.add(sessionData) + } } } } @@ -61,7 +86,13 @@ class HLS { val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":") val streamInfo = lines.find { it.startsWith("#EXT-X-STREAM-INF:") }?.let { parseStreamInfo(it) } + val initSegment = + lines.find { it.startsWith("#EXT-X-MAP:") }?.substringAfter(":")?.split(",")?.get(0) + ?.substringAfter("=")?.trim('"') val segments = mutableListOf() + if (initSegment != null) { + segments.add(MediaSegment(0.0, resolveUrl(sourceUrl, initSegment))) + } var currentSegment: MediaSegment? = null lines.forEach { line -> when { @@ -109,10 +140,10 @@ class HLS { } } - fun parseAndGetAudioSources(source: Any, content: String, url: String): List { + fun parseAndGetAudioSources(source: Any, content: String, url: String, isAudioSource: Boolean? = null): List { val masterPlaylist: MasterPlaylist try { - masterPlaylist = parseMasterPlaylist(content, url) + masterPlaylist = parseMasterPlaylist(content, url, isAudioSource) return masterPlaylist.getAudioSources() } catch (e: Throwable) { if (content.lines().any { it.startsWith("#EXTINF:") }) { @@ -270,7 +301,8 @@ class HLS { val name: String?, val isDefault: Boolean?, val isAutoSelect: Boolean?, - val isForced: Boolean? + val isForced: Boolean?, + val container: String? = null, ) { fun toM3U8Line(): String = buildString { append("#EXT-X-MEDIA:") @@ -340,7 +372,7 @@ class HLS { val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ") return@mapNotNull when (it.type) { - "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri) + "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", it.container ?: "", it.language ?: "", null, false, it.uri) else -> null } } @@ -376,7 +408,7 @@ class HLS { val programDateTime: ZonedDateTime?, val playlistType: String?, val streamInfo: StreamInfo?, - val segments: List + val segments: List, ) { fun buildM3U8(): String = buildString { append("#EXTM3U\n") From 3d258180bd1588a659b3079b4f1b887cd5fd43fe Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 11 Feb 2025 10:31:47 -0600 Subject: [PATCH 006/330] restore hard code HLS as mp4 Changelog: changed --- .../com/futo/platformplayer/downloads/VideoDownload.kt | 6 +++--- .../java/com/futo/platformplayer/downloads/VideoExport.kt | 6 +++--- app/src/main/java/com/futo/platformplayer/parsers/HLS.kt | 8 ++------ 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 011db8de..76528cd8 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -410,11 +410,11 @@ class VideoDownload { else audioSource; if(actualVideoSource != null) { - videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName(); + videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource.container)}".sanitizeFileName(); videoFilePath = File(downloadDir, videoFileName!!).absolutePath; } if(actualAudioSource != null) { - audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(if (actualAudioSource.container == "application/vnd.apple.mpegurl") actualAudioSource.codec else actualAudioSource.container)}".sanitizeFileName(); + audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource.container)}".sanitizeFileName(); audioFilePath = File(downloadDir, audioFileName!!).absolutePath; } if(subtitleSource != null) { @@ -1149,7 +1149,7 @@ class VideoDownload { else if (container.contains("video/mp4")) return "mp4"; else if (container.contains("audio/mpeg")) - return "mp3"; + return "mpga"; else if (container.contains("audio/mp3")) return "mp3"; else if (container.contains("audio/webm")) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt index 7ebb70ff..6761168c 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt @@ -69,7 +69,7 @@ class VideoExport { outputFile = f; } else if (v != null) { val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container); - val f = downloadRoot.createFile(v.container, outputFileName) + val f = downloadRoot.createFile(if (v.container == "application/vnd.apple.mpegurl") "video/mp4" else v.container, outputFileName) ?: throw Exception("Failed to create file in external directory."); Logger.i(TAG, "Copying video."); @@ -81,8 +81,8 @@ class VideoExport { outputFile = f; } else if (a != null) { val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container); - val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") a.codec else a.container, outputFileName) - ?: throw Exception("Failed to create file in external directory."); + val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") "video/mp4" else a.container, outputFileName) + ?: throw Exception("Failed to create file in external directory."); Logger.i(TAG, "Copying audio."); diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index bc01b4a6..916bc74c 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -37,10 +37,7 @@ class HLS { if (playlist is HlsMediaPlaylist) { independentSegments = playlist.hasIndependentSegments if (isAudioSource == true) { - val firstSegmentUrlFile = - Uri.parse(playlist.segments[0].initializationSegment?.url ?: playlist.segments[0].url).buildUpon().clearQuery().fragment(null) - .build().toString() - mediaRenditions.add(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null, URLConnection.guessContentTypeFromName(firstSegmentUrlFile))) + mediaRenditions.add(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null)) } else { variantPlaylists.add(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null))) } @@ -302,7 +299,6 @@ class HLS { val isDefault: Boolean?, val isAutoSelect: Boolean?, val isForced: Boolean?, - val container: String? = null, ) { fun toM3U8Line(): String = buildString { append("#EXT-X-MEDIA:") @@ -372,7 +368,7 @@ class HLS { val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ") return@mapNotNull when (it.type) { - "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", it.container ?: "", it.language ?: "", null, false, it.uri) + "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri) else -> null } } From 4bc561ceab6319ac42409aafa629664b9b595931 Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 10 Feb 2025 22:21:06 -0600 Subject: [PATCH 007/330] fix HLS audio download and download audio only Changelog: changed --- .../futo/platformplayer/UISlideOverlays.kt | 2 +- .../platformplayer/downloads/VideoDownload.kt | 12 ++- .../platformplayer/downloads/VideoExport.kt | 2 +- .../com/futo/platformplayer/parsers/HLS.kt | 78 +++++++++++++------ 4 files changed, 62 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index f1d42838..ff587dd5 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -292,7 +292,7 @@ class UISlideOverlays { val masterPlaylist: HLS.MasterPlaylist try { - masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl) + masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl, source is IHLSManifestAudioSource) masterPlaylist.getAudioSources().forEach { it -> diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index cd4ae885..0445da00 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -630,10 +630,8 @@ class VideoDownload { private suspend fun combineSegments(context: Context, segmentFiles: List, targetFile: File) = withContext(Dispatchers.IO) { suspendCancellableCoroutine { continuation -> - val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt") - fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" }) - - val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\"" + val cmd = + "-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\"" val statisticsCallback = StatisticsCallback { _ -> //TODO: Show progress? @@ -643,7 +641,6 @@ class VideoDownload { val session = FFmpegKit.executeAsync(cmd, { session -> if (ReturnCode.isSuccess(session.returnCode)) { - fileList.delete() continuation.resumeWith(Result.success(Unit)) } else { val errorMessage = if (ReturnCode.isCancel(session.returnCode)) { @@ -651,7 +648,6 @@ class VideoDownload { } else { "Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}" } - fileList.delete() continuation.resumeWithException(RuntimeException(errorMessage)) } }, @@ -1160,8 +1156,10 @@ class VideoDownload { fun audioContainerToExtension(container: String): String { if (container.contains("audio/mp4")) return "mp4a"; + else if (container.contains("video/mp4")) + return "mp4"; else if (container.contains("audio/mpeg")) - return "mpga"; + return "mp3"; else if (container.contains("audio/mp3")) return "mp3"; else if (container.contains("audio/webm")) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt index 7c1c4e09..7ebb70ff 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt @@ -81,7 +81,7 @@ class VideoExport { outputFile = f; } else if (a != null) { val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container); - val f = downloadRoot.createFile(a.container, outputFileName) + val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") a.codec else a.container, outputFileName) ?: throw Exception("Failed to create file in external directory."); Logger.i(TAG, "Copying audio."); diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index 9d1a3faa..bc01b4a6 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -1,5 +1,11 @@ package com.futo.platformplayer.parsers +import android.net.Uri +import androidx.annotation.OptIn +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory +import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist +import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource @@ -7,13 +13,20 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.toYesNo import com.futo.platformplayer.yesNoToBoolean +import java.io.ByteArrayInputStream import java.net.URI +import java.net.URLConnection import java.time.ZonedDateTime import java.time.format.DateTimeFormatter class HLS { companion object { - fun parseMasterPlaylist(masterPlaylistContent: String, sourceUrl: String): MasterPlaylist { + @OptIn(UnstableApi::class) + fun parseMasterPlaylist(masterPlaylistContent: String, sourceUrl: String, isAudioSource: Boolean? = null): MasterPlaylist { + val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray()) + val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser() + .parse(Uri.parse(sourceUrl), inputStream) + val baseUrl = URI(sourceUrl).resolve("./").toString() val variantPlaylists = mutableListOf() @@ -21,27 +34,39 @@ class HLS { val sessionDataList = mutableListOf() var independentSegments = false - masterPlaylistContent.lines().forEachIndexed { index, line -> - when { - line.startsWith("#EXT-X-STREAM-INF") -> { - val nextLine = masterPlaylistContent.lines().getOrNull(index + 1) - ?: throw Exception("Expected URI following #EXT-X-STREAM-INF, found none") - val url = resolveUrl(baseUrl, nextLine) + if (playlist is HlsMediaPlaylist) { + independentSegments = playlist.hasIndependentSegments + if (isAudioSource == true) { + val firstSegmentUrlFile = + Uri.parse(playlist.segments[0].initializationSegment?.url ?: playlist.segments[0].url).buildUpon().clearQuery().fragment(null) + .build().toString() + mediaRenditions.add(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null, URLConnection.guessContentTypeFromName(firstSegmentUrlFile))) + } else { + variantPlaylists.add(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null))) + } + } else if (playlist is HlsMultivariantPlaylist) { + masterPlaylistContent.lines().forEachIndexed { index, line -> + when { + line.startsWith("#EXT-X-STREAM-INF") -> { + val nextLine = masterPlaylistContent.lines().getOrNull(index + 1) + ?: throw Exception("Expected URI following #EXT-X-STREAM-INF, found none") + val url = resolveUrl(baseUrl, nextLine) - variantPlaylists.add(VariantPlaylistReference(url, parseStreamInfo(line))) - } + variantPlaylists.add(VariantPlaylistReference(url, parseStreamInfo(line))) + } - line.startsWith("#EXT-X-MEDIA") -> { - mediaRenditions.add(parseMediaRendition(line, baseUrl)) - } + line.startsWith("#EXT-X-MEDIA") -> { + mediaRenditions.add(parseMediaRendition(line, baseUrl)) + } - line == "#EXT-X-INDEPENDENT-SEGMENTS" -> { - independentSegments = true - } + line == "#EXT-X-INDEPENDENT-SEGMENTS" -> { + independentSegments = true + } - line.startsWith("#EXT-X-SESSION-DATA") -> { - val sessionData = parseSessionData(line) - sessionDataList.add(sessionData) + line.startsWith("#EXT-X-SESSION-DATA") -> { + val sessionData = parseSessionData(line) + sessionDataList.add(sessionData) + } } } } @@ -61,7 +86,13 @@ class HLS { val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":") val streamInfo = lines.find { it.startsWith("#EXT-X-STREAM-INF:") }?.let { parseStreamInfo(it) } + val initSegment = + lines.find { it.startsWith("#EXT-X-MAP:") }?.substringAfter(":")?.split(",")?.get(0) + ?.substringAfter("=")?.trim('"') val segments = mutableListOf() + if (initSegment != null) { + segments.add(MediaSegment(0.0, resolveUrl(sourceUrl, initSegment))) + } var currentSegment: MediaSegment? = null lines.forEach { line -> when { @@ -109,10 +140,10 @@ class HLS { } } - fun parseAndGetAudioSources(source: Any, content: String, url: String): List { + fun parseAndGetAudioSources(source: Any, content: String, url: String, isAudioSource: Boolean? = null): List { val masterPlaylist: MasterPlaylist try { - masterPlaylist = parseMasterPlaylist(content, url) + masterPlaylist = parseMasterPlaylist(content, url, isAudioSource) return masterPlaylist.getAudioSources() } catch (e: Throwable) { if (content.lines().any { it.startsWith("#EXTINF:") }) { @@ -270,7 +301,8 @@ class HLS { val name: String?, val isDefault: Boolean?, val isAutoSelect: Boolean?, - val isForced: Boolean? + val isForced: Boolean?, + val container: String? = null, ) { fun toM3U8Line(): String = buildString { append("#EXT-X-MEDIA:") @@ -340,7 +372,7 @@ class HLS { val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ") return@mapNotNull when (it.type) { - "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri) + "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", it.container ?: "", it.language ?: "", null, false, it.uri) else -> null } } @@ -376,7 +408,7 @@ class HLS { val programDateTime: ZonedDateTime?, val playlistType: String?, val streamInfo: StreamInfo?, - val segments: List + val segments: List, ) { fun buildM3U8(): String = buildString { append("#EXTM3U\n") From ca781dfe153d3b8ed11b2d3d86c0f5c282442b67 Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 11 Feb 2025 10:31:47 -0600 Subject: [PATCH 008/330] restore hard code HLS as mp4 Changelog: changed --- .../com/futo/platformplayer/downloads/VideoDownload.kt | 2 +- .../java/com/futo/platformplayer/downloads/VideoExport.kt | 6 +++--- app/src/main/java/com/futo/platformplayer/parsers/HLS.kt | 8 ++------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 0445da00..f845cf92 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -1159,7 +1159,7 @@ class VideoDownload { else if (container.contains("video/mp4")) return "mp4"; else if (container.contains("audio/mpeg")) - return "mp3"; + return "mpga"; else if (container.contains("audio/mp3")) return "mp3"; else if (container.contains("audio/webm")) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt index 7ebb70ff..6761168c 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt @@ -69,7 +69,7 @@ class VideoExport { outputFile = f; } else if (v != null) { val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container); - val f = downloadRoot.createFile(v.container, outputFileName) + val f = downloadRoot.createFile(if (v.container == "application/vnd.apple.mpegurl") "video/mp4" else v.container, outputFileName) ?: throw Exception("Failed to create file in external directory."); Logger.i(TAG, "Copying video."); @@ -81,8 +81,8 @@ class VideoExport { outputFile = f; } else if (a != null) { val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container); - val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") a.codec else a.container, outputFileName) - ?: throw Exception("Failed to create file in external directory."); + val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") "video/mp4" else a.container, outputFileName) + ?: throw Exception("Failed to create file in external directory."); Logger.i(TAG, "Copying audio."); diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index bc01b4a6..916bc74c 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -37,10 +37,7 @@ class HLS { if (playlist is HlsMediaPlaylist) { independentSegments = playlist.hasIndependentSegments if (isAudioSource == true) { - val firstSegmentUrlFile = - Uri.parse(playlist.segments[0].initializationSegment?.url ?: playlist.segments[0].url).buildUpon().clearQuery().fragment(null) - .build().toString() - mediaRenditions.add(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null, URLConnection.guessContentTypeFromName(firstSegmentUrlFile))) + mediaRenditions.add(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null)) } else { variantPlaylists.add(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null))) } @@ -302,7 +299,6 @@ class HLS { val isDefault: Boolean?, val isAutoSelect: Boolean?, val isForced: Boolean?, - val container: String? = null, ) { fun toM3U8Line(): String = buildString { append("#EXT-X-MEDIA:") @@ -372,7 +368,7 @@ class HLS { val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ") return@mapNotNull when (it.type) { - "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", it.container ?: "", it.language ?: "", null, false, it.uri) + "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri) else -> null } } From 8839d9f1c684b1b6a63e2e577514aa8862146053 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 12 Feb 2025 16:31:30 +0100 Subject: [PATCH 009/330] Fix for misisng exports for export playlist --- .../platformplayer/states/StateDownloads.kt | 35 +++++++++++++------ .../platformplayer/stores/v2/ManagedStore.kt | 11 ++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt index 5bba7e77..e82cd0da 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt @@ -48,6 +48,17 @@ class StateDownloads { private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath); private val _downloaded = FragmentedStorage.storeJson("downloaded") + .withOnModified({ + synchronized(_downloadedSet) { + if(!_downloadedSet.contains(it.id)) + _downloadedSet.add(it.id); + } + }, { + synchronized(_downloadedSet) { + if(_downloadedSet.contains(it.id)) + _downloadedSet.remove(it.id); + } + }) .load() .apply { afterLoadingDownloaded(this) }; private val _downloading = FragmentedStorage.storeJson("downloading") @@ -87,9 +98,6 @@ class StateDownloads { Logger.i("StateDownloads", "Deleting local video ${id.value}"); val downloaded = getCachedVideo(id); if(downloaded != null) { - synchronized(_downloadedSet) { - _downloadedSet.remove(id); - } _downloaded.delete(downloaded); } onDownloadedChanged.emit(); @@ -263,9 +271,6 @@ class StateDownloads { if(existing.groupID == null) { existing.groupID = VideoDownload.GROUP_WATCHLATER; existing.groupType = VideoDownload.GROUP_WATCHLATER; - synchronized(_downloadedSet) { - _downloadedSet.add(existing.id); - } _downloaded.save(existing); } } @@ -308,9 +313,6 @@ class StateDownloads { if(existing.groupID == null) { existing.groupID = playlist.id; existing.groupType = VideoDownload.GROUP_PLAYLIST; - synchronized(_downloadedSet) { - _downloadedSet.add(existing.id); - } _downloaded.save(existing); } } @@ -476,7 +478,16 @@ class StateDownloads { val root = DocumentFile.fromTreeUri(context, it!!); - val localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId) + val playlist = StatePlaylists.instance.getPlaylist(playlistId); + var localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId); + if(playlist != null) { + val missing = playlist.videos + .filter { vid -> !localVideos.any { it.id.value == null || it.id.value == vid.id.value } } + .map { getCachedVideo(it.id) } + .filterNotNull(); + if(missing.size > 0) + localVideos = localVideos + missing; + }; var lastNotifyTime = -1L; @@ -484,6 +495,7 @@ class StateDownloads { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { it.setText("Exporting videos.."); var i = 0; + var success = 0; for (video in localVideos) { withContext(Dispatchers.Main) { it.setText("Exporting videos...(${i}/${localVideos.size})"); @@ -501,6 +513,7 @@ class StateDownloads { lastNotifyTime = now; } }, root); + success++; } catch(ex: Throwable) { Logger.e(TAG, "Failed export [${video.name}]: ${ex.message}", ex); } @@ -509,7 +522,7 @@ class StateDownloads { withContext(Dispatchers.Main) { it.setProgress(1f); it.dismiss(); - UIDialogs.appToast("Finished exporting playlist"); + UIDialogs.appToast("Finished exporting playlist (${success} videos${if(i < success) ", ${i} errors" else ""})"); } }; } diff --git a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt index cb10cd20..90e79ccc 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt @@ -33,6 +33,9 @@ class ManagedStore{ val className: String? get() = _class.classifier?.assume>()?.simpleName; + private var _onModificationCreate: ((T) -> Unit)? = null; + private var _onModificationDelete: ((T) -> Unit)? = null; + val name: String; constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer, niceName: String? = null) { @@ -62,6 +65,12 @@ class ManagedStore{ return this; } + fun withOnModified(created: (T)->Unit, deleted: (T)->Unit): ManagedStore { + _onModificationCreate = created; + _onModificationDelete = deleted; + return this; + } + fun load(): ManagedStore { synchronized(_files) { _files.clear(); @@ -265,6 +274,7 @@ class ManagedStore{ file = saveNew(obj); if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction)) saveReconstruction(file, obj); + _onModificationCreate?.invoke(obj) } } } @@ -300,6 +310,7 @@ class ManagedStore{ _files.remove(item); Logger.v(TAG, "Deleting file ${logName(file.id)}"); file.delete(); + _onModificationDelete?.invoke(item) } } } From 83843f192da2238cffee2ff76d90e258db2d4f10 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 12 Feb 2025 18:43:15 +0100 Subject: [PATCH 010/330] Show total downloaded content duration, Indicator how many subscriptions, save queue as playlist --- .../futo/platformplayer/UISlideOverlays.kt | 30 +++++++++++++++++++ .../mainactivity/main/CreatorsFragment.kt | 9 +++++- .../mainactivity/main/DownloadsFragment.kt | 3 +- .../futo/platformplayer/states/StatePlayer.kt | 7 +++++ .../views/adapters/SubscriptionAdapter.kt | 6 +++- .../views/overlays/QueueEditorOverlay.kt | 21 +++++++++++++ app/src/main/res/layout/fragment_creators.xml | 11 ++++++- app/src/main/res/layout/overlay_queue.xml | 15 ++++++++++ 8 files changed, 98 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 849d1b8c..382f14de 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -79,6 +79,36 @@ class UISlideOverlays { return menu; } + fun showQueueOptionsOverlay(context: Context, container: ViewGroup) { + UISlideOverlays.showOverlay(container, "Queue options", null, { + + }, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, { + val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name)); + val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput); + + addPlaylistOverlay.onOK.subscribe { + val text = nameInput.text.trim() + if (text.isBlank()) { + return@subscribe; + } + + addPlaylistOverlay.hide(); + nameInput.deactivate(); + nameInput.clear(); + StatePlayer.instance.saveQueueAsPlaylist(text); + UIDialogs.appToast("Playlist [${text}] created"); + }; + + addPlaylistOverlay.onCancel.subscribe { + nameInput.deactivate(); + nameInput.clear(); + }; + + addPlaylistOverlay.show(); + nameInput.activate(); + }, false)); + } + fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay { val items = arrayListOf(); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorsFragment.kt index 6efc7a3d..54649ebf 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorsFragment.kt @@ -10,6 +10,7 @@ import android.widget.EditText import android.widget.FrameLayout import android.widget.ImageButton import android.widget.Spinner +import android.widget.TextView import androidx.core.widget.addTextChangedListener import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -26,6 +27,7 @@ class CreatorsFragment : MainFragment() { private var _overlayContainer: FrameLayout? = null; private var _containerSearch: FrameLayout? = null; private var _editSearch: EditText? = null; + private var _textMeta: TextView? = null; private var _buttonClearSearch: ImageButton? = null override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -34,6 +36,7 @@ class CreatorsFragment : MainFragment() { val editSearch: EditText = view.findViewById(R.id.edit_search); val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search) _editSearch = editSearch + _textMeta = view.findViewById(R.id.text_meta); _buttonClearSearch = buttonClearSearch buttonClearSearch.setOnClickListener { editSearch.text.clear() @@ -41,7 +44,11 @@ class CreatorsFragment : MainFragment() { _buttonClearSearch?.visibility = View.INVISIBLE; } - val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)); + val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs -> + _textMeta?.let { + it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}"; + } + }; adapter.onClick.subscribe { platformUser -> navigate(platformUser) }; adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt index 7995d543..440aa235 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt @@ -22,6 +22,7 @@ import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.toHumanBytesSize +import com.futo.platformplayer.toHumanDuration import com.futo.platformplayer.views.AnyInsertedAdapterView import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder @@ -215,7 +216,7 @@ class DownloadsFragment : MainFragment() { _listDownloadedHeader.visibility = GONE; } else { _listDownloadedHeader.visibility = VISIBLE; - _listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})"; + _listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()}${if(downloaded.size > 0) ", ${downloaded.sumOf { it.duration }.toHumanDuration(false)}" else ""})"; } lastDownloads = downloaded; diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt index 286941c4..b8368ea5 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt @@ -13,6 +13,7 @@ import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails +import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.logging.Logger @@ -130,6 +131,12 @@ class StatePlayer { closeMediaSession(); } + fun saveQueueAsPlaylist(name: String){ + val videos = _queue.toList(); + val playlist = Playlist(name, videos.map { SerializedPlatformVideo.fromVideo(it) }); + StatePlaylists.instance.createOrUpdatePlaylist(playlist); + } + //Notifications fun hasMediaSession() : Boolean { return MediaPlaybackService.getService() != null; diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt index e3644cc3..ef3f7cb0 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt @@ -16,6 +16,7 @@ class SubscriptionAdapter : RecyclerView.Adapter { private lateinit var _sortedDataset: List; private val _inflater: LayoutInflater; private val _confirmationMessage: String; + private val _onDatasetChanged: ((List)->Unit)?; var onClick = Event1(); var onSettings = Event1(); @@ -30,9 +31,10 @@ class SubscriptionAdapter : RecyclerView.Adapter { updateDataset(); } - constructor(inflater: LayoutInflater, confirmationMessage: String) : super() { + constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List)->Unit)? = null) : super() { _inflater = inflater; _confirmationMessage = confirmationMessage; + _onDatasetChanged = onDatasetChanged; StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper()) StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { updateDataset() } @@ -78,6 +80,8 @@ class SubscriptionAdapter : RecyclerView.Adapter { .filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) } .toList(); + _onDatasetChanged?.invoke(_sortedDataset); + notifyDataSetChanged(); } diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt index 215e8dcb..edaed188 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt @@ -2,16 +2,26 @@ package com.futo.platformplayer.views.overlays import android.content.Context import android.util.AttributeSet +import android.widget.FrameLayout +import android.widget.ImageView import android.widget.LinearLayout import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.R +import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.views.lists.VideoListEditorView +import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem +import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay +import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput class QueueEditorOverlay : LinearLayout { private val _topbar : OverlayTopbar; private val _editor : VideoListEditorView; + private val _btnSettings: ImageView; + + private val _overlayContainer: FrameLayout; + val onClose = Event0(); @@ -19,6 +29,9 @@ class QueueEditorOverlay : LinearLayout { inflate(context, R.layout.overlay_queue, this) _topbar = findViewById(R.id.topbar); _editor = findViewById(R.id.editor); + _btnSettings = findViewById(R.id.button_settings); + _overlayContainer = findViewById(R.id.overlay_container); + _topbar.onClose.subscribe(this, onClose::emit); _editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) } @@ -28,6 +41,10 @@ class QueueEditorOverlay : LinearLayout { } _editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) } + _btnSettings.setOnClickListener { + handleSettings(); + } + _topbar.setInfo(context.getString(R.string.queue), ""); } @@ -40,4 +57,8 @@ class QueueEditorOverlay : LinearLayout { fun cleanup() { _topbar.onClose.remove(this); } + + fun handleSettings() { + UISlideOverlays.showQueueOptionsOverlay(context, _overlayContainer); + } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_creators.xml b/app/src/main/res/layout/fragment_creators.xml index 62694f56..a3848565 100644 --- a/app/src/main/res/layout/fragment_creators.xml +++ b/app/src/main/res/layout/fragment_creators.xml @@ -16,7 +16,7 @@ + + diff --git a/app/src/main/res/layout/overlay_queue.xml b/app/src/main/res/layout/overlay_queue.xml index 9dc827fb..4cee7598 100644 --- a/app/src/main/res/layout/overlay_queue.xml +++ b/app/src/main/res/layout/overlay_queue.xml @@ -21,5 +21,20 @@ android:layout_height="0dp" app:layout_constraintTop_toBottomOf="@id/topbar" app:layout_constraintBottom_toBottomOf="parent" /> + + \ No newline at end of file From b9bbfb44c59b4f4606532768f98be4fcf3843fab Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 12 Feb 2025 18:53:30 +0100 Subject: [PATCH 011/330] Update submodules, fix apple podcast dir --- .gitmodules | 2 +- app/src/stable/assets/sources/apple-podcast | 1 - app/src/stable/assets/sources/apple-podcasts | 1 + app/src/stable/assets/sources/bitchute | 2 +- app/src/stable/assets/sources/peertube | 2 +- app/src/stable/assets/sources/rumble | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/apple-podcasts | 2 +- app/src/unstable/assets/sources/bitchute | 2 +- app/src/unstable/assets/sources/peertube | 2 +- app/src/unstable/assets/sources/rumble | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 12 files changed, 11 insertions(+), 11 deletions(-) delete mode 160000 app/src/stable/assets/sources/apple-podcast create mode 160000 app/src/stable/assets/sources/apple-podcasts diff --git a/.gitmodules b/.gitmodules index dad5c74e..5f6ab0dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -83,7 +83,7 @@ path = app/src/stable/assets/sources/dailymotion url = ../plugins/dailymotion.git [submodule "app/src/stable/assets/sources/apple-podcast"] - path = app/src/stable/assets/sources/apple-podcast + path = app/src/stable/assets/sources/apple-podcasts url = ../plugins/apple-podcasts.git [submodule "app/src/unstable/assets/sources/apple-podcasts"] path = app/src/unstable/assets/sources/apple-podcasts diff --git a/app/src/stable/assets/sources/apple-podcast b/app/src/stable/assets/sources/apple-podcast deleted file mode 160000 index f79c7141..00000000 --- a/app/src/stable/assets/sources/apple-podcast +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f79c7141bcb11464103abc56fd7be492fe8568ab diff --git a/app/src/stable/assets/sources/apple-podcasts b/app/src/stable/assets/sources/apple-podcasts new file mode 160000 index 00000000..090104c7 --- /dev/null +++ b/app/src/stable/assets/sources/apple-podcasts @@ -0,0 +1 @@ +Subproject commit 090104c7fa112d9772f43c7c2620e8c2cf3c9d6a diff --git a/app/src/stable/assets/sources/bitchute b/app/src/stable/assets/sources/bitchute index 8d7c0e25..7f869aa4 160000 --- a/app/src/stable/assets/sources/bitchute +++ b/app/src/stable/assets/sources/bitchute @@ -1 +1 @@ -Subproject commit 8d7c0e252738450f2a8bb2a48e9f8bdc24cfea54 +Subproject commit 7f869aa4b117214095feb367d38414402cd08417 diff --git a/app/src/stable/assets/sources/peertube b/app/src/stable/assets/sources/peertube index cfabdc97..20fd03d9 160000 --- a/app/src/stable/assets/sources/peertube +++ b/app/src/stable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit cfabdc97ab435822c44b0135b3b76519327ba05a +Subproject commit 20fd03d9847b308d81ce474144bee79b04385477 diff --git a/app/src/stable/assets/sources/rumble b/app/src/stable/assets/sources/rumble index 670cbc04..b9e6259f 160000 --- a/app/src/stable/assets/sources/rumble +++ b/app/src/stable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 670cbc043e8901026c43e1a2e4ac44e12e32143b +Subproject commit b9e6259f4eedd41d1838506d32d240e177c1feff diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 15d3391a..65524663 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 15d3391a5d091405b0c9bd92ff87ebcf2f6944eb +Subproject commit 655246634f4aac712c2cc26c8abc1bc29001e3d8 diff --git a/app/src/unstable/assets/sources/apple-podcasts b/app/src/unstable/assets/sources/apple-podcasts index f79c7141..090104c7 160000 --- a/app/src/unstable/assets/sources/apple-podcasts +++ b/app/src/unstable/assets/sources/apple-podcasts @@ -1 +1 @@ -Subproject commit f79c7141bcb11464103abc56fd7be492fe8568ab +Subproject commit 090104c7fa112d9772f43c7c2620e8c2cf3c9d6a diff --git a/app/src/unstable/assets/sources/bitchute b/app/src/unstable/assets/sources/bitchute index 8d7c0e25..7f869aa4 160000 --- a/app/src/unstable/assets/sources/bitchute +++ b/app/src/unstable/assets/sources/bitchute @@ -1 +1 @@ -Subproject commit 8d7c0e252738450f2a8bb2a48e9f8bdc24cfea54 +Subproject commit 7f869aa4b117214095feb367d38414402cd08417 diff --git a/app/src/unstable/assets/sources/peertube b/app/src/unstable/assets/sources/peertube index cfabdc97..20fd03d9 160000 --- a/app/src/unstable/assets/sources/peertube +++ b/app/src/unstable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit cfabdc97ab435822c44b0135b3b76519327ba05a +Subproject commit 20fd03d9847b308d81ce474144bee79b04385477 diff --git a/app/src/unstable/assets/sources/rumble b/app/src/unstable/assets/sources/rumble index 670cbc04..b9e6259f 160000 --- a/app/src/unstable/assets/sources/rumble +++ b/app/src/unstable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 670cbc043e8901026c43e1a2e4ac44e12e32143b +Subproject commit b9e6259f4eedd41d1838506d32d240e177c1feff diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 2c816009..65524663 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 2c816009f7a09ceb79a707654edbb01e7fb7a3a4 +Subproject commit 655246634f4aac712c2cc26c8abc1bc29001e3d8 From 1dfe18aa6fe8fa9b50af081f59d783e6c67d5a3b Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 12 Feb 2025 18:58:01 +0100 Subject: [PATCH 012/330] Add Apple podcasts --- app/src/stable/res/raw/plugin_config.json | 3 ++- app/src/unstable/res/raw/plugin_config.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/stable/res/raw/plugin_config.json b/app/src/stable/res/raw/plugin_config.json index 3b7dacec..d98fc987 100644 --- a/app/src/stable/res/raw/plugin_config.json +++ b/app/src/stable/res/raw/plugin_config.json @@ -12,7 +12,8 @@ "cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json", "4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json", "9c87e8db-e75d-48f4-afe5-2d203d4b95c5": "sources/dailymotion/build/DailymotionConfig.json", - "e8b1ad5f-0c6d-497d-a5fa-0a785a16d902": "sources/bitchute/BitchuteConfig.json" + "e8b1ad5f-0c6d-497d-a5fa-0a785a16d902": "sources/bitchute/BitchuteConfig.json", + "89ae4889-0420-4d16-ad6c-19c776b28f99": "sources/apple-podcasts/ApplePodcastsConfig.json" }, "SOURCES_EMBEDDED_DEFAULT": [ "35ae969a-a7db-11ed-afa1-0242ac120002" diff --git a/app/src/unstable/res/raw/plugin_config.json b/app/src/unstable/res/raw/plugin_config.json index 4e4cc1dc..cfbf3e87 100644 --- a/app/src/unstable/res/raw/plugin_config.json +++ b/app/src/unstable/res/raw/plugin_config.json @@ -12,7 +12,8 @@ "cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json", "4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json", "9c87e8db-e75d-48f4-afe5-2d203d4b95c5": "sources/dailymotion/build/DailymotionConfig.json", - "e8b1ad5f-0c6d-497d-a5fa-0a785a16d902": "sources/bitchute/BitchuteConfig.json" + "e8b1ad5f-0c6d-497d-a5fa-0a785a16d902": "sources/bitchute/BitchuteConfig.json", + "89ae4889-0420-4d16-ad6c-19c776b28f99": "sources/apple-podcasts/ApplePodcastsConfig.json" }, "SOURCES_EMBEDDED_DEFAULT": [ "35ae969a-a7db-11ed-afa1-0242ac120002" From 36c51f1a0c1d6a30ef8e708585fe2d2dbf7ecd6e Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 12 Feb 2025 19:06:43 +0100 Subject: [PATCH 013/330] Refs --- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 65524663..8f8774a7 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 655246634f4aac712c2cc26c8abc1bc29001e3d8 +Subproject commit 8f8774a782aa49889774920688de371f28317ca6 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 65524663..8f8774a7 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 655246634f4aac712c2cc26c8abc1bc29001e3d8 +Subproject commit 8f8774a782aa49889774920688de371f28317ca6 From 2f0ba1b1f7ceb0342f7cb30b8dcb6508979c827c Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 12 Feb 2025 19:17:20 +0100 Subject: [PATCH 014/330] Setting to check disabled plugins for updates (off by default) --- app/src/main/java/com/futo/platformplayer/Settings.kt | 3 +++ .../main/java/com/futo/platformplayer/states/StatePlugins.kt | 3 +++ app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 8 insertions(+) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 0a62e28f..c95947ea 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -644,6 +644,9 @@ class Settings : FragmentedStorageFileJson() { @Serializable class Plugins { + @FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1) + var checkDisabledPluginsForUpdates: Boolean = false; + @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) var clearCookiesOnLogout: Boolean = true; diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt index 02154677..3506bc54 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.states import android.content.Context import com.futo.platformplayer.R +import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.LoginActivity import com.futo.platformplayer.api.http.ManagedHttpClient @@ -101,6 +102,8 @@ class StatePlugins { if (availableClient !is JSClient) { continue } + if(!Settings.instance.plugins.checkDisabledPluginsForUpdates && StatePlatform.instance.isClientEnabled(availableClient.id)) + continue; val newConfig = checkForUpdates(availableClient.config); if (newConfig != null) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e746d8fc..e7026f7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -286,6 +286,8 @@ Also removes any data related plugin like login or settings Announcement Notifications + Check disabled plugins for updates + Check disabled plugins for updates Planned Content Notifications Schedules discovered planned content as notifications, resulting in more accurate notifications for this content. Attempt to utilize byte ranges From 44c8800bec12293168afc19f5ba90708ada7c611 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 12 Feb 2025 19:25:29 +0100 Subject: [PATCH 015/330] plugin disabled update check fix --- .../main/java/com/futo/platformplayer/states/StatePlugins.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt index 3506bc54..cbb7b4d4 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt @@ -102,7 +102,7 @@ class StatePlugins { if (availableClient !is JSClient) { continue } - if(!Settings.instance.plugins.checkDisabledPluginsForUpdates && StatePlatform.instance.isClientEnabled(availableClient.id)) + if(!Settings.instance.plugins.checkDisabledPluginsForUpdates && !StatePlatform.instance.isClientEnabled(availableClient.id)) continue; val newConfig = checkForUpdates(availableClient.config); From 157d5b4c3659f6b1cd40f99c03e86248c70c4525 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 12 Feb 2025 20:03:33 +0100 Subject: [PATCH 016/330] Fix container id conflict --- .../futo/platformplayer/views/overlays/QueueEditorOverlay.kt | 2 +- app/src/main/res/layout/overlay_queue.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt index edaed188..a7181e90 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt @@ -30,7 +30,7 @@ class QueueEditorOverlay : LinearLayout { _topbar = findViewById(R.id.topbar); _editor = findViewById(R.id.editor); _btnSettings = findViewById(R.id.button_settings); - _overlayContainer = findViewById(R.id.overlay_container); + _overlayContainer = findViewById(R.id.overlay_container_queue); _topbar.onClose.subscribe(this, onClose::emit); diff --git a/app/src/main/res/layout/overlay_queue.xml b/app/src/main/res/layout/overlay_queue.xml index 4cee7598..904372ea 100644 --- a/app/src/main/res/layout/overlay_queue.xml +++ b/app/src/main/res/layout/overlay_queue.xml @@ -33,7 +33,7 @@ app:srcCompat="@drawable/ic_settings" /> From 8b7c9df286d56f923736da30849f1812901ab5b3 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 12 Feb 2025 20:16:50 +0100 Subject: [PATCH 017/330] Add to queue button on recommendations, no toast on add to watch later if dup --- .../main/java/com/futo/platformplayer/UISlideOverlays.kt | 5 +++-- .../fragment/mainactivity/main/ChannelFragment.kt | 4 ++-- .../fragment/mainactivity/main/ContentFeedView.kt | 4 ++-- .../fragment/mainactivity/main/VideoDetailView.kt | 9 +++++++-- .../com/futo/platformplayer/states/StatePlaylists.kt | 6 +++++- .../views/adapters/feedtypes/PreviewVideoView.kt | 2 +- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 382f14de..67497b1e 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -1075,8 +1075,9 @@ class UISlideOverlays { StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), tag = "watch later", - call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); - UIDialogs.appToast("Added to watch later", false); + call = { + if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true)) + UIDialogs.appToast("Added to watch later", false); }), ) ); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index 8bea629a..817a8ca2 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -238,8 +238,8 @@ class ChannelFragment : MainFragment() { } adapter.onAddToWatchLaterClicked.subscribe { content -> if (content is IPlatformVideo) { - StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true) - UIDialogs.toast("Added to watch later\n[${content.name}]") + if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)) + UIDialogs.toast("Added to watch later\n[${content.name}]") } } adapter.onUrlClicked.subscribe { url -> diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt index 04a51189..4390a80c 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt @@ -82,8 +82,8 @@ abstract class ContentFeedView : FeedView Date: Thu, 13 Feb 2025 21:00:02 +0100 Subject: [PATCH 018/330] Remove accidental always update --- .../fragment/mainactivity/main/SourceDetailFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt index 1bdedfcd..1b621c03 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt @@ -556,7 +556,7 @@ class SourceDetailFragment : MainFragment() { Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}"); val config = SourcePluginConfig.fromJson(configJson); - if (config.version <= c.version && config.name != "Youtube") { + if (config.version <= c.version) { Logger.i(TAG, "Plugin is up to date."); withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); }; return@launch; From 3cd4b4503f0fc89074d5281bc643c9bcd96d77f9 Mon Sep 17 00:00:00 2001 From: Kai Date: Wed, 19 Feb 2025 11:59:59 -0600 Subject: [PATCH 019/330] fix other cookie handling Changelog: changed --- .../api/media/platforms/js/internal/JSHttpClient.kt | 8 +++++++- .../futo/platformplayer/engine/packages/PackageHttp.kt | 7 ++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt index da10e2ec..6f835304 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt @@ -127,7 +127,7 @@ class JSHttpClient : ManagedHttpClient { } if(doApplyCookies) { - if (_currentCookieMap.isNotEmpty()) { + if (_currentCookieMap.isNotEmpty() || _otherCookieMap.isNotEmpty()) { val cookiesToApply = hashMapOf(); synchronized(_currentCookieMap) { for(cookie in _currentCookieMap @@ -135,6 +135,12 @@ class JSHttpClient : ManagedHttpClient { .flatMap { it.value.toList() }) cookiesToApply[cookie.first] = cookie.second; }; + synchronized(_otherCookieMap) { + for(cookie in _otherCookieMap + .filter { domain.matchesDomain(it.key) } + .flatMap { it.value.toList() }) + cookiesToApply[cookie.first] = cookie.second; + } if(cookiesToApply.size > 0) { val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; "); diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt index 686318a1..6f131b4d 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt @@ -126,9 +126,9 @@ class PackageHttp: V8Package { @V8Function fun GET(url: String, headers: MutableMap = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse { return if(useAuth) - _packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING) + _packageClientAuth.GET(url, headers, useByteResponse) else - _packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING); + _packageClient.GET(url, headers, useByteResponse); } @V8Function fun POST(url: String, body: Any, headers: MutableMap = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse { @@ -385,7 +385,8 @@ class PackageHttp: V8Package { } @V8Function - fun GET(url: String, headers: MutableMap = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { + fun GET(url: String, headers: MutableMap = HashMap(), useByteResponse: Boolean = false) : IBridgeHttpResponse { + val returnType: ReturnType = if(useByteResponse) ReturnType.BYTES else ReturnType.STRING applyDefaultHeaders(headers); return logExceptions { catchHttp { From 7ffa6b1bb318d30ca42ad37771ba9289d057a97c Mon Sep 17 00:00:00 2001 From: Kai Date: Wed, 19 Feb 2025 12:32:17 -0600 Subject: [PATCH 020/330] revert params Changelog: changed --- .../com/futo/platformplayer/engine/packages/PackageHttp.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt index 6f131b4d..686318a1 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt @@ -126,9 +126,9 @@ class PackageHttp: V8Package { @V8Function fun GET(url: String, headers: MutableMap = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse { return if(useAuth) - _packageClientAuth.GET(url, headers, useByteResponse) + _packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING) else - _packageClient.GET(url, headers, useByteResponse); + _packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING); } @V8Function fun POST(url: String, body: Any, headers: MutableMap = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse { @@ -385,8 +385,7 @@ class PackageHttp: V8Package { } @V8Function - fun GET(url: String, headers: MutableMap = HashMap(), useByteResponse: Boolean = false) : IBridgeHttpResponse { - val returnType: ReturnType = if(useByteResponse) ReturnType.BYTES else ReturnType.STRING + fun GET(url: String, headers: MutableMap = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { applyDefaultHeaders(headers); return logExceptions { catchHttp { From 78f516988074f5a45a1bd3596e42ea165f006a47 Mon Sep 17 00:00:00 2001 From: Kai Date: Thu, 20 Feb 2025 14:43:13 -0600 Subject: [PATCH 021/330] add recommendations assignment in video details class Changelog: added --- app/src/main/assets/scripts/source.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/assets/scripts/source.js b/app/src/main/assets/scripts/source.js index 0c87cac4..b6b4ab6d 100644 --- a/app/src/main/assets/scripts/source.js +++ b/app/src/main/assets/scripts/source.js @@ -263,6 +263,10 @@ class PlatformVideoDetails extends PlatformVideo { this.rating = obj.rating ?? null; //IRating this.subtitles = obj.subtitles ?? []; this.isShort = !!obj.isShort ?? false; + + if (obj.getContentRecommendations) { + this.getContentRecommendations = obj.getContentRecommendations + } } } From b5ac8b3ec6d94a2af55d587a1993a1ea268bc8a0 Mon Sep 17 00:00:00 2001 From: Kai DeLorenzo Date: Thu, 20 Feb 2025 21:59:29 +0000 Subject: [PATCH 022/330] Edit Authentication.md --- docs/Authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Authentication.md b/docs/Authentication.md index f21581a1..f54eced5 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -8,7 +8,7 @@ The goal of the authentication system is to provide plugins the ability to make > >You should always only login (and install for that matter) plugins you trust. -How to actually use the authenticated client is described in the Http package documentation (See [Package: Http](_blank)). +How to actually use the authenticated client is described in the Http package documentation (See [Package: Http](docs/packages/packageHttp.md)). This documentation will exclusively focus on configuring authentication and how it behaves. ## How it works @@ -58,5 +58,5 @@ Headers are exclusively applied to the domains they are retrieved from. A plugin By default, when authentication requests are made, the authenticated client will behave similar to that of a normal browser. Meaning that if the server you are communicating with sets new cookies, the client will use those cookies instead. These new cookies are NOT saved to disk, meaning that whenever that plugin reloads the cookies will revert to those assigned at login. This behavior can be modified by using custom http clients as described in the http package documentation. - (See [Package: Http](_blank)) + (See [Package: Http](docs/packages/packageHttp.md)) From 9014fb581dc780732f606f4723f0dfae86234f5d Mon Sep 17 00:00:00 2001 From: Kai Date: Thu, 20 Feb 2025 16:07:35 -0600 Subject: [PATCH 023/330] add support for downloading encrypted HLS streams Changelog: changed --- .../platformplayer/downloads/VideoDownload.kt | 63 +++++++++++++++++-- .../com/futo/platformplayer/parsers/HLS.kt | 30 ++++++++- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index ede24707..10d48130 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -47,6 +47,7 @@ import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.toHumanBitrate import com.futo.platformplayer.toHumanBytesSpeed +import com.futo.polycentric.core.hexStringToByteArray import hasAnySource import isDownloadable import kotlinx.coroutines.CancellationException @@ -59,6 +60,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.serialization.Contextual import kotlinx.serialization.Transient +import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -69,6 +71,9 @@ import java.util.concurrent.Executors import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinTask import java.util.concurrent.ThreadLocalRandom +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec import kotlin.coroutines.resumeWithException import kotlin.time.times @@ -564,6 +569,14 @@ class VideoDownload { } } + private fun decryptSegment(encryptedSegment: ByteArray, key: ByteArray, iv: ByteArray): ByteArray { + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + val secretKey = SecretKeySpec(key, "AES") + val ivSpec = IvParameterSpec(iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) + return cipher.doFinal(encryptedSegment) + } + private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long { if(targetFile.exists()) targetFile.delete(); @@ -579,6 +592,14 @@ class VideoDownload { ?: throw Exception("Variant playlist content is empty") val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl) + val decryptionInfo: DecryptionInfo? = if (variantPlaylist.decryptionInfo != null) { + val keyResponse = client.get(variantPlaylist.decryptionInfo.keyUrl) + check(keyResponse.isOk) { "HLS request failed for decryption key: ${keyResponse.code}" } + DecryptionInfo(keyResponse.body!!.bytes(), variantPlaylist.decryptionInfo.iv.hexStringToByteArray()) + } else { + null + } + variantPlaylist.segments.forEachIndexed { index, segment -> if (segment !is HLS.MediaSegment) { return@forEachIndexed @@ -590,7 +611,7 @@ class VideoDownload { try { segmentFiles.add(segmentFile) - val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri) { segmentLength, totalRead, lastSpeed -> + val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri, if (index == 0) null else decryptionInfo) { segmentLength, totalRead, lastSpeed -> val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed) @@ -771,7 +792,7 @@ class VideoDownload { else { Logger.i(TAG, "Download $name Sequential"); try { - sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress); + sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, null, onProgress); } catch (e: Throwable) { Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)") throw e @@ -798,7 +819,31 @@ class VideoDownload { } return sourceLength!!; } - private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long { + + data class DecryptionInfo( + val key: ByteArray, + val iv: ByteArray + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DecryptionInfo + + if (!key.contentEquals(other.key)) return false + if (!iv.contentEquals(other.iv)) return false + + return true + } + + override fun hashCode(): Int { + var result = key.contentHashCode() + result = 31 * result + iv.contentHashCode() + return result + } + } + + private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, decryptionInfo: DecryptionInfo?, onProgress: (Long, Long, Long) -> Unit): Long { val progressRate: Int = 4096 * 5; var lastProgressCount: Int = 0; val speedRate: Int = 4096 * 5; @@ -818,6 +863,8 @@ class VideoDownload { val sourceLength = result.body.contentLength(); val sourceStream = result.body.byteStream(); + val segmentBuffer = ByteArrayOutputStream() + var totalRead: Long = 0; try { var read: Int; @@ -828,7 +875,7 @@ class VideoDownload { if (read < 0) break; - fileStream.write(buffer, 0, read); + segmentBuffer.write(buffer, 0, read); totalRead += read; @@ -854,6 +901,14 @@ class VideoDownload { result.body.close() } + if (decryptionInfo != null) { + val decryptedData = + decryptSegment(segmentBuffer.toByteArray(), decryptionInfo.key, decryptionInfo.iv) + fileStream.write(decryptedData) + } else { + fileStream.write(segmentBuffer.toByteArray()) + } + onProgress(sourceLength, totalRead, 0); return sourceLength; } diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index 9d1a3faa..1faee408 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -61,7 +61,27 @@ class HLS { val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":") val streamInfo = lines.find { it.startsWith("#EXT-X-STREAM-INF:") }?.let { parseStreamInfo(it) } + val keyInfo = + lines.find { it.startsWith("#EXT-X-KEY:") }?.substringAfter(":")?.split(",") + + val key = keyInfo?.find { it.startsWith("URI=") }?.substringAfter("=")?.trim('"') + val iv = + keyInfo?.find { it.startsWith("IV=") }?.substringAfter("=")?.substringAfter("x") + + val decryptionInfo: DecryptionInfo? = key?.let { k -> + iv?.let { i -> + DecryptionInfo(k, i) + } + } + + val initSegment = + lines.find { it.startsWith("#EXT-X-MAP:") }?.substringAfter(":")?.split(",")?.get(0) + ?.substringAfter("=")?.trim('"') val segments = mutableListOf() + if (initSegment != null) { + segments.add(MediaSegment(0.0, resolveUrl(sourceUrl, initSegment))) + } + var currentSegment: MediaSegment? = null lines.forEach { line -> when { @@ -86,7 +106,7 @@ class HLS { } } - return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, streamInfo, segments) + return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, streamInfo, segments, decryptionInfo) } fun parseAndGetVideoSources(source: Any, content: String, url: String): List { @@ -368,6 +388,11 @@ class HLS { } } + data class DecryptionInfo( + val keyUrl: String, + val iv: String + ) + data class VariantPlaylist( val version: Int?, val targetDuration: Int?, @@ -376,7 +401,8 @@ class HLS { val programDateTime: ZonedDateTime?, val playlistType: String?, val streamInfo: StreamInfo?, - val segments: List + val segments: List, + val decryptionInfo: DecryptionInfo? = null ) { fun buildM3U8(): String = buildString { append("#EXTM3U\n") From 470b7bd2e5e68d5c7ef78c3b3c00440cc908f4a6 Mon Sep 17 00:00:00 2001 From: Kai Date: Thu, 20 Feb 2025 21:27:44 -0600 Subject: [PATCH 024/330] add non iv version Changelog: added --- .../platformplayer/downloads/VideoDownload.kt | 34 +++++++++++-------- .../com/futo/platformplayer/parsers/HLS.kt | 6 ++-- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 10d48130..903671b9 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -65,6 +65,7 @@ import java.io.File import java.io.FileOutputStream import java.io.IOException import java.lang.Thread.sleep +import java.nio.ByteBuffer import java.time.OffsetDateTime import java.util.UUID import java.util.concurrent.Executors @@ -593,9 +594,9 @@ class VideoDownload { val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl) val decryptionInfo: DecryptionInfo? = if (variantPlaylist.decryptionInfo != null) { - val keyResponse = client.get(variantPlaylist.decryptionInfo.keyUrl) + val keyResponse = client.get(variantPlaylist.decryptionInfo.keyUrl, mutableMapOf("Cookie" to "sails.sid=s%3AeSKom53v4W3_0CliWJFFMj9k3hcAuyhx.Nf9lF1sUSQ0GUvCKBOM64bsV%2BZMOkiKke43eHO6gTZI;")) check(keyResponse.isOk) { "HLS request failed for decryption key: ${keyResponse.code}" } - DecryptionInfo(keyResponse.body!!.bytes(), variantPlaylist.decryptionInfo.iv.hexStringToByteArray()) + DecryptionInfo(keyResponse.body!!.bytes(), variantPlaylist.decryptionInfo.iv?.hexStringToByteArray()) } else { null } @@ -611,7 +612,7 @@ class VideoDownload { try { segmentFiles.add(segmentFile) - val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri, if (index == 0) null else decryptionInfo) { segmentLength, totalRead, lastSpeed -> + val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri, if (index == 0) null else decryptionInfo, index) { segmentLength, totalRead, lastSpeed -> val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed) @@ -651,10 +652,8 @@ class VideoDownload { private suspend fun combineSegments(context: Context, segmentFiles: List, targetFile: File) = withContext(Dispatchers.IO) { suspendCancellableCoroutine { continuation -> - val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt") - fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" }) - - val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\"" + val cmd = + "-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\"" val statisticsCallback = StatisticsCallback { _ -> //TODO: Show progress? @@ -664,7 +663,6 @@ class VideoDownload { val session = FFmpegKit.executeAsync(cmd, { session -> if (ReturnCode.isSuccess(session.returnCode)) { - fileList.delete() continuation.resumeWith(Result.success(Unit)) } else { val errorMessage = if (ReturnCode.isCancel(session.returnCode)) { @@ -672,7 +670,6 @@ class VideoDownload { } else { "Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}" } - fileList.delete() continuation.resumeWithException(RuntimeException(errorMessage)) } }, @@ -792,7 +789,7 @@ class VideoDownload { else { Logger.i(TAG, "Download $name Sequential"); try { - sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, null, onProgress); + sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, null, 0, onProgress); } catch (e: Throwable) { Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)") throw e @@ -822,7 +819,7 @@ class VideoDownload { data class DecryptionInfo( val key: ByteArray, - val iv: ByteArray + val iv: ByteArray? ) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -843,7 +840,7 @@ class VideoDownload { } } - private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, decryptionInfo: DecryptionInfo?, onProgress: (Long, Long, Long) -> Unit): Long { + private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, decryptionInfo: DecryptionInfo?, index: Int, onProgress: (Long, Long, Long) -> Unit): Long { val progressRate: Int = 4096 * 5; var lastProgressCount: Int = 0; val speedRate: Int = 4096 * 5; @@ -902,8 +899,15 @@ class VideoDownload { } if (decryptionInfo != null) { - val decryptedData = - decryptSegment(segmentBuffer.toByteArray(), decryptionInfo.key, decryptionInfo.iv) + var iv = decryptionInfo.iv + if (iv == null) { + iv = ByteBuffer.allocate(16) + .putLong(0L) + .putLong(index.toLong()) + .array() + } + + val decryptedData = decryptSegment(segmentBuffer.toByteArray(), decryptionInfo.key, iv!!) fileStream.write(decryptedData) } else { fileStream.write(segmentBuffer.toByteArray()) @@ -1222,7 +1226,7 @@ class VideoDownload { else if (container.contains("audio/webm")) return "webm"; else if (container == "application/vnd.apple.mpegurl") - return "mp4a"; + return "mp4"; else return "audio";// throw IllegalStateException("Unknown container: " + container) } diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index 1faee408..3d1ce821 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -69,9 +69,7 @@ class HLS { keyInfo?.find { it.startsWith("IV=") }?.substringAfter("=")?.substringAfter("x") val decryptionInfo: DecryptionInfo? = key?.let { k -> - iv?.let { i -> - DecryptionInfo(k, i) - } + DecryptionInfo(k, iv) } val initSegment = @@ -390,7 +388,7 @@ class HLS { data class DecryptionInfo( val keyUrl: String, - val iv: String + val iv: String? ) data class VariantPlaylist( From 0006da7385378d133d6c0118f5075a6a1c33fe27 Mon Sep 17 00:00:00 2001 From: Koen J Date: Tue, 25 Feb 2025 11:00:54 +0100 Subject: [PATCH 025/330] Implemented sync display names. --- app/build.gradle | 2 +- .../activities/SyncHomeActivity.kt | 3 +- .../mainactivity/main/VideoDetailView.kt | 2 +- .../futo/platformplayer/states/StateSync.kt | 21 +++++++- .../sync/internal/SyncSession.kt | 51 ++++++++++++++++--- .../sync/internal/SyncSocketSession.kt | 11 ++-- 6 files changed, 74 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 866a47dd..8d55d000 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -197,7 +197,7 @@ dependencies { implementation 'org.jsoup:jsoup:1.15.3' implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.arthenica:ffmpeg-kit-full:5.1' + implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS' implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0' implementation 'com.github.dhaval2404:imagepicker:2.1' implementation 'com.google.zxing:core:3.4.1' diff --git a/app/src/main/java/com/futo/platformplayer/activities/SyncHomeActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/SyncHomeActivity.kt index 2d9e51da..d1cd7706 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/SyncHomeActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/SyncHomeActivity.kt @@ -101,7 +101,8 @@ class SyncHomeActivity : AppCompatActivity() { private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView { val connected = session?.connected ?: false syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None) - .setName(publicKey) + .setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey) + //TODO: also display public key? .setStatus(if (connected) "Connected" else "Disconnected") return syncDeviceView } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 147eb2c2..b84e8c5e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -922,7 +922,7 @@ class VideoDetailView : ConstraintLayout { } else if(devices.size == 1){ val device = devices.first(); Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url) - UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , { + UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non '${device.displayName}'" , { Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url) fragment.lifecycleScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt index 0197b856..96c25f9d 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt @@ -44,6 +44,7 @@ import kotlin.system.measureTimeMillis class StateSync { private val _authorizedDevices = FragmentedStorage.get("authorized_devices") + private val _nameStorage = FragmentedStorage.get("sync_remembered_name_storage") private val _syncKeyPair = FragmentedStorage.get("sync_key_pair") private val _lastAddressStorage = FragmentedStorage.get("sync_last_address_storage") private val _syncSessionData = FragmentedStorage.get>("syncSessionData") @@ -305,12 +306,22 @@ class StateSync { synchronized(_sessions) { session = _sessions[s.remotePublicKey] if (session == null) { + val remoteDeviceName = synchronized(_nameStorage) { + _nameStorage.get(remotePublicKey) + } + session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession -> if (!isNewSession) { return@SyncSession } - Logger.i(TAG, "${s.remotePublicKey} authorized") + it.remoteDeviceName?.let { remoteDeviceName -> + synchronized(_nameStorage) { + _nameStorage.setAndSave(remotePublicKey, remoteDeviceName) + } + } + + Logger.i(TAG, "${s.remotePublicKey} authorized (name: ${it.displayName})") synchronized(_lastAddressStorage) { _lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress) } @@ -341,7 +352,7 @@ class StateSync { deviceRemoved.emit(it.remotePublicKey) - }) + }, remoteDeviceName) _sessions[remotePublicKey] = session!! } session!!.addSocketSession(s) @@ -469,6 +480,12 @@ class StateSync { } } + fun getCachedName(publicKey: String): String? { + return synchronized(_nameStorage) { + _nameStorage.get(publicKey) + } + } + suspend fun delete(publicKey: String) { withContext(Dispatchers.IO) { try { diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt index 6281ca23..8b5621e0 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt @@ -6,12 +6,10 @@ import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.models.Subscription -import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.smartMerge import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateHistory -import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StateSubscriptionGroups import com.futo.platformplayer.states.StateSubscriptions @@ -30,6 +28,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.ByteArrayInputStream import java.nio.ByteBuffer +import java.nio.ByteOrder import java.time.Instant import java.time.OffsetDateTime import java.time.ZoneOffset @@ -53,6 +52,9 @@ class SyncSession : IAuthorizable { private val _id = UUID.randomUUID() private var _remoteId: UUID? = null private var _lastAuthorizedRemoteId: UUID? = null + var remoteDeviceName: String? = null + private set + val displayName: String get() = remoteDeviceName ?: remotePublicKey var connected: Boolean = false private set(v) { @@ -62,7 +64,7 @@ class SyncSession : IAuthorizable { } } - constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) { + constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit, remoteDeviceName: String?) { this.remotePublicKey = remotePublicKey _onAuthorized = onAuthorized _onUnauthorized = onUnauthorized @@ -85,7 +87,20 @@ class SyncSession : IAuthorizable { fun authorize(socketSession: SyncSocketSession) { Logger.i(TAG, "Sent AUTHORIZED with session id $_id") - socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray())) + + if (socketSession.remoteVersion >= 3) { + val idStringBytes = _id.toString().toByteArray() + val nameBytes = "${android.os.Build.MANUFACTURER}-${android.os.Build.MODEL}".toByteArray() + val buffer = ByteArray(1 + idStringBytes.size + 1 + nameBytes.size) + socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).apply { + put(idStringBytes.size.toByte()) + put(idStringBytes) + put(nameBytes.size.toByte()) + put(nameBytes) + }.apply { flip() }) + } else { + socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray())) + } _authorized = true checkAuthorized() } @@ -138,15 +153,37 @@ class SyncSession : IAuthorizable { when (opcode) { Opcode.NOTIFY_AUTHORIZED.value -> { - val str = data.toUtf8String() - _remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000") + if (socketSession.remoteVersion >= 3) { + val idByteCount = data.get().toInt() + if (idByteCount > 64) + throw Exception("Id should always be smaller than 64 bytes") + + val idBytes = ByteArray(idByteCount) + data.get(idBytes) + + val nameByteCount = data.get().toInt() + if (nameByteCount > 64) + throw Exception("Name should always be smaller than 64 bytes") + + val nameBytes = ByteArray(nameByteCount) + data.get(nameBytes) + + _remoteId = UUID.fromString(idBytes.toString(Charsets.UTF_8)) + remoteDeviceName = nameBytes.toString(Charsets.UTF_8) + } else { + val str = data.toUtf8String() + _remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000") + remoteDeviceName = null + } + _remoteAuthorized = true - Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId") + Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId (device name: '${remoteDeviceName ?: "not set"}')") checkAuthorized() return } Opcode.NOTIFY_UNAUTHORIZED.value -> { _remoteId = null + remoteDeviceName = null _lastAuthorizedRemoteId = null _remoteAuthorized = false _onUnauthorized(this) diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt index 4a1def91..c997cec4 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt @@ -46,6 +46,8 @@ class SyncSocketSession { val localPublicKey: String get() = _localPublicKey private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit var authorizable: IAuthorizable? = null + var remoteVersion: Int = -1 + private set val remoteAddress: String @@ -162,11 +164,12 @@ class SyncSocketSession { } private fun performVersionCheck() { - val CURRENT_VERSION = 2 + val CURRENT_VERSION = 3 + val MINIMUM_VERSION = 2 _outputStream.writeInt(CURRENT_VERSION) - val version = _inputStream.readInt() - Logger.i(TAG, "performVersionCheck (version = $version)") - if (version != CURRENT_VERSION) + remoteVersion = _inputStream.readInt() + Logger.i(TAG, "performVersionCheck (version = $remoteVersion)") + if (remoteVersion < MINIMUM_VERSION) throw Exception("Invalid version") } From edc2b3d2956ad0315fc09822be0e0be9c4c003b2 Mon Sep 17 00:00:00 2001 From: Koen J Date: Tue, 25 Feb 2025 15:32:30 +0100 Subject: [PATCH 026/330] Fixed issue where video reload would reset video timestamp. --- .../mainactivity/main/VideoDetailView.kt | 72 +++++++++++++------ .../views/video/FutoVideoPlayerBase.kt | 3 +- 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index b84e8c5e..afda7722 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -579,6 +579,14 @@ class VideoDetailView : ConstraintLayout { _minimize_title.setOnClickListener { onMaximize.emit(false) }; _minimize_meta.setOnClickListener { onMaximize.emit(false) }; + _player.onStateChange.subscribe { + if (_player.activelyPlaying) { + Logger.i(TAG, "Play changed, resetting error counter _didTriggerDatasourceErrorCount = 0 (_player.activelyPlaying: ${_player.activelyPlaying})") + _didTriggerDatasourceErrorCount = 0; + _didTriggerDatasourceError = false; + } + } + _player.onPlayChanged.subscribe { if (StateCasting.instance.activeDevice == null) { handlePlayChanged(it); @@ -963,6 +971,7 @@ class VideoDetailView : ConstraintLayout { throw IllegalStateException("Expected media content, found ${video.contentType}"); withContext(Dispatchers.Main) { + _videoResumePositionMilliseconds = _player.position setVideoDetails(video); } } @@ -1265,8 +1274,6 @@ class VideoDetailView : ConstraintLayout { @OptIn(ExperimentalCoroutinesApi::class) fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) { Logger.i(TAG, "setVideoDetails (${videoDetail.name})") - _didTriggerDatasourceErrroCount = 0; - _didTriggerDatasourceError = false; _autoplayVideo = null Logger.i(TAG, "Autoplay video cleared (setVideoDetails)") @@ -1277,6 +1284,10 @@ class VideoDetailView : ConstraintLayout { _lastVideoSource = null; _lastAudioSource = null; _lastSubtitleSource = null; + + Logger.i(TAG, "_didTriggerDatasourceErrorCount reset to 0 because new video") + _didTriggerDatasourceErrorCount = 0; + _didTriggerDatasourceError = false; } if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now()) @@ -1831,7 +1842,7 @@ class VideoDetailView : ConstraintLayout { } } - private var _didTriggerDatasourceErrroCount = 0; + private var _didTriggerDatasourceErrorCount = 0; private var _didTriggerDatasourceError = false; private fun onDataSourceError(exception: Throwable) { Logger.e(TAG, "onDataSourceError", exception); @@ -1841,32 +1852,53 @@ class VideoDetailView : ConstraintLayout { return; val config = currentVideo.sourceConfig; - if(_didTriggerDatasourceErrroCount <= 3) { + if(_didTriggerDatasourceErrorCount <= 3) { _didTriggerDatasourceError = true; - _didTriggerDatasourceErrroCount++; + _didTriggerDatasourceErrorCount++; + + UIDialogs.toast("Detected video error, attempting automatic reload (${_didTriggerDatasourceErrorCount})"); + Logger.i(TAG, "Block detected, attempting bypass (_didTriggerDatasourceErrorCount = ${_didTriggerDatasourceErrorCount})"); - UIDialogs.toast("Block detected, attempting bypass"); //return; fragment.lifecycleScope.launch(Dispatchers.IO) { - val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await(); - val previousVideoSource = _lastVideoSource; - val previousAudioSource = _lastAudioSource; + try { + val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await(); + val previousVideoSource = _lastVideoSource; + val previousAudioSource = _lastAudioSource; - if(newDetails is IPlatformVideoDetails) { - val newVideoSource = if(previousVideoSource != null) - VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS); - else null; - val newAudioSource = if(previousAudioSource != null) - VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong()); - else null; - withContext(Dispatchers.Main) { - video = newDetails; - _player.setSource(newVideoSource, newAudioSource, true, true); + if (newDetails is IPlatformVideoDetails) { + val newVideoSource = if (previousVideoSource != null) + VideoHelper.selectBestVideoSource( + newDetails.video, + previousVideoSource.height * previousVideoSource.width, + FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS + ); + else null; + val newAudioSource = if (previousAudioSource != null) + VideoHelper.selectBestAudioSource( + newDetails.video, + FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, + previousAudioSource.language, + previousAudioSource.bitrate.toLong() + ); + else null; + withContext(Dispatchers.Main) { + video = newDetails; + _player.setSource(newVideoSource, newAudioSource, true, true); + } + } + } catch (e: Throwable) { + Logger.e(TAG, "Failed to get video details, attempting retrying without reloading.", e) + fragment.lifecycleScope.launch(Dispatchers.Main) { + video?.let { + _videoResumePositionMilliseconds = _player.position + setVideoDetails(it, false) + } } } } } - else if(_didTriggerDatasourceErrroCount > 3) { + else if(_didTriggerDatasourceErrorCount > 3) { UIDialogs.showDialog(context, R.drawable.ic_error_pred, context.getString(R.string.media_error), context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental), diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index 61366bf5..c872ca02 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -96,6 +96,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val exoPlayerStateName: String; var playing: Boolean = false; + val activelyPlaying: Boolean get() = (exoPlayer?.player?.playbackState == Player.STATE_READY) && (exoPlayer?.player?.playWhenReady ?: false) val position: Long get() = exoPlayer?.player?.currentPosition ?: 0; val duration: Long get() = exoPlayer?.player?.duration ?: 0; @@ -829,7 +830,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss"); when (error.errorCode) { - PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> { + PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> { Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}"); if(error.cause is HttpDataSource.InvalidResponseCodeException) { val cause = error.cause as HttpDataSource.InvalidResponseCodeException From 1bbfa7d39ecb3a8712f59e7d840b9f53d5deac67 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 26 Feb 2025 21:29:06 +0100 Subject: [PATCH 027/330] WIP home filtering --- .../mainactivity/main/HomeFragment.kt | 43 ++++++++++- .../futo/platformplayer/views/ToggleBar.kt | 74 +++++++++++++++++++ app/src/main/res/layout/view_toggle_bar.xml | 16 ++++ app/src/main/res/values/strings.xml | 2 +- 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt create mode 100644 app/src/main/res/layout/view_toggle_bar.xml diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt index 26210bc8..9cdac8f9 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt @@ -23,6 +23,7 @@ import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.NoResultsView +import com.futo.platformplayer.views.ToggleBar import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewHolder @@ -94,6 +95,8 @@ class HomeFragment : MainFragment() { class HomeView : ContentFeedView { override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle(); + private var _toggleBar: ToggleBar? = null; + private val _taskGetPager: TaskHandler>; override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar @@ -127,6 +130,8 @@ class HomeFragment : MainFragment() { }, fragment); }; + initializeToolbarContent(); + setPreviewsEnabled(Settings.instance.home.previewFeedItems); showAnnouncementView() } @@ -201,13 +206,43 @@ class HomeFragment : MainFragment() { loadResults(); } - override fun filterResults(results: List): List { - return results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) }; + private val _filterLock = Object(); + private var _toggleRecent = false; + fun initializeToolbarContent() { + //Not stable enough with current viewport paging, doesn't work with less results, and reloads content instead of just re-filtering existing + /* + _toggleBar = ToggleBar(context).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + synchronized(_filterLock) { + _toggleBar?.setToggles( + //TODO: loadResults needs to be replaced with an internal reload of the current content + ToggleBar.Toggle("Recent", _toggleRecent) { _toggleRecent = it; loadResults(false) } + ) + } + + _toolbarContentView.addView(_toggleBar, 0); + */ } - private fun loadResults() { + override fun filterResults(results: List): List { + return results.filter { + if(StateMeta.instance.isVideoHidden(it.url)) + return@filter false; + if(StateMeta.instance.isCreatorHidden(it.author.url)) + return@filter false; + + if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 23) { + return@filter false; + } + + return@filter true; + }; + } + + private fun loadResults(withRefetch: Boolean = true) { setLoading(true); - _taskGetPager.run(true); + _taskGetPager.run(withRefetch); } private fun loadedResult(pager : IPager) { if (pager is EmptyPager) { diff --git a/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt b/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt new file mode 100644 index 00000000..be3d8df8 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt @@ -0,0 +1,74 @@ +package com.futo.platformplayer.views + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import com.futo.platformplayer.R +import com.futo.platformplayer.Settings +import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.api.media.models.channels.SerializedChannel +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.models.Subscription +import com.futo.platformplayer.models.SubscriptionGroup +import com.futo.platformplayer.states.StateSubscriptionGroups +import com.futo.platformplayer.states.StateSubscriptions +import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny +import com.futo.platformplayer.views.others.ToggleTagView +import com.futo.platformplayer.views.adapters.viewholders.SubscriptionBarViewHolder +import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder +import com.futo.platformplayer.views.subscriptions.SubscriptionExploreButton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ToggleBar : LinearLayout { + private val _tagsContainer: LinearLayout; + + override fun onAttachedToWindow() { + super.onAttachedToWindow(); + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow(); + StateSubscriptionGroups.instance.onGroupsChanged.remove(this); + } + + constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { + inflate(context, R.layout.view_toggle_bar, this); + + _tagsContainer = findViewById(R.id.container_tags); + } + + fun setToggles(vararg buttons: Toggle) { + _tagsContainer.removeAllViews(); + for(button in buttons) { + _tagsContainer.addView(ToggleTagView(context).apply { + this.setInfo(button.name, button.isActive); + this.onClick.subscribe { button.action(it); }; + }); + } + } + + class Toggle { + val name: String; + val icon: Int; + val action: (Boolean)->Unit; + val isActive: Boolean; + + constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) { + this.name = name; + this.icon = icon; + this.action = action; + this.isActive = isActive; + } + constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) { + this.name = name; + this.icon = 0; + this.action = action; + this.isActive = isActive; + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/view_toggle_bar.xml b/app/src/main/res/layout/view_toggle_bar.xml new file mode 100644 index 00000000..3da2f363 --- /dev/null +++ b/app/src/main/res/layout/view_toggle_bar.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e7026f7a..d4df1905 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -418,7 +418,7 @@ Log Level Logging Sync Grayjay - Sync your settings across multiple devices + Sync your data across multiple devices Manage Polycentric identity Manage your Polycentric identity Manual check From 442272f51788a4172bd0628d6be9f0aabefd77ab Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 27 Feb 2025 10:38:03 +0100 Subject: [PATCH 028/330] SettingsActivity can now be landscape. --- app/src/main/AndroidManifest.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index afd659a7..c9917a2d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -156,7 +156,6 @@ android:theme="@style/Theme.FutoVideo.NoActionBar" /> Date: Thu, 27 Feb 2025 14:34:51 +0100 Subject: [PATCH 029/330] submods --- app/src/stable/assets/sources/apple-podcasts | 2 +- app/src/stable/assets/sources/bilibili | 2 +- app/src/stable/assets/sources/bitchute | 2 +- app/src/stable/assets/sources/dailymotion | 2 +- app/src/stable/assets/sources/kick | 2 +- app/src/stable/assets/sources/nebula | 2 +- app/src/stable/assets/sources/odysee | 2 +- app/src/stable/assets/sources/patreon | 2 +- app/src/stable/assets/sources/peertube | 2 +- app/src/stable/assets/sources/rumble | 2 +- app/src/stable/assets/sources/soundcloud | 2 +- app/src/stable/assets/sources/spotify | 2 +- app/src/stable/assets/sources/twitch | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/apple-podcasts | 2 +- app/src/unstable/assets/sources/bilibili | 2 +- app/src/unstable/assets/sources/bitchute | 2 +- app/src/unstable/assets/sources/dailymotion | 2 +- app/src/unstable/assets/sources/kick | 2 +- app/src/unstable/assets/sources/nebula | 2 +- app/src/unstable/assets/sources/odysee | 2 +- app/src/unstable/assets/sources/patreon | 2 +- app/src/unstable/assets/sources/peertube | 2 +- app/src/unstable/assets/sources/rumble | 2 +- app/src/unstable/assets/sources/soundcloud | 2 +- app/src/unstable/assets/sources/spotify | 2 +- app/src/unstable/assets/sources/twitch | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 28 files changed, 28 insertions(+), 28 deletions(-) diff --git a/app/src/stable/assets/sources/apple-podcasts b/app/src/stable/assets/sources/apple-podcasts index 090104c7..07e39f9d 160000 --- a/app/src/stable/assets/sources/apple-podcasts +++ b/app/src/stable/assets/sources/apple-podcasts @@ -1 +1 @@ -Subproject commit 090104c7fa112d9772f43c7c2620e8c2cf3c9d6a +Subproject commit 07e39f9df71b1937adf5bfb718a115fc232aa6f8 diff --git a/app/src/stable/assets/sources/bilibili b/app/src/stable/assets/sources/bilibili index 13b30fd7..ce0571bd 160000 --- a/app/src/stable/assets/sources/bilibili +++ b/app/src/stable/assets/sources/bilibili @@ -1 +1 @@ -Subproject commit 13b30fd76e30a60c114c97b876542f7f106b5881 +Subproject commit ce0571bdeaed4e341351ef477ef4b6599aa4d0fb diff --git a/app/src/stable/assets/sources/bitchute b/app/src/stable/assets/sources/bitchute index 7f869aa4..3fbd872a 160000 --- a/app/src/stable/assets/sources/bitchute +++ b/app/src/stable/assets/sources/bitchute @@ -1 +1 @@ -Subproject commit 7f869aa4b117214095feb367d38414402cd08417 +Subproject commit 3fbd872ad8bd7df62c5fbec7437e1200d82b74e1 diff --git a/app/src/stable/assets/sources/dailymotion b/app/src/stable/assets/sources/dailymotion index d00c7ff8..b34134ca 160000 --- a/app/src/stable/assets/sources/dailymotion +++ b/app/src/stable/assets/sources/dailymotion @@ -1 +1 @@ -Subproject commit d00c7ff8e557d8b5624c162e4e554f65625c5e29 +Subproject commit b34134ca2dbb1662b060b4a67f14e7c5d077889d diff --git a/app/src/stable/assets/sources/kick b/app/src/stable/assets/sources/kick index 8d957b6f..2046944c 160000 --- a/app/src/stable/assets/sources/kick +++ b/app/src/stable/assets/sources/kick @@ -1 +1 @@ -Subproject commit 8d957b6fc4f354f4ab68d3cb2d1a7fa19323edeb +Subproject commit 2046944c18f48c15dfbea82f3f89d7ba6dce5e14 diff --git a/app/src/stable/assets/sources/nebula b/app/src/stable/assets/sources/nebula index 9e6dcf09..f30a3bfc 160000 --- a/app/src/stable/assets/sources/nebula +++ b/app/src/stable/assets/sources/nebula @@ -1 +1 @@ -Subproject commit 9e6dcf093538511eac56dc44a32b99139f1f1005 +Subproject commit f30a3bfc0f6a894d816ab7fa732b8f63eb54b84e diff --git a/app/src/stable/assets/sources/odysee b/app/src/stable/assets/sources/odysee index 04b4d8ed..f2f83344 160000 --- a/app/src/stable/assets/sources/odysee +++ b/app/src/stable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit 04b4d8ed3163b7146bb58c418c201899e04e34cb +Subproject commit f2f83344ebc905b36c0689bfef407bb95e6d9af0 diff --git a/app/src/stable/assets/sources/patreon b/app/src/stable/assets/sources/patreon index 9c835e07..e5dce87c 160000 --- a/app/src/stable/assets/sources/patreon +++ b/app/src/stable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit 9c835e075c66ea014e544d9fe35fbb317d72a196 +Subproject commit e5dce87c9d8ae7571df40a6fa252404e18b963f1 diff --git a/app/src/stable/assets/sources/peertube b/app/src/stable/assets/sources/peertube index 20fd03d9..2bcab14d 160000 --- a/app/src/stable/assets/sources/peertube +++ b/app/src/stable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit 20fd03d9847b308d81ce474144bee79b04385477 +Subproject commit 2bcab14d01a564aa8ab9218de54042fc68b9ee76 diff --git a/app/src/stable/assets/sources/rumble b/app/src/stable/assets/sources/rumble index b9e6259f..a32dbb62 160000 --- a/app/src/stable/assets/sources/rumble +++ b/app/src/stable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit b9e6259f4eedd41d1838506d32d240e177c1feff +Subproject commit a32dbb626aacfc6264e505cd5c7f34dd8a60edfc diff --git a/app/src/stable/assets/sources/soundcloud b/app/src/stable/assets/sources/soundcloud index a72aeb85..ae47f2ea 160000 --- a/app/src/stable/assets/sources/soundcloud +++ b/app/src/stable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit a72aeb85d0fc0c17382cb1a7066fe4ec8b63691c +Subproject commit ae47f2eaacaf2879405435358965c47eb3d48096 diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify index eb231ade..0d05e35c 160000 --- a/app/src/stable/assets/sources/spotify +++ b/app/src/stable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit eb231adeae7acd0ed8b14e2ebc2b93424ac6811c +Subproject commit 0d05e35cfc81acfa78594c91c381b79694aaf86d diff --git a/app/src/stable/assets/sources/twitch b/app/src/stable/assets/sources/twitch index 1b2833cd..a75e8460 160000 --- a/app/src/stable/assets/sources/twitch +++ b/app/src/stable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit 1b2833cdf22afec8b1177bf8bc3e5f83bc014e37 +Subproject commit a75e846045a7882002dd7a6bfa83550f52d9dbab diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 8f8774a7..857c147b 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 8f8774a782aa49889774920688de371f28317ca6 +Subproject commit 857c147b3a3d3e9d0a79c47f1bd5813e08ed2daf diff --git a/app/src/unstable/assets/sources/apple-podcasts b/app/src/unstable/assets/sources/apple-podcasts index 090104c7..07e39f9d 160000 --- a/app/src/unstable/assets/sources/apple-podcasts +++ b/app/src/unstable/assets/sources/apple-podcasts @@ -1 +1 @@ -Subproject commit 090104c7fa112d9772f43c7c2620e8c2cf3c9d6a +Subproject commit 07e39f9df71b1937adf5bfb718a115fc232aa6f8 diff --git a/app/src/unstable/assets/sources/bilibili b/app/src/unstable/assets/sources/bilibili index 13b30fd7..ce0571bd 160000 --- a/app/src/unstable/assets/sources/bilibili +++ b/app/src/unstable/assets/sources/bilibili @@ -1 +1 @@ -Subproject commit 13b30fd76e30a60c114c97b876542f7f106b5881 +Subproject commit ce0571bdeaed4e341351ef477ef4b6599aa4d0fb diff --git a/app/src/unstable/assets/sources/bitchute b/app/src/unstable/assets/sources/bitchute index 7f869aa4..3fbd872a 160000 --- a/app/src/unstable/assets/sources/bitchute +++ b/app/src/unstable/assets/sources/bitchute @@ -1 +1 @@ -Subproject commit 7f869aa4b117214095feb367d38414402cd08417 +Subproject commit 3fbd872ad8bd7df62c5fbec7437e1200d82b74e1 diff --git a/app/src/unstable/assets/sources/dailymotion b/app/src/unstable/assets/sources/dailymotion index d00c7ff8..b34134ca 160000 --- a/app/src/unstable/assets/sources/dailymotion +++ b/app/src/unstable/assets/sources/dailymotion @@ -1 +1 @@ -Subproject commit d00c7ff8e557d8b5624c162e4e554f65625c5e29 +Subproject commit b34134ca2dbb1662b060b4a67f14e7c5d077889d diff --git a/app/src/unstable/assets/sources/kick b/app/src/unstable/assets/sources/kick index 8d957b6f..2046944c 160000 --- a/app/src/unstable/assets/sources/kick +++ b/app/src/unstable/assets/sources/kick @@ -1 +1 @@ -Subproject commit 8d957b6fc4f354f4ab68d3cb2d1a7fa19323edeb +Subproject commit 2046944c18f48c15dfbea82f3f89d7ba6dce5e14 diff --git a/app/src/unstable/assets/sources/nebula b/app/src/unstable/assets/sources/nebula index 9e6dcf09..f30a3bfc 160000 --- a/app/src/unstable/assets/sources/nebula +++ b/app/src/unstable/assets/sources/nebula @@ -1 +1 @@ -Subproject commit 9e6dcf093538511eac56dc44a32b99139f1f1005 +Subproject commit f30a3bfc0f6a894d816ab7fa732b8f63eb54b84e diff --git a/app/src/unstable/assets/sources/odysee b/app/src/unstable/assets/sources/odysee index 04b4d8ed..f2f83344 160000 --- a/app/src/unstable/assets/sources/odysee +++ b/app/src/unstable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit 04b4d8ed3163b7146bb58c418c201899e04e34cb +Subproject commit f2f83344ebc905b36c0689bfef407bb95e6d9af0 diff --git a/app/src/unstable/assets/sources/patreon b/app/src/unstable/assets/sources/patreon index 9c835e07..e5dce87c 160000 --- a/app/src/unstable/assets/sources/patreon +++ b/app/src/unstable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit 9c835e075c66ea014e544d9fe35fbb317d72a196 +Subproject commit e5dce87c9d8ae7571df40a6fa252404e18b963f1 diff --git a/app/src/unstable/assets/sources/peertube b/app/src/unstable/assets/sources/peertube index 20fd03d9..2bcab14d 160000 --- a/app/src/unstable/assets/sources/peertube +++ b/app/src/unstable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit 20fd03d9847b308d81ce474144bee79b04385477 +Subproject commit 2bcab14d01a564aa8ab9218de54042fc68b9ee76 diff --git a/app/src/unstable/assets/sources/rumble b/app/src/unstable/assets/sources/rumble index b9e6259f..a32dbb62 160000 --- a/app/src/unstable/assets/sources/rumble +++ b/app/src/unstable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit b9e6259f4eedd41d1838506d32d240e177c1feff +Subproject commit a32dbb626aacfc6264e505cd5c7f34dd8a60edfc diff --git a/app/src/unstable/assets/sources/soundcloud b/app/src/unstable/assets/sources/soundcloud index a72aeb85..ae47f2ea 160000 --- a/app/src/unstable/assets/sources/soundcloud +++ b/app/src/unstable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit a72aeb85d0fc0c17382cb1a7066fe4ec8b63691c +Subproject commit ae47f2eaacaf2879405435358965c47eb3d48096 diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify index eb231ade..0d05e35c 160000 --- a/app/src/unstable/assets/sources/spotify +++ b/app/src/unstable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit eb231adeae7acd0ed8b14e2ebc2b93424ac6811c +Subproject commit 0d05e35cfc81acfa78594c91c381b79694aaf86d diff --git a/app/src/unstable/assets/sources/twitch b/app/src/unstable/assets/sources/twitch index 1b2833cd..a75e8460 160000 --- a/app/src/unstable/assets/sources/twitch +++ b/app/src/unstable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit 1b2833cdf22afec8b1177bf8bc3e5f83bc014e37 +Subproject commit a75e846045a7882002dd7a6bfa83550f52d9dbab diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 8f8774a7..857c147b 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 8f8774a782aa49889774920688de371f28317ca6 +Subproject commit 857c147b3a3d3e9d0a79c47f1bd5813e08ed2daf From bbeb9b83a0ba4538107758ab993566aa625749dc Mon Sep 17 00:00:00 2001 From: Koen J Date: Wed, 5 Mar 2025 11:58:09 +0100 Subject: [PATCH 030/330] Removed dynamic Polycentric calls. --- .../views/adapters/PlaylistView.kt | 43 ----------- .../views/adapters/SubscriptionViewHolder.kt | 41 ---------- .../adapters/feedtypes/PreviewPostView.kt | 43 ----------- .../adapters/feedtypes/PreviewVideoView.kt | 71 ----------------- .../viewholders/CreatorBarViewHolder.kt | 76 ------------------- .../adapters/viewholders/CreatorViewHolder.kt | 39 ---------- .../viewholders/SubscriptionBarViewHolder.kt | 38 ---------- .../main/res/layout/list_locked_thumbnail.xml | 17 +---- .../main/res/layout/list_playlist_feed.xml | 17 +---- app/src/main/res/layout/list_post_preview.xml | 18 +---- .../main/res/layout/list_post_thumbnail.xml | 19 +---- .../main/res/layout/list_video_thumbnail.xml | 17 +---- .../layout/list_video_thumbnail_nested.xml | 19 +---- 13 files changed, 13 insertions(+), 445 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt index b606bf26..c8d86b14 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt @@ -29,21 +29,12 @@ open class PlaylistView : LinearLayout { protected val _imageThumbnail: ImageView protected val _imageChannel: ImageView? protected val _creatorThumbnail: CreatorThumbnail? - protected val _imageNeopassChannel: ImageView?; protected val _platformIndicator: PlatformIndicator; protected val _textPlaylistName: TextView protected val _textVideoCount: TextView protected val _textVideoCountLabel: TextView; protected val _textPlaylistItems: TextView protected val _textChannelName: TextView - protected var _neopassAnimator: ObjectAnimator? = null; - - private val _taskLoadValidClaims = TaskHandler(StateApp.instance.scopeGetter, - { PolycentricCache.instance.getValidClaimsAsync(it).await() }) - .success { it -> updateClaimsLayout(it, animate = true) } - .exception { - Logger.w(TAG, "Failed to load claims.", it); - }; val onPlaylistClicked = Event1(); val onChannelClicked = Event1(); @@ -66,7 +57,6 @@ open class PlaylistView : LinearLayout { _textVideoCountLabel = findViewById(R.id.text_video_count_label); _textChannelName = findViewById(R.id.text_channel_name); _textPlaylistItems = findViewById(R.id.text_playlist_items); - _imageNeopassChannel = findViewById(R.id.image_neopass_channel); setOnClickListener { onOpenClicked() }; _imageChannel?.setOnClickListener { currentPlaylist?.let { onChannelClicked.emit(it.author) } }; @@ -88,20 +78,6 @@ open class PlaylistView : LinearLayout { open fun bind(content: IPlatformContent) { - _taskLoadValidClaims.cancel(); - - if (content.author.id.claimType > 0) { - val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id); - if (cachedClaims != null) { - updateClaimsLayout(cachedClaims, animate = false); - } else { - updateClaimsLayout(null, animate = false); - _taskLoadValidClaims.run(content.author.id); - } - } else { - updateClaimsLayout(null, animate = false); - } - isClickable = true; _imageChannel?.let { @@ -155,25 +131,6 @@ open class PlaylistView : LinearLayout { } } - private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) { - _neopassAnimator?.cancel(); - _neopassAnimator = null; - - val firstClaim = claims?.ownedClaims?.firstOrNull(); - val harborAvailable = firstClaim != null - if (harborAvailable) { - _imageNeopassChannel?.visibility = View.VISIBLE - if (animate) { - _neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500) - _neopassAnimator?.start() - } - } else { - _imageNeopassChannel?.visibility = View.GONE - } - - _creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto()) - } - companion object { private val TAG = "VideoPreviewViewHolder" } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt index 603f79d3..05a3f5e7 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt @@ -32,14 +32,6 @@ class SubscriptionViewHolder : ViewHolder { private val _platformIndicator : PlatformIndicator; private val _textMeta: TextView; - private val _taskLoadProfile = TaskHandler( - StateApp.instance.scopeGetter, - { PolycentricCache.instance.getProfileAsync(it) }) - .success { it -> onProfileLoaded(null, it, true) } - .exception { - Logger.w(TAG, "Failed to load profile.", it); - }; - var subscription: Subscription? = null private set; @@ -74,45 +66,12 @@ class SubscriptionViewHolder : ViewHolder { } fun bind(sub: Subscription) { - _taskLoadProfile.cancel(); - this.subscription = sub; _creatorThumbnail.setThumbnail(sub.channel.thumbnail, false); _textName.text = sub.channel.name; bindViewMetrics(sub); _platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId); - - val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true); - if (cachedProfile != null) { - onProfileLoaded(sub, cachedProfile, false); - if (cachedProfile.expired) { - _taskLoadProfile.run(sub.channel.id); - } - } else { - _taskLoadProfile.run(sub.channel.id); - } - } - - private fun onProfileLoaded(sub: Subscription?, cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { - val dp_46 = 46.dp(itemView.context.resources); - val profile = cachedPolycentricProfile?.profile; - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46) - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; - - if (avatar != null) { - _creatorThumbnail.setThumbnail(avatar, animate); - } else { - _creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); - } - - if (profile != null) { - _textName.text = profile.systemState.username; - } - - if(sub != null) - bindViewMetrics(sub) } fun bindViewMetrics(sub: Subscription?) { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewPostView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewPostView.kt index 5a476421..cbf0b8a6 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewPostView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewPostView.kt @@ -44,7 +44,6 @@ class PreviewPostView : LinearLayout { private val _imageAuthorThumbnail: ImageView; private val _textAuthorName: TextView; - private val _imageNeopassChannel: ImageView; private val _textMetadata: TextView; private val _textTitle: TextView; private val _textDescription: TextView; @@ -64,15 +63,6 @@ class PreviewPostView : LinearLayout { private val _layoutComments: LinearLayout?; private val _textComments: TextView?; - private var _neopassAnimator: ObjectAnimator? = null; - - private val _taskLoadValidClaims = TaskHandler(StateApp.instance.scopeGetter, - { PolycentricCache.instance.getValidClaimsAsync(it).await() }) - .success { it -> updateClaimsLayout(it, animate = true) } - .exception { - Logger.w(TAG, "Failed to load claims.", it); - }; - val content: IPlatformContent? get() = _content; val onContentClicked = Event1(); @@ -83,7 +73,6 @@ class PreviewPostView : LinearLayout { _imageAuthorThumbnail = findViewById(R.id.image_author_thumbnail); _textAuthorName = findViewById(R.id.text_author_name); - _imageNeopassChannel = findViewById(R.id.image_neopass_channel); _textMetadata = findViewById(R.id.text_metadata); _textTitle = findViewById(R.id.text_title); _textDescription = findViewById(R.id.text_description); @@ -130,21 +119,8 @@ class PreviewPostView : LinearLayout { } fun bind(content: IPlatformContent) { - _taskLoadValidClaims.cancel(); _content = content; - if (content.author.id.claimType > 0) { - val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id); - if (cachedClaims != null) { - updateClaimsLayout(cachedClaims, animate = false); - } else { - updateClaimsLayout(null, animate = false); - _taskLoadValidClaims.run(content.author.id); - } - } else { - updateClaimsLayout(null, animate = false); - } - _textAuthorName.text = content.author.name; _textMetadata.text = content.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: ""; @@ -292,25 +268,6 @@ class PreviewPostView : LinearLayout { }; } - private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) { - _neopassAnimator?.cancel(); - _neopassAnimator = null; - - val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty(); - if (harborAvailable) { - _imageNeopassChannel.visibility = View.VISIBLE - if (animate) { - _neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500) - _neopassAnimator?.start() - } - } else { - _imageNeopassChannel.visibility = View.GONE - } - - //TODO: Necessary if we decide to use creator thumbnail with neopass indicator instead - //_creatorThumbnail?.setHarborAvailable(harborAvailable, animate) - } - companion object { val TAG = "PreviewPostView"; } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt index 33066060..f2123832 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt @@ -47,7 +47,6 @@ open class PreviewVideoView : LinearLayout { protected val _imageVideo: ImageView protected val _imageChannel: ImageView? protected val _creatorThumbnail: CreatorThumbnail? - protected val _imageNeopassChannel: ImageView?; protected val _platformIndicator: PlatformIndicator; protected val _textVideoName: TextView protected val _textChannelName: TextView @@ -57,7 +56,6 @@ open class PreviewVideoView : LinearLayout { protected var _playerVideoThumbnail: FutoThumbnailPlayer? = null; protected val _containerLive: LinearLayout; protected val _playerContainer: FrameLayout; - protected var _neopassAnimator: ObjectAnimator? = null; protected val _layoutDownloaded: FrameLayout; protected val _button_add_to_queue : View; @@ -65,15 +63,6 @@ open class PreviewVideoView : LinearLayout { protected val _button_add_to : View; protected val _exoPlayer: PlayerManager?; - - private val _taskLoadProfile = TaskHandler( - StateApp.instance.scopeGetter, - { PolycentricCache.instance.getProfileAsync(it) }) - .success { it -> onProfileLoaded(it, true) } - .exception { - Logger.w(TAG, "Failed to load profile.", it); - }; - private val _timeBar: ProgressBar?; val onVideoClicked = Event2(); @@ -108,7 +97,6 @@ open class PreviewVideoView : LinearLayout { _button_add_to_queue = findViewById(R.id.button_add_to_queue); _button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later); _button_add_to = findViewById(R.id.button_add_to); - _imageNeopassChannel = findViewById(R.id.image_neopass_channel); _layoutDownloaded = findViewById(R.id.layout_downloaded); _timeBar = findViewById(R.id.time_bar) @@ -160,15 +148,12 @@ open class PreviewVideoView : LinearLayout { open fun bind(content: IPlatformContent) { - _taskLoadProfile.cancel(); - isClickable = true; val isPlanned = (content.datetime?.getNowDiffSeconds() ?: 0) < 0; stopPreview(); - _imageNeopassChannel?.visibility = View.GONE; _creatorThumbnail?.setThumbnail(content.author.thumbnail, false); val thumbnail = content.author.thumbnail @@ -186,16 +171,6 @@ open class PreviewVideoView : LinearLayout { _textChannelName.text = content.author.name - val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true); - if (cachedProfile != null) { - onProfileLoaded(cachedProfile, false); - if (cachedProfile.expired) { - _taskLoadProfile.run(content.author.id); - } - } else { - _taskLoadProfile.run(content.author.id); - } - _imageChannel?.clipToOutline = true; _textVideoName.text = content.name; @@ -335,52 +310,6 @@ open class PreviewVideoView : LinearLayout { _playerVideoThumbnail?.setMuteChangedListener(callback); } - private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { - _neopassAnimator?.cancel(); - _neopassAnimator = null; - - val profile = cachedPolycentricProfile?.profile; - if (_creatorThumbnail != null) { - val dp_32 = 32.dp(context.resources); - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_32 * dp_32) - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; - - if (avatar != null) { - _creatorThumbnail.setThumbnail(avatar, animate); - } else { - _creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); - } - } else if (_imageChannel != null) { - val dp_28 = 28.dp(context.resources); - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_28 * dp_28) - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; - - if (avatar != null) { - _imageChannel.let { - Glide.with(_imageChannel) - .load(avatar) - .placeholder(R.drawable.placeholder_channel_thumbnail) - .into(_imageChannel); - } - - _imageNeopassChannel?.visibility = View.VISIBLE - if (animate) { - _neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500) - _neopassAnimator?.start() - } else { - _imageNeopassChannel?.alpha = 1.0f; - } - } else { - _imageNeopassChannel?.visibility = View.GONE - } - } - - if (profile != null) { - _textChannelName.text = profile.systemState.username - } - } - companion object { private val TAG = "VideoPreviewViewHolder" } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt index 14322506..897718bf 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt @@ -27,14 +27,6 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi val onClick = Event1(); - private val _taskLoadProfile = TaskHandler( - StateApp.instance.scopeGetter, - { PolycentricCache.instance.getProfileAsync(it) }) - .success { onProfileLoaded(it, true) } - .exception { - Logger.w(TAG, "Failed to load profile.", it); - }; - init { _creatorThumbnail = _view.findViewById(R.id.creator_thumbnail); _name = _view.findViewById(R.id.text_channel_name); @@ -45,40 +37,10 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi } override fun bind(value: IPlatformChannel) { - _taskLoadProfile.cancel(); - _channel = value; _creatorThumbnail.setThumbnail(value.thumbnail, false); _name.text = value.name; - - val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true); - if (cachedProfile != null) { - onProfileLoaded(cachedProfile, false); - if (cachedProfile.expired) { - _taskLoadProfile.run(value.id); - } - } else { - _taskLoadProfile.run(value.id); - } - } - - private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { - val dp_55 = 55.dp(itemView.context.resources) - val profile = cachedPolycentricProfile?.profile; - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55) - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; - - if (avatar != null) { - _creatorThumbnail.setThumbnail(avatar, animate); - } else { - _creatorThumbnail.setThumbnail(_channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); - } - - if (profile != null) { - _name.text = profile.systemState.username; - } } companion object { @@ -94,14 +56,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda val onClick = Event1(); - private val _taskLoadProfile = TaskHandler( - StateApp.instance.scopeGetter, - { PolycentricCache.instance.getProfileAsync(it) }) - .success { onProfileLoaded(it, true) } - .exception { - Logger.w(TAG, "Failed to load profile.", it); - }; - init { _creatorThumbnail = _view.findViewById(R.id.creator_thumbnail); _name = _view.findViewById(R.id.text_channel_name); @@ -112,8 +66,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda } override fun bind(value: Selectable) { - _taskLoadProfile.cancel(); - _channel = value; if(value.active) @@ -123,34 +75,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda _creatorThumbnail.setThumbnail(value.channel.thumbnail, false); _name.text = value.channel.name; - - val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true); - if (cachedProfile != null) { - onProfileLoaded(cachedProfile, false); - if (cachedProfile.expired) { - _taskLoadProfile.run(value.channel.id); - } - } else { - _taskLoadProfile.run(value.channel.id); - } - } - - private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { - val dp_55 = 55.dp(itemView.context.resources) - val profile = cachedPolycentricProfile?.profile; - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55) - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; - - if (avatar != null) { - _creatorThumbnail.setThumbnail(avatar, animate); - } else { - _creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); - } - - if (profile != null) { - _name.text = profile.systemState.username; - } } companion object { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt index 5c57ffa9..b5784d4c 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt @@ -34,14 +34,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo val onClick = Event1(); - private val _taskLoadProfile = TaskHandler( - StateApp.instance.scopeGetter, - { PolycentricCache.instance.getProfileAsync(it) }) - .success { it -> onProfileLoaded(it, true) } - .exception { - Logger.w(TAG, "Failed to load profile.", it); - }; - init { _textName = _view.findViewById(R.id.text_channel_name); _creatorThumbnail = _view.findViewById(R.id.creator_thumbnail); @@ -61,21 +53,9 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo } override fun bind(value: PlatformAuthorLink) { - _taskLoadProfile.cancel(); - _creatorThumbnail.setThumbnail(value.thumbnail, false); _textName.text = value.name; - val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true); - if (cachedProfile != null) { - onProfileLoaded(cachedProfile, false); - if (cachedProfile.expired) { - _taskLoadProfile.run(value.id); - } - } else { - _taskLoadProfile.run(value.id); - } - if(value.subscribers == null || (value.subscribers ?: 0) <= 0L) _textMetadata.visibility = View.GONE; else { @@ -87,25 +67,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo _authorLink = value; } - private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { - val dp_61 = 61.dp(itemView.context.resources); - - val profile = cachedPolycentricProfile?.profile; - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_61 * dp_61) - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; - - if (avatar != null) { - _creatorThumbnail.setThumbnail(avatar, animate); - } else { - _creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); - } - - if (profile != null) { - _textName.text = profile.systemState.username; - } - } - companion object { private const val TAG = "CreatorViewHolder"; } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt index 2b6350c4..da491cf6 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt @@ -27,14 +27,6 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter. private var _subscription: Subscription? = null; private var _channel: SerializedChannel? = null; - private val _taskLoadProfile = TaskHandler( - StateApp.instance.scopeGetter, - { PolycentricCache.instance.getProfileAsync(it) }) - .success { onProfileLoaded(it, true) } - .exception { - Logger.w(TAG, "Failed to load profile.", it); - }; - val onClick = Event1(); init { @@ -47,44 +39,14 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter. } override fun bind(value: Subscription) { - _taskLoadProfile.cancel(); - _channel = value.channel; _creatorThumbnail.setThumbnail(value.channel.thumbnail, false); _name.text = value.channel.name; - val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true); - if (cachedProfile != null) { - onProfileLoaded(cachedProfile, false); - if (cachedProfile.expired) { - _taskLoadProfile.run(value.channel.id); - } - } else { - _taskLoadProfile.run(value.channel.id); - } - _subscription = value; } - private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { - val dp_55 = 55.dp(itemView.context.resources) - val profile = cachedPolycentricProfile?.profile; - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55) - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; - - if (avatar != null) { - _creatorThumbnail.setThumbnail(avatar, animate); - } else { - _creatorThumbnail.setThumbnail(_channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); - } - - if (profile != null) { - _name.text = profile.systemState.username; - } - } - companion object { private const val TAG = "SubscriptionBarViewHolder"; } diff --git a/app/src/main/res/layout/list_locked_thumbnail.xml b/app/src/main/res/layout/list_locked_thumbnail.xml index 462d65d4..f00e28f3 100644 --- a/app/src/main/res/layout/list_locked_thumbnail.xml +++ b/app/src/main/res/layout/list_locked_thumbnail.xml @@ -254,7 +254,7 @@ - - - - + app:layout_constraintTop_toBottomOf="@id/text_playlist_name" /> - - - - - - - - - - Date: Wed, 5 Mar 2025 17:04:48 +0100 Subject: [PATCH 031/330] Implemented new ApiMethods calls. --- .../platformplayer/Extensions_Polycentric.kt | 40 +- .../PolycentricCreateProfileActivity.kt | 6 +- .../PolycentricImportProfileActivity.kt | 4 +- .../activities/PolycentricProfileActivity.kt | 6 +- .../platformplayer/dialogs/CommentDialog.kt | 2 +- .../channel/tab/ChannelAboutFragment.kt | 6 +- .../channel/tab/ChannelContentsFragment.kt | 2 +- .../channel/tab/ChannelListFragment.kt | 2 +- .../tab/ChannelMonetizationFragment.kt | 2 +- .../channel/tab/IChannelTabFragment.kt | 2 +- .../mainactivity/main/ChannelFragment.kt | 54 +-- .../mainactivity/main/CommentsFragment.kt | 2 +- .../mainactivity/main/PostDetailFragment.kt | 30 +- .../mainactivity/main/VideoDetailView.kt | 62 +-- .../topbar/NavigationTopBarFragment.kt | 2 +- .../images/PolycentricModelLoader.java | 17 +- .../polycentric/PolycentricCache.kt | 353 ------------------ .../futo/platformplayer/states/StateCache.kt | 10 +- .../futo/platformplayer/states/StateMeta.kt | 2 +- .../platformplayer/states/StatePolycentric.kt | 56 +-- .../states/StateSubscriptionGroups.kt | 41 +- .../states/StateSubscriptions.kt | 7 - .../stores/CachedPolycentricProfileStorage.kt | 31 -- .../platformplayer/views/MonetizationView.kt | 5 +- .../futo/platformplayer/views/SupportView.kt | 2 +- .../views/adapters/ChannelViewPagerAdapter.kt | 2 +- .../views/adapters/CommentViewHolder.kt | 19 +- .../CommentWithReferenceViewHolder.kt | 2 +- .../views/adapters/PlaylistView.kt | 1 - .../views/adapters/SubscriptionViewHolder.kt | 1 - .../adapters/feedtypes/PreviewPostView.kt | 1 - .../adapters/feedtypes/PreviewVideoView.kt | 1 - .../viewholders/CreatorBarViewHolder.kt | 1 - .../adapters/viewholders/CreatorViewHolder.kt | 1 - .../viewholders/SubscriptionBarViewHolder.kt | 1 - .../SubscriptionGroupListViewHolder.kt | 1 - .../views/others/CreatorThumbnail.kt | 4 +- .../views/overlays/SupportOverlay.kt | 2 +- .../views/overlays/WebviewOverlay.kt | 2 - .../views/segments/CommentsList.kt | 2 +- dep/polycentricandroid | 2 +- 41 files changed, 124 insertions(+), 665 deletions(-) delete mode 100644 app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt delete mode 100644 app/src/main/java/com/futo/platformplayer/stores/CachedPolycentricProfileStorage.kt diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt index 74f1372e..139407ed 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt @@ -1,13 +1,13 @@ package com.futo.platformplayer import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StatePlatform import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.Store import com.futo.polycentric.core.SystemState +import com.futo.polycentric.core.base64UrlToByteArray import userpackage.Protocol import kotlin.math.abs import kotlin.math.min @@ -40,33 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) } } +fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? { + val urlData = if (this.startsWith("polycentric://")) { + this.substring("polycentric://".length) + } else this; + + val urlBytes = urlData.base64UrlToByteArray(); + val urlInfo = Protocol.URLInfo.parseFrom(urlBytes); + if (urlInfo.urlType != 4L) { + return null + } + + val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body); + return dataLink +} + fun Protocol.Claim.resolveChannelUrl(): String? { return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) }) } fun Protocol.Claim.resolveChannelUrls(): List { return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) }) -} - -suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() { - val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system)) - if (!systemState.servers.contains(PolycentricCache.SERVER)) { - Logger.w("Backfill", "Polycentric prod server not added, adding it.") - addServer(PolycentricCache.SERVER) - } - - val exceptions = fullyBackfillServers() - for (pair in exceptions) { - val server = pair.key - val exception = pair.value - - StateAnnouncement.instance.registerAnnouncement( - "backfill-failed", - "Backfill failed", - "Failed to backfill server $server. $exception", - AnnouncementType.SESSION_RECURRING - ); - - Logger.e("Backfill", "Failed to backfill server $server.", exception) - } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricCreateProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricCreateProfileActivity.kt index d5fce50b..f7432c05 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricCreateProfileActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricCreateProfileActivity.kt @@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs -import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.views.LoaderView +import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.Store +import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() { Logger.e(TAG, "Failed to save process secret to secret storage.", e) } - processHandle.addServer(PolycentricCache.SERVER); + processHandle.addServer(ApiMethods.SERVER); processHandle.setUsername(username); StatePolycentric.instance.setProcessHandle(processHandle); } catch (e: Throwable) { diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt index 825463b3..ab6d70a3 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt @@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.views.overlays.LoaderOverlay +import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.KeyPair import com.futo.polycentric.core.Process import com.futo.polycentric.core.ProcessSecret @@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() { } StatePolycentric.instance.setProcessHandle(processHandle); - processHandle.fullyBackfillClient(PolycentricCache.SERVER); + processHandle.fullyBackfillClient(ApiMethods.SERVER); withContext(Dispatchers.Main) { startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java)); finish(); diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt index e296a118..3493363e 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt @@ -21,10 +21,8 @@ import com.bumptech.glide.Glide import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.dp -import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.setNavigationBarColorAndIcons @@ -32,8 +30,10 @@ import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.overlays.LoaderOverlay +import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.Store import com.futo.polycentric.core.SystemState +import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl import com.futo.polycentric.core.toBase64Url import com.futo.polycentric.core.toURLInfoSystemLinkUrl @@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() { lifecycleScope.launch(Dispatchers.IO) { try { - processHandle.fullyBackfillClient(PolycentricCache.SERVER) + processHandle.fullyBackfillClient(ApiMethods.SERVER) withContext(Dispatchers.Main) { updateUI(); diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt index 9d0282f0..794c8537 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt @@ -22,7 +22,6 @@ import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComm import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.dp -import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp @@ -30,6 +29,7 @@ import com.futo.platformplayer.states.StatePolycentric import com.futo.polycentric.core.ClaimType import com.futo.polycentric.core.Store import com.futo.polycentric.core.SystemState +import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.google.android.material.button.MaterialButton diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt index 8a412f75..777443c5 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt @@ -13,7 +13,6 @@ import com.futo.platformplayer.R import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.dp import com.futo.platformplayer.fixHtmlLinks -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.selectBestImage @@ -21,6 +20,7 @@ import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.toHumanNumber import com.futo.platformplayer.views.platform.PlatformLinkView +import com.futo.polycentric.core.PolycentricProfile import com.futo.polycentric.core.toName import com.futo.polycentric.core.toURLInfoSystemLinkUrl @@ -134,9 +134,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment { } } if(!map.containsKey("Harbor")) - this.context?.let { - map.set("Harbor", polycentricProfile.getHarborUrl(it)); - } + map.set("Harbor", polycentricProfile.getHarborUrl()); if (map.isNotEmpty()) setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "") diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt index a7e313e3..b26c9b35 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt @@ -29,7 +29,6 @@ import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.fragment.mainactivity.main.FeedView -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StatePlatform @@ -39,6 +38,7 @@ import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter +import com.futo.polycentric.core.PolycentricProfile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.math.max diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt index 807fbd90..dc32acaa 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt @@ -16,12 +16,12 @@ import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder +import com.futo.polycentric.core.PolycentricProfile class ChannelListFragment : Fragment, IChannelTabFragment { private var _channels: ArrayList = arrayListOf(); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt index 53268d16..fd401042 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt @@ -8,8 +8,8 @@ import android.widget.TextView import androidx.fragment.app.Fragment import com.futo.platformplayer.R import com.futo.platformplayer.api.media.models.channels.IPlatformChannel -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.views.SupportView +import com.futo.polycentric.core.PolycentricProfile class ChannelMonetizationFragment : Fragment, IChannelTabFragment { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt index 2b615d25..3821b34c 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt @@ -1,7 +1,7 @@ package com.futo.platformplayer.fragment.channel.tab import com.futo.platformplayer.api.media.models.channels.IPlatformChannel -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile +import com.futo.polycentric.core.PolycentricProfile interface IChannelTabFragment { fun setChannel(channel: IPlatformChannel) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index 817a8ca2..6be65482 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -42,7 +42,6 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.SearchType import com.futo.platformplayer.models.Subscription -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectHighestResolutionImage import com.futo.platformplayer.states.StatePlatform @@ -55,29 +54,14 @@ import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.subscriptions.SubscribeButton -import com.futo.polycentric.core.OwnedClaim -import com.futo.polycentric.core.PublicKey -import com.futo.polycentric.core.Store -import com.futo.polycentric.core.SystemState -import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl +import com.futo.polycentric.core.ApiMethods +import com.futo.polycentric.core.PolycentricProfile import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable - -@Serializable -data class PolycentricProfile( - val system: PublicKey, val systemState: SystemState, val ownedClaims: List -) { - fun getHarborUrl(context: Context): String{ - val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system)); - val url = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable()); - return "https://harbor.social/" + url.substring("polycentric://".length); - } -} class ChannelFragment : MainFragment() { override val isMainView: Boolean = true @@ -144,15 +128,14 @@ class ChannelFragment : MainFragment() { private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {} - private val _taskLoadPolycentricProfile: TaskHandler + private val _taskLoadPolycentricProfile: TaskHandler private val _taskGetChannel: TaskHandler init { inflater.inflate(R.layout.fragment_channel, this) - _taskLoadPolycentricProfile = - TaskHandler({ fragment.lifecycleScope }, + _taskLoadPolycentricProfile = TaskHandler({ fragment.lifecycleScope }, { id -> - return@TaskHandler PolycentricCache.instance.getProfileAsync(id) + return@TaskHandler ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, id.claimFieldType.toLong(), id.claimType.toLong(), id.value!!) }).success { setPolycentricProfile(it, animate = true) }.exception { Logger.w(TAG, "Failed to load polycentric profile.", it) } @@ -328,7 +311,7 @@ class ChannelFragment : MainFragment() { _creatorThumbnail.setThumbnail(parameter.thumbnail, true) Glide.with(_imageBanner).clear(_imageBanner) - loadPolycentricProfile(parameter.id, parameter.url) + loadPolycentricProfile(parameter.id) } _url = parameter.url @@ -342,7 +325,7 @@ class ChannelFragment : MainFragment() { _creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true) Glide.with(_imageBanner).clear(_imageBanner) - loadPolycentricProfile(parameter.channel.id, parameter.channel.url) + loadPolycentricProfile(parameter.channel.id) } _url = parameter.channel.url @@ -359,16 +342,8 @@ class ChannelFragment : MainFragment() { _tabs.selectTab(_tabs.getTabAt(selectedTabIndex)) } - private fun loadPolycentricProfile(id: PlatformID, url: String) { - val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true) - if (cachedPolycentricProfile != null) { - setPolycentricProfile(cachedPolycentricProfile, animate = true) - if (cachedPolycentricProfile.expired) { - _taskLoadPolycentricProfile.run(id) - } - } else { - _taskLoadPolycentricProfile.run(id) - } + private fun loadPolycentricProfile(id: PlatformID) { + _taskLoadPolycentricProfile.run(id) } private fun setLoading(isLoading: Boolean) { @@ -533,20 +508,13 @@ class ChannelFragment : MainFragment() { private fun setPolycentricProfileOr(url: String, or: () -> Unit) { setPolycentricProfile(null, animate = false) - - val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) } - if (cachedProfile != null) { - setPolycentricProfile(cachedProfile, animate = false) - } else { - or() - } + or() } private fun setPolycentricProfile( - cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean + profile: PolycentricProfile?, animate: Boolean ) { val dp35 = 35.dp(resources) - val profile = cachedPolycentricProfile?.profile val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let { it.toURLInfoSystemLinkUrl( profile.system.toProto(), it.process, profile.systemState.servers.toList() diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt index dce725f6..0dae227d 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt @@ -23,7 +23,6 @@ import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.constructs.TaskHandler -import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePlatform @@ -32,6 +31,7 @@ import com.futo.platformplayer.views.adapters.CommentWithReferenceViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.overlays.RepliesOverlay import com.futo.polycentric.core.PublicKey +import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.net.UnknownHostException diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt index 86555b11..d4dc1672 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt @@ -33,10 +33,8 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.dp import com.futo.platformplayer.fixHtmlWhitespace -import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePlatform @@ -47,7 +45,6 @@ import com.futo.platformplayer.views.adapters.ChannelTab import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView import com.futo.platformplayer.views.comments.AddCommentView import com.futo.platformplayer.views.others.CreatorThumbnail -import com.futo.platformplayer.views.others.Toggle import com.futo.platformplayer.views.overlays.RepliesOverlay import com.futo.platformplayer.views.pills.PillRatingLikesDislikes import com.futo.platformplayer.views.platform.PlatformIndicator @@ -57,6 +54,8 @@ import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.Models import com.futo.polycentric.core.Opinion +import com.futo.polycentric.core.PolycentricProfile +import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions import com.google.android.flexbox.FlexboxLayout import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.shape.CornerFamily @@ -112,7 +111,7 @@ class PostDetailFragment : MainFragment { private var _isLoading = false; private var _post: IPlatformPostDetails? = null; private var _postOverview: IPlatformPost? = null; - private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null; + private var _polycentricProfile: PolycentricProfile? = null; private var _version = 0; private var _isRepliesVisible: Boolean = false; private var _repliesAnimator: ViewPropertyAnimator? = null; @@ -169,7 +168,7 @@ class PostDetailFragment : MainFragment { UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment); } else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope }; - private val _taskLoadPolycentricProfile = TaskHandler(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) }) + private val _taskLoadPolycentricProfile = TaskHandler(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }) .success { it -> setPolycentricProfile(it, animate = true) } .exception { Logger.w(TAG, "Failed to load claims.", it); @@ -274,7 +273,7 @@ class PostDetailFragment : MainFragment { }; _buttonStore.setOnClickListener { - _polycentricProfile?.profile?.systemState?.store?.let { + _polycentricProfile?.systemState?.store?.let { try { val uri = Uri.parse(it); val intent = Intent(Intent.ACTION_VIEW); @@ -334,7 +333,7 @@ class PostDetailFragment : MainFragment { } try { - val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null, + val queryReferencesResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, ref, null,null, arrayListOf( Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType( ContentType.OPINION.value).setValue( @@ -604,16 +603,8 @@ class PostDetailFragment : MainFragment { private fun fetchPolycentricProfile() { val author = _post?.author ?: _postOverview?.author ?: return; - val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url, true); - if (cachedPolycentricProfile != null) { - setPolycentricProfile(cachedPolycentricProfile, animate = false); - if (cachedPolycentricProfile.expired) { - _taskLoadPolycentricProfile.run(author.id); - } - } else { setPolycentricProfile(null, animate = false); _taskLoadPolycentricProfile.run(author.id); - } } private fun setChannelMeta(value: IPlatformPost?) { @@ -639,17 +630,18 @@ class PostDetailFragment : MainFragment { _repliesOverlay.cleanup(); } - private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { - _polycentricProfile = cachedPolycentricProfile; + private fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) { + _polycentricProfile = polycentricProfile; - if (cachedPolycentricProfile?.profile == null) { + val pp = _polycentricProfile; + if (pp == null) { _layoutMonetization.visibility = View.GONE; _creatorThumbnail.setHarborAvailable(false, animate, null); return; } _layoutMonetization.visibility = View.VISIBLE; - _creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto()); + _creatorThumbnail.setHarborAvailable(true, animate, pp.system.toProto()); } private fun fetchPost() { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index afda7722..8d930838 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -94,12 +94,10 @@ import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.exceptions.UnsupportedCastException import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlWhitespace -import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.getNowDiffSeconds import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Subscription -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.AnnouncementType @@ -158,6 +156,8 @@ import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.Models import com.futo.polycentric.core.Opinion +import com.futo.polycentric.core.PolycentricProfile +import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.google.protobuf.ByteString import kotlinx.coroutines.Dispatchers @@ -294,7 +294,7 @@ class VideoDetailView : ConstraintLayout { private set; private var _historicalPosition: Long = 0; private var _commentsCount = 0; - private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null; + private var _polycentricProfile: PolycentricProfile? = null; private var _slideUpOverlay: SlideUpMenuOverlay? = null; private var _autoplayVideo: IPlatformVideo? = null @@ -409,12 +409,12 @@ class VideoDetailView : ConstraintLayout { }; _monetization.onSupportTap.subscribe { - _container_content_support.setPolycentricProfile(_polycentricProfile?.profile); + _container_content_support.setPolycentricProfile(_polycentricProfile); switchContentView(_container_content_support); }; _monetization.onStoreTap.subscribe { - _polycentricProfile?.profile?.systemState?.store?.let { + _polycentricProfile?.systemState?.store?.let { try { val uri = Uri.parse(it); val intent = Intent(Intent.ACTION_VIEW); @@ -1236,16 +1236,8 @@ class VideoDetailView : ConstraintLayout { _creatorThumbnail.setThumbnail(video.author.thumbnail, false); _channelName.text = video.author.name; - val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true); - if (cachedPolycentricProfile != null) { - setPolycentricProfile(cachedPolycentricProfile, animate = false); - if (cachedPolycentricProfile.expired) { - _taskLoadPolycentricProfile.run(video.author.id); - } - } else { - setPolycentricProfile(null, animate = false); - _taskLoadPolycentricProfile.run(video.author.id); - } + setPolycentricProfile(null, animate = false); + _taskLoadPolycentricProfile.run(video.author.id); _player.clear(); @@ -1405,11 +1397,8 @@ class VideoDetailView : ConstraintLayout { setTabIndex(2, true) } else { when (Settings.instance.comments.defaultCommentSection) { - 0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex( - 0, - true - ) else setTabIndex(1, true); - 1 -> setTabIndex(1, true); + 0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true) + 1 -> setTabIndex(1, true) 2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true) } } @@ -1447,16 +1436,8 @@ class VideoDetailView : ConstraintLayout { _buttonSubscribe.setSubscribeChannel(video.author.url); setDescription(video.description.fixHtmlLinks()); _creatorThumbnail.setThumbnail(video.author.thumbnail, false); - - - val cachedPolycentricProfile = - PolycentricCache.instance.getCachedProfile(video.author.url, true); - if (cachedPolycentricProfile != null) { - setPolycentricProfile(cachedPolycentricProfile, animate = false); - } else { - setPolycentricProfile(null, animate = false); - _taskLoadPolycentricProfile.run(video.author.id); - } + setPolycentricProfile(null, animate = false); + _taskLoadPolycentricProfile.run(video.author.id); _platform.setPlatformFromClientID(video.id.pluginId); val subTitleSegments: ArrayList = ArrayList(); @@ -1485,7 +1466,7 @@ class VideoDetailView : ConstraintLayout { fragment.lifecycleScope.launch(Dispatchers.IO) { try { val queryReferencesResponse = ApiMethods.getQueryReferences( - PolycentricCache.SERVER, ref, null, null, + ApiMethods.SERVER, ref, null, null, arrayListOf( Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder() .setFromType(ContentType.OPINION.value).setValue( @@ -1501,10 +1482,8 @@ class VideoDetailView : ConstraintLayout { val likes = queryReferencesResponse.countsList[0]; val dislikes = queryReferencesResponse.countsList[1]; - val hasLiked = - StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/; - val hasDisliked = - StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/; + val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/; + val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/; withContext(Dispatchers.Main) { _rating.visibility = View.VISIBLE; @@ -2805,13 +2784,12 @@ class VideoDetailView : ConstraintLayout { } } - private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { - _polycentricProfile = cachedPolycentricProfile; + private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) { + _polycentricProfile = profile val dp_35 = 35.dp(context.resources) - val profile = cachedPolycentricProfile?.profile; val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35) - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; + ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) } if (avatar != null) { _creatorThumbnail.setThumbnail(avatar, animate); @@ -2820,12 +2798,12 @@ class VideoDetailView : ConstraintLayout { _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); } - val username = cachedPolycentricProfile?.profile?.systemState?.username + val username = profile?.systemState?.username if (username != null) { _channelName.text = username } - _monetization.setPolycentricProfile(cachedPolycentricProfile); + _monetization.setPolycentricProfile(profile); } fun setProgressBarOverlayed(isOverlayed: Boolean?) { @@ -3013,7 +2991,7 @@ class VideoDetailView : ConstraintLayout { Logger.w(TAG, "Failed to load recommendations.", it); }; - private val _taskLoadPolycentricProfile = TaskHandler(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) }) + private val _taskLoadPolycentricProfile = TaskHandler(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }) .success { it -> setPolycentricProfile(it, animate = true) } .exception { Logger.w(TAG, "Failed to load claims.", it); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/NavigationTopBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/NavigationTopBarFragment.kt index 4348edac..3309c851 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/NavigationTopBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/NavigationTopBarFragment.kt @@ -14,9 +14,9 @@ import com.futo.platformplayer.R import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.channels.IPlatformChannel -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.views.casting.CastButton +import com.futo.polycentric.core.PolycentricProfile class NavigationTopBarFragment : TopFragment() { private var _buttonBack: ImageButton? = null; diff --git a/app/src/main/java/com/futo/platformplayer/images/PolycentricModelLoader.java b/app/src/main/java/com/futo/platformplayer/images/PolycentricModelLoader.java index ded61ffc..c0155a33 100644 --- a/app/src/main/java/com/futo/platformplayer/images/PolycentricModelLoader.java +++ b/app/src/main/java/com/futo/platformplayer/images/PolycentricModelLoader.java @@ -1,5 +1,7 @@ package com.futo.platformplayer.images; +import static com.futo.platformplayer.Extensions_PolycentricKt.getDataLinkFromUrl; + import android.util.Log; import androidx.annotation.NonNull; @@ -12,10 +14,14 @@ import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.signature.ObjectKey; -import com.futo.platformplayer.polycentric.PolycentricCache; +import com.futo.polycentric.core.ApiMethods; import kotlin.Unit; +import kotlinx.coroutines.CoroutineScopeKt; import kotlinx.coroutines.Deferred; +import kotlinx.coroutines.Dispatchers; +import userpackage.Protocol; + import java.lang.Exception; import java.nio.ByteBuffer; import java.util.concurrent.CancellationException; @@ -60,7 +66,14 @@ public class PolycentricModelLoader implements ModelLoader { @Override public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback callback) { Log.i("PolycentricModelLoader", this._model); - _deferred = PolycentricCache.getInstance().getDataAsync(_model); + + Protocol.URLInfoDataLink dataLink = getDataLinkFromUrl(_model); + if (dataLink == null) { + callback.onLoadFailed(new Exception("Data link cannot be null")); + return; + } + + _deferred = ApiMethods.Companion.getDataFromServerAndReassemble(CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()), dataLink); _deferred.invokeOnCompletion(throwable -> { if (throwable != null) { Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString()); diff --git a/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt b/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt deleted file mode 100644 index abe4ca8e..00000000 --- a/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt +++ /dev/null @@ -1,353 +0,0 @@ -package com.futo.platformplayer.polycentric - -import com.futo.platformplayer.api.media.PlatformID -import com.futo.platformplayer.constructs.BatchedTaskHandler -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile -import com.futo.platformplayer.getNowDiffSeconds -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.resolveChannelUrls -import com.futo.platformplayer.serializers.OffsetDateTimeSerializer -import com.futo.platformplayer.states.StatePolycentric -import com.futo.platformplayer.stores.CachedPolycentricProfileStorage -import com.futo.platformplayer.stores.FragmentedStorage -import com.futo.polycentric.core.ApiMethods -import com.futo.polycentric.core.ContentType -import com.futo.polycentric.core.OwnedClaim -import com.futo.polycentric.core.PublicKey -import com.futo.polycentric.core.SignedEvent -import com.futo.polycentric.core.StorageTypeSystemState -import com.futo.polycentric.core.SystemState -import com.futo.polycentric.core.base64ToByteArray -import com.futo.polycentric.core.base64UrlToByteArray -import com.futo.polycentric.core.getClaimIfValid -import com.futo.polycentric.core.getValidClaims -import com.google.protobuf.ByteString -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.cancel -import kotlinx.serialization.Serializable -import userpackage.Protocol -import java.nio.ByteBuffer -import java.time.OffsetDateTime -import kotlin.system.measureTimeMillis - -class PolycentricCache { - data class CachedOwnedClaims(val ownedClaims: List?, val creationTime: OffsetDateTime = OffsetDateTime.now()) { - val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS - } - @Serializable - data class CachedPolycentricProfile(val profile: PolycentricProfile?, @Serializable(with = OffsetDateTimeSerializer::class) val creationTime: OffsetDateTime = OffsetDateTime.now()) { - val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS - } - - private val _cache = hashMapOf() - private val _profileCache = hashMapOf() - private val _profileUrlCache: CachedPolycentricProfileStorage; - private val _scope = CoroutineScope(Dispatchers.IO); - init { - Logger.i(TAG, "Initializing Polycentric cache"); - val time = measureTimeMillis { - _profileUrlCache = FragmentedStorage.get("profileUrlCache") - } - Logger.i(TAG, "Initialized Polycentric cache (${_profileUrlCache.map.size}, ${time}ms)"); - } - - private val _taskGetProfile = BatchedTaskHandler(_scope, - { system -> - val signedEventsList = ApiMethods.getQueryLatest( - SERVER, - system.toProto(), - listOf( - ContentType.BANNER.value, - ContentType.AVATAR.value, - ContentType.USERNAME.value, - ContentType.DESCRIPTION.value, - ContentType.STORE.value, - ContentType.SERVER.value, - ContentType.STORE_DATA.value, - ContentType.PROMOTION_BANNER.value, - ContentType.PROMOTION.value, - ContentType.MEMBERSHIP_URLS.value, - ContentType.DONATION_DESTINATIONS.value - ) - ).eventsList.map { e -> SignedEvent.fromProto(e) }; - - val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType } - .map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } }; - - val storageSystemState = StorageTypeSystemState.create() - for (signedEvent in signedProfileEvents) { - storageSystemState.update(signedEvent.event) - } - - val signedClaimEvents = ApiMethods.getQueryIndex( - SERVER, - system.toProto(), - ContentType.CLAIM.value, - limit = 200 - ).eventsList.map { e -> SignedEvent.fromProto(e) }; - - val ownedClaims: ArrayList = arrayListOf() - for (signedEvent in signedClaimEvents) { - if (signedEvent.event.contentType != ContentType.CLAIM.value) { - continue; - } - - val response = ApiMethods.getQueryReferences( - SERVER, - Protocol.Reference.newBuilder() - .setReference(signedEvent.toPointer().toProto().toByteString()) - .setReferenceType(2) - .build(), - null, - Protocol.QueryReferencesRequestEvents.newBuilder() - .setFromType(ContentType.VOUCH.value) - .build() - ); - - val ownedClaim = response.itemsList.map { SignedEvent.fromProto(it.event) }.getClaimIfValid(signedEvent); - if (ownedClaim != null) { - ownedClaims.add(ownedClaim); - } - } - - Logger.i(TAG, "Retrieved profile (ownedClaims = $ownedClaims)"); - val systemState = SystemState.fromStorageTypeSystemState(storageSystemState); - return@BatchedTaskHandler CachedPolycentricProfile(PolycentricProfile(system, systemState, ownedClaims)); - }, - { system -> return@BatchedTaskHandler getCachedProfile(system); }, - { system, result -> - synchronized(_cache) { - _profileCache[system] = result; - - if (result.profile != null) { - for (claim in result.profile.ownedClaims) { - val urls = claim.claim.resolveChannelUrls(); - for (url in urls) - _profileUrlCache.map[url] = result; - } - } - - _profileUrlCache.save(); - } - }); - - private val _batchTaskGetClaims = BatchedTaskHandler(_scope, - { id -> - val resolved = if (id.claimFieldType == -1) ApiMethods.getResolveClaim(SERVER, system, id.claimType.toLong(), id.value!!) - else ApiMethods.getResolveClaim(SERVER, system, id.claimType.toLong(), id.claimFieldType.toLong(), id.value!!); - Logger.v(TAG, "getResolveClaim(url = $SERVER, system = $system, id = $id, claimType = ${id.claimType}, matchAnyField = ${id.value})"); - val protoEvents = resolved.matchesList.flatMap { arrayListOf(it.claim).apply { addAll(it.proofChainList) } } - val resolvedEvents = protoEvents.map { i -> SignedEvent.fromProto(i) }; - return@BatchedTaskHandler CachedOwnedClaims(resolvedEvents.getValidClaims()); - }, - { id -> return@BatchedTaskHandler getCachedValidClaims(id); }, - { id, result -> - synchronized(_cache) { - _cache[id] = result; - } - }); - - private val _batchTaskGetData = BatchedTaskHandler(_scope, - { - val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported"); - return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink); - }, - { return@BatchedTaskHandler null }, - { _, _ -> }); - - fun getCachedValidClaims(id: PlatformID, ignoreExpired: Boolean = false): CachedOwnedClaims? { - if (!StatePolycentric.instance.enabled || id.claimType <= 0) { - return CachedOwnedClaims(null); - } - - synchronized(_cache) { - val cached = _cache[id] - if (cached == null) { - return null - } - - if (!ignoreExpired && cached.expired) { - return null; - } - - return cached; - } - } - - //TODO: Review all return null in this file, perhaps it should be CachedX(null) instead - fun getValidClaimsAsync(id: PlatformID): Deferred { - if (!StatePolycentric.instance.enabled || id.value == null || id.claimType <= 0) { - return _scope.async { CachedOwnedClaims(null) }; - } - - Logger.v(TAG, "getValidClaims (id: $id)") - val def = _batchTaskGetClaims.execute(id); - def.invokeOnCompletion { - if (it == null) { - return@invokeOnCompletion - } - - handleException(it, handleNetworkException = { /* Do nothing (do not cache) */ }, handleOtherException = { - //Cache failed result - synchronized(_cache) { - _cache[id] = CachedOwnedClaims(null); - } - }) - }; - return def; - } - - fun getDataAsync(url: String): Deferred { - StatePolycentric.instance.ensureEnabled() - return _batchTaskGetData.execute(url); - } - - fun getCachedProfile(url: String, ignoreExpired: Boolean = false): CachedPolycentricProfile? { - if (!StatePolycentric.instance.enabled) { - return CachedPolycentricProfile(null) - } - - synchronized (_profileCache) { - val cached = _profileUrlCache.get(url) ?: return null; - if (!ignoreExpired && cached.expired) { - return null; - } - - return cached; - } - } - - fun getCachedProfile(system: PublicKey, ignoreExpired: Boolean = false): CachedPolycentricProfile? { - if (!StatePolycentric.instance.enabled) { - return CachedPolycentricProfile(null) - } - - synchronized(_profileCache) { - val cached = _profileCache[system] ?: return null; - if (!ignoreExpired && cached.expired) { - return null; - } - - return cached; - } - } - - suspend fun getProfileAsync(id: PlatformID, urlNullCache: String? = null): CachedPolycentricProfile? { - if (!StatePolycentric.instance.enabled || id.claimType <= 0) { - return CachedPolycentricProfile(null); - } - - val cachedClaims = getCachedValidClaims(id); - if (cachedClaims != null) { - if (!cachedClaims.ownedClaims.isNullOrEmpty()) { - Logger.v(TAG, "getProfileAsync (id: $id) != null (with cached valid claims)") - return getProfileAsync(cachedClaims.ownedClaims.first().system).await(); - } else { - return null; - } - } else { - Logger.v(TAG, "getProfileAsync (id: $id) no cached valid claims, will be retrieved") - - val claims = getValidClaimsAsync(id).await() - if (!claims.ownedClaims.isNullOrEmpty()) { - Logger.v(TAG, "getProfileAsync (id: $id) != null (with retrieved valid claims)") - return getProfileAsync(claims.ownedClaims.first().system).await() - } else { - synchronized (_cache) { - if (urlNullCache != null) { - _profileUrlCache.setAndSave(urlNullCache, CachedPolycentricProfile(null)) - } - } - return null; - } - } - } - - fun getProfileAsync(system: PublicKey): Deferred { - if (!StatePolycentric.instance.enabled) { - return _scope.async { CachedPolycentricProfile(null) }; - } - - Logger.i(TAG, "getProfileAsync (system: ${system})") - val def = _taskGetProfile.execute(system); - def.invokeOnCompletion { - if (it == null) { - return@invokeOnCompletion - } - - handleException(it, handleNetworkException = { /* Do nothing (do not cache) */ }, handleOtherException = { - //Cache failed result - synchronized(_cache) { - val cachedProfile = CachedPolycentricProfile(null); - _profileCache[system] = cachedProfile; - } - }) - }; - return def; - } - - private fun handleException(e: Throwable, handleNetworkException: () -> Unit, handleOtherException: () -> Unit) { - val isNetworkException = when(e) { - is java.net.UnknownHostException, - is java.net.SocketTimeoutException, - is java.net.ConnectException -> true - else -> when(e.cause) { - is java.net.UnknownHostException, - is java.net.SocketTimeoutException, - is java.net.ConnectException -> true - else -> false - } - } - if (isNetworkException) { - handleNetworkException() - } else { - handleOtherException() - } - } - - companion object { - private val system = Protocol.PublicKey.newBuilder() - .setKeyType(1) - .setKey(ByteString.copyFrom("gX0eCWctTm6WHVGot4sMAh7NDAIwWsIM5tRsOz9dX04=".base64ToByteArray())) //Production key - //.setKey(ByteString.copyFrom("LeQkzn1j625YZcZHayfCmTX+6ptrzsA+CdAyq+BcEdQ".base64ToByteArray())) //Test key koen-futo - .build(); - - private const val TAG = "PolycentricCache" - const val SERVER = "https://srv1-prod.polycentric.io" - private var _instance: PolycentricCache? = null; - private val CACHE_EXPIRATION_SECONDS = 60 * 5; - - @JvmStatic - val instance: PolycentricCache - get(){ - if(_instance == null) - _instance = PolycentricCache(); - return _instance!!; - }; - - fun finish() { - _instance?.let { - _instance = null; - it._scope.cancel("PolycentricCache finished"); - } - } - - fun getDataLinkFromUrl(it: String): Protocol.URLInfoDataLink? { - val urlData = if (it.startsWith("polycentric://")) { - it.substring("polycentric://".length) - } else it; - - val urlBytes = urlData.base64UrlToByteArray(); - val urlInfo = Protocol.URLInfo.parseFrom(urlBytes); - if (urlInfo.urlType != 4L) { - return null - } - - val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body); - return dataLink - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StateCache.kt b/app/src/main/java/com/futo/platformplayer/states/StateCache.kt index 7d91c9ce..8157ed2a 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateCache.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateCache.kt @@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.serializers.PlatformContentSerializer import com.futo.platformplayer.stores.db.ManagedDBStore @@ -50,14 +49,7 @@ class StateCache { val subs = StateSubscriptions.instance.getSubscriptions(); Logger.i(TAG, "Subscriptions CachePager polycentric urls"); val allUrls = subs - .map { - val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf(); - if(!otherUrls.contains(it.channel.url)) - return@map listOf(listOf(it.channel.url), otherUrls).flatten(); - else - return@map otherUrls; - } - .flatten() + .map { it.channel.url } .distinct() .filter { StatePlatform.instance.hasEnabledChannelClient(it) }; diff --git a/app/src/main/java/com/futo/platformplayer/states/StateMeta.kt b/app/src/main/java/com/futo/platformplayer/states/StateMeta.kt index b20866bf..bd45b205 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateMeta.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateMeta.kt @@ -14,7 +14,7 @@ class StateMeta { return when(lastCommentSection.value){ "Polycentric" -> 0; "Platform" -> 1; - else -> 1 + else -> 0 } } fun setLastCommentSection(value: Int) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt index e98aff0c..9d6f7437 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt @@ -21,9 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.awaitFirstDeferred import com.futo.platformplayer.dp -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.selectBestImage @@ -33,6 +31,7 @@ import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.ClaimType import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.Opinion +import com.futo.polycentric.core.PolycentricProfile import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.PublicKey import com.futo.polycentric.core.SignedEvent @@ -234,34 +233,7 @@ class StatePolycentric { if (!enabled) { return Pair(false, listOf(url)); } - var polycentricProfile: PolycentricProfile? = null; - try { - val polycentricCached = PolycentricCache.instance.getCachedProfile(url, cacheOnly) - polycentricProfile = polycentricCached?.profile; - if (polycentricCached == null && channelId != null) { - Logger.i("StateSubscriptions", "Get polycentric profile not cached"); - if(!cacheOnly) { - polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId, if(doCacheNull) url else null) }?.profile; - didUpdate = true; - } - } else { - Logger.i("StateSubscriptions", "Get polycentric profile cached"); - } - } - catch(ex: Throwable) { - Logger.w(StateSubscriptions.TAG, "Polycentric getCachedProfile failed for subscriptions", ex); - //TODO: Some way to communicate polycentric failing without blocking here - } - if(polycentricProfile != null) { - val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType } - .mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList(); - if(urls.any { it.equals(url, true) }) - return Pair(didUpdate, urls); - else - return Pair(didUpdate, listOf(url) + urls); - } - else - return Pair(didUpdate, listOf(url)); + return Pair(didUpdate, listOf(url)); } fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager? { @@ -325,7 +297,7 @@ class StatePolycentric { id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()), name = systemState.username, url = author, - thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) }, + thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(ApiMethods.SERVER)) }, subscribers = null ), msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content, @@ -349,7 +321,7 @@ class StatePolycentric { suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies { ensureEnabled() - val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null, + val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null, null, listOf( Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder() @@ -382,7 +354,7 @@ class StatePolycentric { } val pointer = Protocol.Pointer.parseFrom(reference.reference) - val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder() + val events = ApiMethods.getEvents(ApiMethods.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder() .addRangesForProcesses(Protocol.RangesForProcess.newBuilder() .setProcess(pointer.process) .addRanges(Protocol.Range.newBuilder() @@ -400,11 +372,11 @@ class StatePolycentric { } val post = Protocol.Post.parseFrom(ev.content); - val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER)); + val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER)); val dp_25 = 25.dp(StateApp.instance.context.resources) val profileEvents = ApiMethods.getQueryLatest( - PolycentricCache.SERVER, + ApiMethods.SERVER, ev.system.toProto(), listOf( ContentType.AVATAR.value, @@ -433,7 +405,7 @@ class StatePolycentric { id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()), name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown", url = systemLinkUrl, - thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) }, + thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) }, subscribers = null ), msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content, @@ -445,12 +417,12 @@ class StatePolycentric { ) } - suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List? = null): IPager { + suspend fun getCommentPager(contextUrl: String, reference: Reference, extraByteReferences: List? = null): IPager { if (!enabled) { return EmptyPager() } - val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null, + val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null, Protocol.QueryReferencesRequestEvents.newBuilder() .setFromType(ContentType.POST.value) .addAllCountLwwElementReferences(arrayListOf( @@ -486,7 +458,7 @@ class StatePolycentric { } override suspend fun nextPageAsync() { - val nextPageResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, _cursor, + val nextPageResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, _cursor, Protocol.QueryReferencesRequestEvents.newBuilder() .setFromType(ContentType.POST.value) .addAllCountLwwElementReferences(arrayListOf( @@ -534,7 +506,7 @@ class StatePolycentric { return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){ Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]"); val profileEvents = ApiMethods.getQueryLatest( - PolycentricCache.SERVER, + ApiMethods.SERVER, ev.system.toProto(), listOf( ContentType.AVATAR.value, @@ -558,7 +530,7 @@ class StatePolycentric { val unixMilliseconds = ev.unixMilliseconds //TODO: Don't use single hardcoded sderver here - val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER)); + val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER)); val dp_25 = 25.dp(StateApp.instance.context.resources) return@async PolycentricPlatformComment( contextUrl = contextUrl, @@ -566,7 +538,7 @@ class StatePolycentric { id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()), name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown", url = systemLinkUrl, - thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) }, + thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) }, subscribers = null ), msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content, diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt index 7da01216..f979251d 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt @@ -1,54 +1,17 @@ package com.futo.platformplayer.states -import com.futo.platformplayer.Settings -import com.futo.platformplayer.UIDialogs -import com.futo.platformplayer.api.media.models.ResultCapabilities -import com.futo.platformplayer.api.media.models.channels.IPlatformChannel -import com.futo.platformplayer.api.media.models.channels.SerializedChannel -import com.futo.platformplayer.api.media.models.contents.IPlatformContent -import com.futo.platformplayer.api.media.platforms.js.JSClient -import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig -import com.futo.platformplayer.api.media.structures.* -import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable import com.futo.platformplayer.constructs.Event0 -import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.constructs.Event2 -import com.futo.platformplayer.engine.exceptions.PluginException -import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException -import com.futo.platformplayer.engine.exceptions.ScriptCriticalException -import com.futo.platformplayer.exceptions.ChannelException -import com.futo.platformplayer.findNonRuntimeException -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile -import com.futo.platformplayer.getNowDiffDays import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.SubscriptionGroup -import com.futo.platformplayer.polycentric.PolycentricCache -import com.futo.platformplayer.resolveChannelUrl -import com.futo.platformplayer.states.StateHistory.Companion import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.StringDateMapStorage -import com.futo.platformplayer.stores.SubscriptionStorage -import com.futo.platformplayer.stores.v2.ReconstructStore -import com.futo.platformplayer.stores.v2.ManagedStore -import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm -import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms import com.futo.platformplayer.sync.internal.GJSyncOpcodes import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage -import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.time.OffsetDateTime -import java.util.concurrent.ExecutionException -import java.util.concurrent.ForkJoinPool -import java.util.concurrent.ForkJoinTask -import kotlin.collections.ArrayList -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine -import kotlin.streams.asSequence -import kotlin.streams.toList -import kotlin.system.measureTimeMillis /*** * Used to maintain subscription groups diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index 52fb9f2e..65892a1e 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -15,7 +15,6 @@ import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.ImportCache import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.SubscriptionGroup -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.StringDateMapStorage @@ -335,12 +334,6 @@ class StateSubscriptions { return true; } - //TODO: This causes issues, because what if the profile is not cached yet when the susbcribe button is loaded for example? - val cachedProfile = PolycentricCache.instance.getCachedProfile(urls.first(), true)?.profile; - if (cachedProfile != null) { - return cachedProfile.ownedClaims.any { c -> _subscriptions.hasItem { s -> c.claim.resolveChannelUrl() == s.channel.url } }; - } - return false; } } diff --git a/app/src/main/java/com/futo/platformplayer/stores/CachedPolycentricProfileStorage.kt b/app/src/main/java/com/futo/platformplayer/stores/CachedPolycentricProfileStorage.kt deleted file mode 100644 index b9c036ac..00000000 --- a/app/src/main/java/com/futo/platformplayer/stores/CachedPolycentricProfileStorage.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.futo.platformplayer.stores - -import com.futo.platformplayer.polycentric.PolycentricCache -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -@kotlinx.serialization.Serializable -class CachedPolycentricProfileStorage : FragmentedStorageFileJson() { - var map: HashMap = hashMapOf(); - - override fun encode(): String { - val encoded = Json.encodeToString(this); - return encoded; - } - - fun get(key: String) : PolycentricCache.CachedPolycentricProfile? { - return map[key]; - } - - fun setAndSave(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile { - map[key] = value; - save(); - return value; - } - - fun setAndSaveBlocking(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile { - map[key] = value; - saveBlocking(); - return value; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt index 62da748b..b109acb5 100644 --- a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt @@ -16,11 +16,11 @@ import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder import com.futo.platformplayer.views.platform.PlatformIndicator +import com.futo.polycentric.core.PolycentricProfile import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -125,8 +125,7 @@ class MonetizationView : LinearLayout { } } - fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?) { - val profile = cachedPolycentricProfile?.profile; + fun setPolycentricProfile(profile: PolycentricProfile?) { if (profile != null) { if (profile.systemState.store.isNotEmpty()) { _buttonStore.visibility = View.VISIBLE; diff --git a/app/src/main/java/com/futo/platformplayer/views/SupportView.kt b/app/src/main/java/com/futo/platformplayer/views/SupportView.kt index ad3017e7..c85d3450 100644 --- a/app/src/main/java/com/futo/platformplayer/views/SupportView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/SupportView.kt @@ -14,10 +14,10 @@ import androidx.core.view.isVisible import androidx.core.view.size import com.bumptech.glide.Glide import com.futo.platformplayer.R -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.views.buttons.BigButton +import com.futo.polycentric.core.PolycentricProfile import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.google.android.material.imageview.ShapeableImageView diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt index 665829db..3bd06903 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt @@ -17,7 +17,7 @@ import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile +import com.futo.polycentric.core.PolycentricProfile import com.google.android.material.tabs.TabLayout diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt index fe9c6079..74f7d53c 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt @@ -18,8 +18,6 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.fixHtmlLinks -import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions -import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric @@ -29,6 +27,7 @@ import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.pills.PillButton import com.futo.platformplayer.views.pills.PillRatingLikesDislikes +import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.Opinion import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -81,24 +80,18 @@ class CommentViewHolder : ViewHolder { throw Exception("Not implemented for non polycentric comments") } - if (args.hasLiked) { - args.processHandle.opinion(c.reference, Opinion.like); + val newOpinion: Opinion = if (args.hasLiked) { + Opinion.like } else if (args.hasDisliked) { - args.processHandle.opinion(c.reference, Opinion.dislike); + Opinion.dislike } else { - args.processHandle.opinion(c.reference, Opinion.neutral); + Opinion.neutral } _layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f; StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { - try { - Logger.i(TAG, "Started backfill"); - args.processHandle.fullyBackfillServersAnnounceExceptions(); - Logger.i(TAG, "Finished backfill"); - } catch (e: Throwable) { - Logger.e(TAG, "Failed to backfill servers.", e) - } + ApiMethods.setOpinion(args.processHandle, c.reference, newOpinion) } StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked) diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt index db66a1a9..c4b9a51e 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt @@ -16,7 +16,6 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.fixHtmlLinks -import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod import com.futo.platformplayer.states.StateApp @@ -26,6 +25,7 @@ import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.pills.PillButton import com.futo.platformplayer.views.pills.PillRatingLikesDislikes import com.futo.polycentric.core.Opinion +import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.IdentityHashMap diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt index c8d86b14..c9cb8b73 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt @@ -16,7 +16,6 @@ import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.others.CreatorThumbnail diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt index 05a3f5e7..a9e7110a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt @@ -15,7 +15,6 @@ import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.dp import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Subscription -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.toHumanTimeIndicator diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewPostView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewPostView.kt index cbf0b8a6..1d90e09c 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewPostView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewPostView.kt @@ -30,7 +30,6 @@ import com.futo.platformplayer.dp import com.futo.platformplayer.fixHtmlWhitespace import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.views.FeedStyle diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt index f2123832..75d332e5 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt @@ -24,7 +24,6 @@ import com.futo.platformplayer.getNowDiffSeconds import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateDownloads diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt index 897718bf..9a171df6 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt @@ -11,7 +11,6 @@ import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.dp import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.views.adapters.AnyAdapter diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt index b5784d4c..93576fec 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt @@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.dp import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.toHumanNumber diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt index da491cf6..e56fcf3a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt @@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.dp import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Subscription -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.views.adapters.AnyAdapter diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt index 19fe8a30..4f601d26 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt @@ -19,7 +19,6 @@ import com.futo.platformplayer.dp import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.SubscriptionGroup -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.views.adapters.AnyAdapter diff --git a/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt b/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt index 2f592123..472a516f 100644 --- a/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt +++ b/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt @@ -11,8 +11,8 @@ import androidx.constraintlayout.widget.ConstraintLayout import com.bumptech.glide.Glide import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.getDataLinkFromUrl import com.futo.platformplayer.images.GlideHelper.Companion.crossfade -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.views.IdenticonView import userpackage.Protocol @@ -68,7 +68,7 @@ class CreatorThumbnail : ConstraintLayout { if (url.startsWith("polycentric://")) { try { - val dataLink = PolycentricCache.getDataLinkFromUrl(url) + val dataLink = url.getDataLinkFromUrl() setHarborAvailable(true, animate, dataLink?.system); } catch (e: Throwable) { setHarborAvailable(false, animate, null); diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/SupportOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/SupportOverlay.kt index dfc59e05..e451806c 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/SupportOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/SupportOverlay.kt @@ -5,8 +5,8 @@ import android.util.AttributeSet import android.widget.LinearLayout import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event0 -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.views.SupportView +import com.futo.polycentric.core.PolycentricProfile class SupportOverlay : LinearLayout { val onClose = Event0(); diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt index 06deadb8..27befb1e 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt @@ -6,9 +6,7 @@ import android.webkit.WebView import android.widget.LinearLayout import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event0 -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.views.SupportView class WebviewOverlay : LinearLayout { val onClose = Event0(); diff --git a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt index 965d3014..a1ccd142 100644 --- a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt +++ b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt @@ -22,12 +22,12 @@ import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException -import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.views.adapters.CommentViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader +import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.net.UnknownHostException diff --git a/dep/polycentricandroid b/dep/polycentricandroid index 44edd69e..f87f00ab 160000 --- a/dep/polycentricandroid +++ b/dep/polycentricandroid @@ -1 +1 @@ -Subproject commit 44edd69ece9cac4a6dd95a84ca91299e44f3650a +Subproject commit f87f00ab9e1262e300246b8963591bdf3a8fada7 From f63f9dd6db52f02d0efc44e1c952c3cebc3f14a5 Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 7 Mar 2025 14:27:18 -0600 Subject: [PATCH 032/330] initial POC shorts tab Changelog: added --- app/build.gradle | 11 + .../platformplayer/activities/MainActivity.kt | 8 +- .../bottombar/MenuBottomBarFragment.kt | 15 +- .../fragment/mainactivity/main/ShortView.kt | 427 ++++++++++++++++++ .../mainactivity/main/ShortsFragment.kt | 96 ++++ .../futo/platformplayer/states/StatePlayer.kt | 11 + .../views/video/FutoShortPlayer.kt | 179 ++++++++ .../views/video/FutoVideoPlayerBase.kt | 3 +- app/src/main/res/layout/activity_main.xml | 7 +- .../layout/fragment_overview_bottom_bar.xml | 2 +- app/src/main/res/layout/fragment_shorts.xml | 6 + app/src/main/res/layout/modal_comments.xml | 231 ++++++++++ app/src/main/res/layout/view_short.xml | 43 ++ app/src/main/res/layout/view_short_player.xml | 31 ++ app/src/main/res/values/colors.xml | 143 ++++++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/themes.xml | 36 ++ 17 files changed, 1235 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt create mode 100644 app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt create mode 100644 app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt create mode 100644 app/src/main/res/layout/fragment_shorts.xml create mode 100644 app/src/main/res/layout/modal_comments.xml create mode 100644 app/src/main/res/layout/view_short.xml create mode 100644 app/src/main/res/layout/view_short_player.xml diff --git a/app/build.gradle b/app/build.gradle index 8d55d000..5f38c749 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,6 +143,10 @@ android { } buildFeatures { buildConfig true + compose true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.2" } sourceSets { main { @@ -215,6 +219,7 @@ dependencies { //Database implementation("androidx.room:room-runtime:2.6.1") annotationProcessor("androidx.room:room-compiler:2.6.1") + debugImplementation 'androidx.compose.ui:ui-tooling:1.7.8' ksp("androidx.room:room-compiler:2.6.1") implementation("androidx.room:room-ktx:2.6.1") @@ -228,4 +233,10 @@ dependencies { testImplementation "org.mockito:mockito-core:5.4.0" androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + //Compose + def composeBom = platform('androidx.compose:compose-bom:2025.02.00') + implementation composeBom + androidTestImplementation composeBom + implementation 'androidx.compose.material3:material3' } diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 25febdb1..647cf6d8 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -1,13 +1,11 @@ package com.futo.platformplayer.activities import android.annotation.SuppressLint -import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.PackageManager import android.content.res.Configuration -import android.media.AudioManager import android.net.Uri import android.os.Bundle import android.os.StrictMode @@ -57,6 +55,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsF import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment +import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment @@ -74,7 +73,6 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.ImportCache import com.futo.platformplayer.models.UrlVideoWithTime -import com.futo.platformplayer.receivers.MediaButtonReceiver import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateBackup @@ -161,6 +159,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment; lateinit var _fragWatchlist: WatchLaterFragment; lateinit var _fragHistory: HistoryFragment; + lateinit var _fragShorts: ShortsFragment; lateinit var _fragSourceDetail: SourceDetailFragment; lateinit var _fragDownloads: DownloadsFragment; lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment; @@ -315,6 +314,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragPostDetail = PostDetailFragment.newInstance(); _fragWatchlist = WatchLaterFragment.newInstance(); _fragHistory = HistoryFragment.newInstance(); + _fragShorts = ShortsFragment.newInstance(); _fragSourceDetail = SourceDetailFragment.newInstance(); _fragDownloads = DownloadsFragment(); _fragImportSubscriptions = ImportSubscriptionsFragment.newInstance(); @@ -1088,6 +1088,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { if (segment.isMainView) { var transaction = supportFragmentManager.beginTransaction(); + transaction.setReorderingAllowed(true) if (segment.topBar != null) { if (segment.topBar != fragCurrent.topBar) { transaction = transaction @@ -1188,6 +1189,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { PostDetailFragment::class -> _fragPostDetail as T; WatchLaterFragment::class -> _fragWatchlist as T; HistoryFragment::class -> _fragHistory as T; + ShortsFragment::class -> _fragShorts as T; SourceDetailFragment::class -> _fragSourceDetail as T; DownloadsFragment::class -> _fragDownloads as T; ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index c4afcf74..2b7c055a 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -386,16 +386,17 @@ class MenuBottomBarFragment : MainActivityFragment() { it.navigate() } }), - ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate() }), - ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate() }), - ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate() }), - ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate() }), - ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate() }), - ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate() }), + ButtonDefinition(1, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.shorts, canToggle = true, { it.currentMain is ShortsFragment }, { it.navigate() }), + ButtonDefinition(2, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate() }), + ButtonDefinition(3, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate() }), + ButtonDefinition(4, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate() }), + ButtonDefinition(5, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate() }), + ButtonDefinition(6, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate() }), + ButtonDefinition(7, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate() }), ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate() }), ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate() }), ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate() }), - ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, { + ButtonDefinition(11, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, { val c = it.context ?: return@ButtonDefinition; Logger.i(TAG, "settings preventPictureInPicture()"); it.requireFragment().preventPictureInPicture(); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt new file mode 100644 index 00000000..bba0dcee --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -0,0 +1,427 @@ +package com.futo.platformplayer.fragment.mainactivity.main + +import android.app.Dialog +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Animatable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.SoundEffectConstants +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ThumbUp +import androidx.compose.material.icons.outlined.ThumbUp +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.LocalRippleConfiguration +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RippleConfiguration +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.lifecycle.lifecycleScope +import androidx.media3.common.util.UnstableApi +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.futo.platformplayer.R +import com.futo.platformplayer.Settings +import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException +import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException +import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource +import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource +import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource +import com.futo.platformplayer.api.media.models.video.IPlatformVideo +import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails +import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.downloads.VideoLocal +import com.futo.platformplayer.engine.exceptions.ScriptAgeException +import com.futo.platformplayer.engine.exceptions.ScriptException +import com.futo.platformplayer.engine.exceptions.ScriptImplementationException +import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException +import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException +import com.futo.platformplayer.exceptions.UnsupportedCastException +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.states.StatePlugins +import com.futo.platformplayer.views.video.FutoShortPlayer +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.OffsetDateTime +import kotlin.coroutines.cancellation.CancellationException + +@OptIn(ExperimentalMaterial3Api::class) +@UnstableApi +class ShortView : ConstraintLayout { + private var mainFragment: MainFragment? = null + private val player: FutoShortPlayer + private val overlayLoading: FrameLayout + private val overlayLoadingSpinner: ImageView + + private var url: String? = null + private var video: IPlatformVideo? = null + private var videoDetails: IPlatformVideoDetails? = null + + private var playWhenReady = false + + private var _lastVideoSource: IVideoSource? = null + private var _lastAudioSource: IAudioSource? = null + private var _lastSubtitleSource: ISubtitleSource? = null + + private var loadVideoJob: Job? = null + + private val bottomSheet: ModalBottomSheet = ModalBottomSheet() + + // Required constructor for XML inflation + constructor(context: Context) : super(context) { + inflate(context, R.layout.view_short, this) + + player = findViewById(R.id.short_player) + overlayLoading = findViewById(R.id.short_view_loading_overlay) + overlayLoadingSpinner = findViewById(R.id.short_view_loader) + + setupComposeView() + } + + // Required constructor for XML inflation with attributes + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + inflate(context, R.layout.view_short, this) + + player = findViewById(R.id.short_player) + overlayLoading = findViewById(R.id.short_view_loading_overlay) + overlayLoadingSpinner = findViewById(R.id.short_view_loader) + + setupComposeView() + } + + // Required constructor for XML inflation with attributes and style + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + inflate(context, R.layout.view_short, this) + + player = findViewById(R.id.short_player) + overlayLoading = findViewById(R.id.short_view_loading_overlay) + overlayLoadingSpinner = findViewById(R.id.short_view_loader) + + setupComposeView() + } + + constructor(inflater: LayoutInflater, fragment: MainFragment) : super(inflater.context) { + this.mainFragment = fragment + + inflater.inflate(R.layout.view_short, this, true) + player = findViewById(R.id.short_player) + overlayLoading = findViewById(R.id.short_view_loading_overlay) + overlayLoadingSpinner = findViewById(R.id.short_view_loader) + + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT + ) + } + + private fun setupComposeView () { + val composeView: ComposeView = findViewById(R.id.compose_view_test_button) + composeView.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + // In Compose world + MaterialTheme { + var checked by remember { mutableStateOf(false) } + + val tint = Color.White + + val alpha = 0.2f + val rippleConfiguration = + RippleConfiguration(color = tint, rippleAlpha = RippleAlpha(alpha, alpha, alpha, alpha)) + + val view = LocalView.current + + CompositionLocalProvider(LocalRippleConfiguration provides rippleConfiguration) { + IconToggleButton( + checked = checked, + onCheckedChange = { + checked = it + view.playSoundEffect(SoundEffectConstants.CLICK) + }, + ) { + if (checked) { + Icon( + Icons.Filled.ThumbUp, contentDescription = "Liked", tint = tint, + ) + } else { + Icon( + Icons.Outlined.ThumbUp, contentDescription = "Not Liked", tint = tint, + ) + } + } + } + } + } + } + } + + fun setMainFragment(fragment: MainFragment) { + this.mainFragment = fragment + } + + fun setVideo(url: String) { + if (url == this.url) { + return + } + + loadVideo(url) + } + + fun setVideo(video: IPlatformVideo) { + if (url == video.url) { + return + } + this.video = video + + loadVideo(video.url) + } + + fun setVideo(videoDetails: IPlatformVideoDetails) { + if (url == videoDetails.url) { + return + } + + this.videoDetails = videoDetails + } + + fun play() { + player.attach() + playVideo() + } + + fun stop() { + playWhenReady = false + + player.clear() + player.detach() + } + + fun detach() { + loadVideoJob?.cancel() + } + + private fun setLoading(isLoading: Boolean) { + if (isLoading) { + (overlayLoadingSpinner.drawable as Animatable?)?.start() + overlayLoading.visibility = View.VISIBLE + } else { + overlayLoading.visibility = View.GONE + (overlayLoadingSpinner.drawable as Animatable?)?.stop() + } + } + + private fun loadVideo(url: String) { + loadVideoJob?.cancel() + + loadVideoJob = CoroutineScope(Dispatchers.Main).launch { + setLoading(true) + _lastVideoSource = null + _lastAudioSource = null + _lastSubtitleSource = null + + val result = try { + withContext(StateApp.instance.scope.coroutineContext) { + StatePlatform.instance.getContentDetails(url).await() + } + } catch (e: CancellationException) { + return@launch + } catch (e: NoPlatformClientException) { + Logger.w(TAG, "exception", e) + + UIDialogs.showDialog( + context, R.drawable.ic_sources, "No source enabled to support this video\n(${url})", null, null, 0, UIDialogs.Action( + "Close", { }, UIDialogs.ActionStyle.PRIMARY + ) + ) + return@launch + } catch (e: ScriptLoginRequiredException) { + Logger.w(TAG, "exception", e) + UIDialogs.showDialog(context, R.drawable.ic_security, "Authentication", e.message, null, 0, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Login", { + val id = e.config.let { if (it is SourcePluginConfig) it.id else null } + val didLogin = + if (id == null) false else StatePlugins.instance.loginPlugin(context, id) { + loadVideo(url) + } + if (!didLogin) UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login") + }, UIDialogs.ActionStyle.PRIMARY) + ) + return@launch + } catch (e: ContentNotAvailableYetException) { + Logger.w(TAG, "exception", e) + UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${e.availableWhen}.", "Close") { } + return@launch + } catch (e: ScriptImplementationException) { + Logger.w(TAG, "exception", e) + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), e, { loadVideo(url) }, null, mainFragment) + return@launch + } catch (e: ScriptAgeException) { + Logger.w(TAG, "exception", e) + UIDialogs.showDialog( + context, R.drawable.ic_lock, "Age restricted video", e.message, null, 0, UIDialogs.Action("Close", { }, UIDialogs.ActionStyle.PRIMARY) + ) + return@launch + } catch (e: ScriptUnavailableException) { + Logger.w(TAG, "exception", e) + if (video?.datetime == null || video?.datetime!! < OffsetDateTime.now() + .minusHours(1) + ) { + UIDialogs.showDialog( + context, R.drawable.ic_lock, context.getString(R.string.unavailable_video), context.getString(R.string.this_video_is_unavailable), null, 0, UIDialogs.Action(context.getString(R.string.close), { }, UIDialogs.ActionStyle.PRIMARY) + ) + } + + video?.let { StatePlatform.instance.clearContentDetailCache(it.url) } + return@launch + } catch (e: ScriptException) { + Logger.w(TAG, "exception", e) + + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), e, { loadVideo(url) }, null, mainFragment) + return@launch + } catch (e: Throwable) { + Logger.w(ChannelFragment.TAG, "Failed to load video.", e) + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), e, { loadVideo(url) }, null, mainFragment) + return@launch + } + + if (result !is IPlatformVideoDetails) { + Logger.w( + TAG, "Wrong content type", IllegalStateException("Expected media content, found ${result.contentType}") + ) + return@launch + } + + // if it's been canceled then don't set the video details + if (!isActive) { + return@launch + } + + videoDetails = result + video = result + + setLoading(false) + + if (playWhenReady) playVideo() + } + } + + private fun playVideo(resumePositionMs: Long = 0) { + val video = videoDetails + + if (video === null) { + playWhenReady = true + return + } + + bottomSheet.show(mainFragment!!.childFragmentManager, ModalBottomSheet.TAG) + + try { + val videoSource = _lastVideoSource + ?: player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount()) + val audioSource = _lastAudioSource + ?: player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context)) + val subtitleSource = _lastSubtitleSource + ?: (if (video is VideoLocal) video.subtitlesSources.firstOrNull() else null) + Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)") + + if (videoSource == null && audioSource == null) { + UIDialogs.showDialog( + context, R.drawable.ic_lock, context.getString(R.string.unavailable_video), context.getString(R.string.this_video_is_unavailable), null, 0, UIDialogs.Action(context.getString(R.string.close), { }, UIDialogs.ActionStyle.PRIMARY) + ) + StatePlatform.instance.clearContentDetailCache(video.url) + return + } + + val thumbnail = video.thumbnails.getHQThumbnail() + if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap() + .load(thumbnail).into(object : CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + player.setArtwork(BitmapDrawable(resources, resource)) + } + + override fun onLoadCleared(placeholder: Drawable?) { + player.setArtwork(null) + } + }) + else player.setArtwork(null) + player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0) + if (subtitleSource != null) player.swapSubtitles(mainFragment!!.lifecycleScope, subtitleSource) + player.seekTo(resumePositionMs) + + _lastVideoSource = videoSource + _lastAudioSource = audioSource + _lastSubtitleSource = subtitleSource + } catch (ex: UnsupportedCastException) { + Logger.e(TAG, "Failed to load cast media", ex) + UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.unsupported_cast_format), ex) + } catch (ex: Throwable) { + Logger.e(TAG, "Failed to load media", ex) + UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex) + } + } + + companion object { + const val TAG = "VideoDetailView" + } + + class ModalBottomSheet : BottomSheetDialogFragment() { + override fun onCreateDialog( + savedInstanceState: Bundle?, + ): Dialog { + val bottomSheetDialog = BottomSheetDialog( + requireContext() + ) + bottomSheetDialog.setContentView(R.layout.modal_comments) + + val composeView = bottomSheetDialog.findViewById(R.id.compose_view) + + composeView?.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + // In Compose world + MaterialTheme { + val view = LocalView.current + IconButton(onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + }) { + Icon( + Icons.Outlined.ThumbUp, contentDescription = "Close Bottom Sheet" + ) + } + } + } + } + return bottomSheetDialog + } + + companion object { + const val TAG = "ModalBottomSheet" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt new file mode 100644 index 00000000..b11a30a4 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt @@ -0,0 +1,96 @@ +package com.futo.platformplayer.fragment.mainactivity.main + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.OptIn +import androidx.core.view.get +import androidx.media3.common.util.UnstableApi +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 +import com.futo.platformplayer.R + +@UnstableApi +class ShortsFragment : MainFragment() { + override val isMainView: Boolean = true + override val isTab: Boolean = true + override val hasBottomBar: Boolean get() = true + + private var previousShownView: ShortView? = null + + private lateinit var viewPager: ViewPager2 + private lateinit var customViewAdapter: CustomViewAdapter + private val urls = listOf( + "https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra", "https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra" + ) + + override fun onCreateMainView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment_shorts, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewPager = view.findViewById(R.id.viewPager) + + customViewAdapter = CustomViewAdapter(urls, layoutInflater, this) + viewPager.adapter = customViewAdapter + + // TODO something is laggy sometimes when swiping between videos + viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + @OptIn(UnstableApi::class) + override fun onPageSelected(position: Int) { + previousShownView?.stop() + + val focusedView = + ((viewPager[0] as RecyclerView).findViewHolderForAdapterPosition(position) as CustomViewHolder).shortView + focusedView.play() + + + previousShownView = focusedView + } + + }) + + } + + override fun onPause() { + super.onPause() + previousShownView?.stop() + } + + companion object { + private const val TAG = "ShortsFragment" + + fun newInstance() = ShortsFragment() + } + + class CustomViewAdapter( + private val urls: List, private val inflater: LayoutInflater, private val fragment: MainFragment + ) : RecyclerView.Adapter() { + @OptIn(UnstableApi::class) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder { + val shortView = ShortView(inflater, fragment) + return CustomViewHolder(shortView) + } + + @OptIn(UnstableApi::class) + override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { + holder.shortView.setVideo(urls[position]) + } + + @OptIn(UnstableApi::class) + override fun onViewRecycled(holder: CustomViewHolder) { + super.onViewRecycled(holder) + holder.shortView.detach() + } + + override fun getItemCount(): Int = urls.size + } + + @OptIn(UnstableApi::class) + class CustomViewHolder(val shortView: ShortView) : RecyclerView.ViewHolder(shortView) +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt index b8368ea5..b12889e8 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt @@ -38,6 +38,7 @@ class StatePlayer { //Players private var _exoplayer : PlayerManager? = null; private var _thumbnailExoPlayer : PlayerManager? = null; + private var _shortExoPlayer: PlayerManager? = null //Video Status var rotationLock: Boolean = false @@ -633,6 +634,13 @@ class StatePlayer { } return _thumbnailExoPlayer!!; } + fun getShortPlayerOrCreate(context: Context) : PlayerManager { + if(_shortExoPlayer == null) { + val player = createExoPlayer(context); + _shortExoPlayer = PlayerManager(player); + } + return _shortExoPlayer!!; + } @OptIn(UnstableApi::class) private fun createExoPlayer(context : Context): ExoPlayer { @@ -656,10 +664,13 @@ class StatePlayer { fun dispose(){ val player = _exoplayer; val thumbPlayer = _thumbnailExoPlayer; + val shortPlayer = _shortExoPlayer _exoplayer = null; _thumbnailExoPlayer = null; + _shortExoPlayer = null player?.release(); thumbPlayer?.release(); + shortPlayer?.release() } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt new file mode 100644 index 00000000..9c2b003f --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt @@ -0,0 +1,179 @@ +package com.futo.platformplayer.views.video + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.animation.LinearInterpolator +import androidx.annotation.OptIn +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.DefaultTimeBar +import androidx.media3.ui.PlayerView +import androidx.media3.ui.TimeBar +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.futo.platformplayer.R +import com.futo.platformplayer.Settings +import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails +import com.futo.platformplayer.helpers.VideoHelper +import com.futo.platformplayer.states.StatePlayer + +@UnstableApi +class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : + FutoVideoPlayerBase(PLAYER_STATE_NAME, context, attrs) { + + companion object { + private const val TAG = "FutoShortVideoPlayer" + private const val PLAYER_STATE_NAME: String = "ShortPlayer" + } + + private var playerAttached = false + + private val videoView: PlayerView + private val progressBar: DefaultTimeBar + + private val loadArtwork = object : CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + setArtwork(BitmapDrawable(resources, resource)) + } + + override fun onLoadCleared(placeholder: Drawable?) { + setArtwork(null) + } + } + + private val player = StatePlayer.instance.getShortPlayerOrCreate(context) + + private var progressAnimator: ValueAnimator = createProgressBarAnimator() + + private var playerEventListener = object : Player.Listener { + override fun onEvents(player: Player, events: Player.Events) { + if (events.containsAny( + Player.EVENT_POSITION_DISCONTINUITY, Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED + ) + ) { + if (player.duration >= 0) { + progressAnimator.duration = player.duration + setProgressBarDuration(player.duration) + progressAnimator.currentPlayTime = player.currentPosition + } + + if (player.isPlaying) { + if (!progressAnimator.isStarted) { + progressAnimator.start() + } + } else { + if (progressAnimator.isRunning) { + progressAnimator.cancel() + } + } + } + } + } + + init { + LayoutInflater.from(context).inflate(R.layout.view_short_player, this, true) + videoView = findViewById(R.id.video_player) + progressBar = findViewById(R.id.video_player_progress_bar) + + progressBar.addListener(object : TimeBar.OnScrubListener { + override fun onScrubStart(timeBar: TimeBar, position: Long) { + if (progressAnimator.isRunning) { + progressAnimator.cancel() + } + } + + override fun onScrubMove(timeBar: TimeBar, position: Long) {} + + override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { + if (canceled) { + progressAnimator.currentPlayTime = player.player.currentPosition + progressAnimator.start() + return + } + + // the progress bar should never be available to the user without the player being attached to this view + assert(playerAttached) + seekTo(position) + } + }) + } + + @OptIn(UnstableApi::class) + private fun createProgressBarAnimator(): ValueAnimator { + return ValueAnimator.ofFloat(0f, 1f).apply { + interpolator = LinearInterpolator() + + addUpdateListener { animation -> + val progress = animation.animatedValue as Float + val duration = animation.duration + progressBar.setPosition((progress * duration).toLong()) + } + } + } + + fun setProgressBarDuration(duration: Long) { + progressBar.setDuration(duration) + } + + /** + * Attaches this short player instance to the exo player instance for shorts + */ + fun attach() { + // connect the exo player for shorts to the view for this instance + player.attach(videoView, PLAYER_STATE_NAME) + + // direct the base player what exo player instance to use + changePlayer(player) + + playerAttached = true + + player.player.addListener(playerEventListener) + } + + fun detach() { + playerAttached = false + player.player.removeListener(playerEventListener) + player.detach() + } + + fun setPreview(video: IPlatformVideoDetails) { + if (video.live != null) { + setSource(video.live, null, play = true, keepSubtitles = false) + } else { + val videoSource = + VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS) + val audioSource = + VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context)) + if (videoSource == null && audioSource != null) { + val thumbnail = video.thumbnails.getHQThumbnail() + if (!thumbnail.isNullOrBlank()) { + Glide.with(videoView).asBitmap().load(thumbnail).into(loadArtwork) + } else { + Glide.with(videoView).clear(loadArtwork) + setArtwork(null) + } + } else { + Glide.with(videoView).clear(loadArtwork) + } + + setSource(videoSource, audioSource, play = true, keepSubtitles = false) + } + } + + @OptIn(UnstableApi::class) + fun setArtwork(drawable: Drawable?) { + if (drawable != null) { + videoView.defaultArtwork = drawable + videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL + } else { + videoView.defaultArtwork = null + videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF + } + } +} diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index c872ca02..c3eddb12 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -5,6 +5,7 @@ import android.net.Uri import android.util.AttributeSet import android.widget.RelativeLayout import androidx.annotation.OptIn +import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.coroutineScope import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.media3.common.C @@ -68,7 +69,7 @@ import java.io.ByteArrayInputStream import java.io.File import kotlin.math.abs -abstract class FutoVideoPlayerBase : RelativeLayout { +abstract class FutoVideoPlayerBase : ConstraintLayout { private val TAG = "FutoVideoPlayerBase" private val TEMP_DIRECTORY = StateApp.instance.getTempDirectory(); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1708eeb4..df3abc69 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,9 +1,10 @@ - - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_overview_bottom_bar.xml b/app/src/main/res/layout/fragment_overview_bottom_bar.xml index 11c33b2c..30b6b752 100644 --- a/app/src/main/res/layout/fragment_overview_bottom_bar.xml +++ b/app/src/main/res/layout/fragment_overview_bottom_bar.xml @@ -38,7 +38,7 @@ + diff --git a/app/src/main/res/layout/modal_comments.xml b/app/src/main/res/layout/modal_comments.xml new file mode 100644 index 00000000..e079a671 --- /dev/null +++ b/app/src/main/res/layout/modal_comments.xml @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_short.xml b/app/src/main/res/layout/view_short.xml new file mode 100644 index 00000000..8ec7a845 --- /dev/null +++ b/app/src/main/res/layout/view_short.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_short_player.xml b/app/src/main/res/layout/view_short_player.xml new file mode 100644 index 00000000..bfd8762d --- /dev/null +++ b/app/src/main/res/layout/view_short_player.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6d142d08..603193fb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -38,4 +38,147 @@ #B3000000 #ACACAC #C25353 + + + #8F4C38 + #FFFFFF + #FFDBD1 + #723523 + #77574E + #FFFFFF + #FFDBD1 + #5D4037 + #6C5D2F + #FFFFFF + #F5E1A7 + #534619 + #BA1A1A + #FFFFFF + #FFDAD6 + #93000A + #FFF8F6 + #231917 + #FFF8F6 + #231917 + #F5DED8 + #53433F + #85736E + #D8C2BC + #000000 + #392E2B + #FFEDE8 + #FFB5A0 + #FFDBD1 + #3A0B01 + #FFB5A0 + #723523 + #FFDBD1 + #2C150F + #E7BDB2 + #5D4037 + #F5E1A7 + #231B00 + #D8C58D + #534619 + #E8D6D2 + #FFF8F6 + #FFFFFF + #FFF1ED + #FCEAE5 + #F7E4E0 + #F1DFDA + #5D2514 + #FFFFFF + #A15A45 + #FFFFFF + #4B2F28 + #FFFFFF + #87655C + #FFFFFF + #41350A + #FFFFFF + #7B6C3C + #FFFFFF + #740006 + #FFFFFF + #CF2C27 + #FFFFFF + #FFF8F6 + #231917 + #FFF8F6 + #180F0D + #F5DED8 + #41332F + #5F4F4A + #7B6964 + #000000 + #392E2B + #FFEDE8 + #FFB5A0 + #A15A45 + #FFFFFF + #84422F + #FFFFFF + #87655C + #FFFFFF + #6D4D45 + #FFFFFF + #7B6C3C + #FFFFFF + #615426 + #FFFFFF + #D4C3BE + #FFF8F6 + #FFFFFF + #FFF1ED + #F7E4E0 + #EBD9D4 + #DFCEC9 + #501B0B + #FFFFFF + #753725 + #FFFFFF + #3F261E + #FFFFFF + #60423A + #FFFFFF + #362B02 + #FFFFFF + #55481C + #FFFFFF + #600004 + #FFFFFF + #98000A + #FFFFFF + #FFF8F6 + #231917 + #FFF8F6 + #000000 + #F5DED8 + #000000 + #372925 + #554641 + #000000 + #392E2B + #FFFFFF + #FFB5A0 + #753725 + #FFFFFF + #592111 + #FFFFFF + #60423A + #FFFFFF + #472C24 + #FFFFFF + #55481C + #FFFFFF + #3D3206 + #FFFFFF + #C6B5B1 + #FFF8F6 + #FFFFFF + #FFEDE8 + #F1DFDA + #E2D1CC + #D4C3BE \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4df1905..2285becf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ Failed to retrieve data, are you connected? Settings History + Shorts Sources Buy FAQ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 8fa8f2d1..3f2b80c5 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -49,6 +49,42 @@ @style/Theme.FutoVideo.ListView @style/Theme.FutoVideo.TextView @style/Theme.FutoVideo.CheckedTextView + + + @color/md_theme_inversePrimary + @color/md_theme_primaryContainer + @color/md_theme_onPrimaryContainer + @color/md_theme_secondaryContainer + @color/md_theme_onSecondaryContainer + @color/md_theme_tertiary + @color/md_theme_onTertiary + @color/md_theme_tertiaryContainer + @color/md_theme_onTertiaryContainer + @color/md_theme_surfaceVariant + @color/md_theme_onSurfaceVariant + @color/md_theme_inverseSurface + @color/md_theme_inverseOnSurface + @color/md_theme_outline + @color/md_theme_errorContainer + @color/md_theme_onErrorContainer + @style/TextAppearance.Material3.DisplayLarge + @style/TextAppearance.Material3.DisplayMedium + @style/TextAppearance.Material3.DisplaySmall + @style/TextAppearance.Material3.HeadlineLarge + @style/TextAppearance.Material3.HeadlineMedium + @style/TextAppearance.Material3.HeadlineSmall + @style/TextAppearance.Material3.TitleLarge + @style/TextAppearance.Material3.TitleMedium + @style/TextAppearance.Material3.TitleSmall + @style/TextAppearance.Material3.BodyLarge + @style/TextAppearance.Material3.BodyMedium + @style/TextAppearance.Material3.BodySmall + @style/TextAppearance.Material3.LabelLarge + @style/TextAppearance.Material3.LabelMedium + @style/TextAppearance.Material3.LabelSmall + @style/ShapeAppearance.Material3.SmallComponent + @style/ShapeAppearance.Material3.MediumComponent + @style/ShapeAppearance.Material3.LargeComponent + + + + + \ No newline at end of file From dd6bde97a968ae9816cfb91298fab75073143c10 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Wed, 2 Apr 2025 22:53:54 +0200 Subject: [PATCH 046/330] Playlists sort and search support, Playlist search support, wip local playback, other fixes --- ...> DownloadedVideoMuxedSourceDescriptor.kt} | 2 +- .../api/media/platforms/local/LocalClient.kt | 5 + .../local/models/LocalVideoDetails.kt | 85 ++++++++++++++ .../models/LocalVideoMuxedSourceDescriptor.kt | 13 +++ .../platforms/local/models/MediaStoreVideo.kt | 25 +++++ .../models/sources/LocalVideoFileSource.kt | 31 ++++++ .../platformplayer/downloads/VideoLocal.kt | 4 +- .../mainactivity/main/DownloadsFragment.kt | 2 +- .../mainactivity/main/PlaylistsFragment.kt | 79 ++++++++++++- .../mainactivity/main/VideoListEditorView.kt | 43 ++++++- .../platformplayer/helpers/VideoHelper.kt | 33 ++++++ .../platformplayer/models/ImageVariable.kt | 7 ++ .../SubscriptionsTaskFetchAlgorithm.kt | 34 ++++-- .../views/overlays/ImageVariableOverlay.kt | 6 +- app/src/main/res/drawable/ic_search_off.xml | 10 ++ .../main/res/layout/fragment_playlists.xml | 105 +++++++++++++----- .../res/layout/fragment_video_list_editor.xml | 48 ++++++-- app/src/main/res/values/strings.xml | 10 ++ 18 files changed, 481 insertions(+), 61 deletions(-) rename app/src/main/java/com/futo/platformplayer/api/media/models/streams/{LocalVideoMuxedSourceDescriptor.kt => DownloadedVideoMuxedSourceDescriptor.kt} (89%) create mode 100644 app/src/main/java/com/futo/platformplayer/api/media/platforms/local/LocalClient.kt create mode 100644 app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/LocalVideoDetails.kt create mode 100644 app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/LocalVideoMuxedSourceDescriptor.kt create mode 100644 app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/MediaStoreVideo.kt create mode 100644 app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoFileSource.kt create mode 100644 app/src/main/res/drawable/ic_search_off.xml diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/LocalVideoMuxedSourceDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/DownloadedVideoMuxedSourceDescriptor.kt similarity index 89% rename from app/src/main/java/com/futo/platformplayer/api/media/models/streams/LocalVideoMuxedSourceDescriptor.kt rename to app/src/main/java/com/futo/platformplayer/api/media/models/streams/DownloadedVideoMuxedSourceDescriptor.kt index b5309931..df96de13 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/LocalVideoMuxedSourceDescriptor.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/DownloadedVideoMuxedSourceDescriptor.kt @@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.downloads.VideoLocal -class LocalVideoMuxedSourceDescriptor( +class DownloadedVideoMuxedSourceDescriptor( private val video: VideoLocal ) : VideoMuxedSourceDescriptor() { override val videoSources: Array get() = video.videoSource.toTypedArray(); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/LocalClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/LocalClient.kt new file mode 100644 index 00000000..3f6e5b82 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/LocalClient.kt @@ -0,0 +1,5 @@ +package com.futo.platformplayer.api.media.platforms.local + +class LocalClient { + //TODO +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/LocalVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/LocalVideoDetails.kt new file mode 100644 index 00000000..1c169e64 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/LocalVideoDetails.kt @@ -0,0 +1,85 @@ +package com.futo.platformplayer.api.media.platforms.local.models + +import com.futo.platformplayer.api.media.IPlatformClient +import com.futo.platformplayer.api.media.PlatformID +import com.futo.platformplayer.api.media.models.PlatformAuthorLink +import com.futo.platformplayer.api.media.models.Thumbnails +import com.futo.platformplayer.api.media.models.comments.IPlatformComment +import com.futo.platformplayer.api.media.models.contents.ContentType +import com.futo.platformplayer.api.media.models.contents.IPlatformContent +import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker +import com.futo.platformplayer.api.media.models.ratings.IRating +import com.futo.platformplayer.api.media.models.ratings.RatingLikes +import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor +import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor +import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource +import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource +import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource +import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource +import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails +import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource +import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.downloads.VideoLocal +import java.io.File +import java.time.Instant +import java.time.OffsetDateTime +import java.time.ZoneId + +class LocalVideoDetails: IPlatformVideoDetails { + + override val contentType: ContentType get() = ContentType.UNKNOWN; + + override val id: PlatformID; + override val name: String; + override val author: PlatformAuthorLink; + + override val datetime: OffsetDateTime?; + + override val url: String; + override val shareUrl: String; + override val rating: IRating = RatingLikes(0); + override val description: String = ""; + + override val video: IVideoSourceDescriptor; + override val preview: IVideoSourceDescriptor? = null; + override val live: IVideoSource? = null; + override val dash: IDashManifestSource? = null; + override val hls: IHLSManifestSource? = null; + override val subtitles: List = listOf() + + override val thumbnails: Thumbnails; + override val duration: Long; + override val viewCount: Long = 0; + override val isLive: Boolean = false; + override val isShort: Boolean = false; + + constructor(file: File) { + id = PlatformID("Local", file.path, "LOCAL") + name = file.name; + author = PlatformAuthorLink.UNKNOWN; + + url = file.canonicalPath; + shareUrl = ""; + + duration = 0; + thumbnails = Thumbnails(arrayOf()); + + datetime = OffsetDateTime.ofInstant( + Instant.ofEpochMilli(file.lastModified()), + ZoneId.systemDefault() + ); + video = LocalVideoMuxedSourceDescriptor(LocalVideoFileSource(file)); + } + + override fun getComments(client: IPlatformClient): IPager? { + return null; + } + + override fun getPlaybackTracker(): IPlaybackTracker? { + return null; + } + + override fun getContentRecommendations(client: IPlatformClient): IPager? { + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/LocalVideoMuxedSourceDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/LocalVideoMuxedSourceDescriptor.kt new file mode 100644 index 00000000..da8ae431 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/LocalVideoMuxedSourceDescriptor.kt @@ -0,0 +1,13 @@ +package com.futo.platformplayer.api.media.platforms.local.models + +import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor +import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource +import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource +import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource +import com.futo.platformplayer.downloads.VideoLocal + +class LocalVideoMuxedSourceDescriptor( + private val video: LocalVideoFileSource +) : VideoMuxedSourceDescriptor() { + override val videoSources: Array get() = arrayOf(video); +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/MediaStoreVideo.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/MediaStoreVideo.kt new file mode 100644 index 00000000..52876b90 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/MediaStoreVideo.kt @@ -0,0 +1,25 @@ +package com.futo.platformplayer.api.media.platforms.local.models + +import android.content.Context +import android.database.Cursor +import android.provider.MediaStore +import android.provider.MediaStore.Video + +class MediaStoreVideo { + + + companion object { + val URI = MediaStore.Files.getContentUri("external"); + val PROJECTION = arrayOf(Video.Media._ID, Video.Media.TITLE, Video.Media.DURATION, Video.Media.HEIGHT, Video.Media.WIDTH, Video.Media.MIME_TYPE); + val ORDER = MediaStore.Video.Media.TITLE; + + fun readMediaStoreVideo(cursor: Cursor) { + + } + + fun query(context: Context, selection: String, args: Array, order: String? = null): Cursor? { + val cursor = context.contentResolver.query(URI, PROJECTION, selection, args, order ?: ORDER, null); + return cursor; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoFileSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoFileSource.kt new file mode 100644 index 00000000..9e2f7792 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoFileSource.kt @@ -0,0 +1,31 @@ +package com.futo.platformplayer.api.media.platforms.local.models.sources + +import android.content.Context +import android.provider.MediaStore +import android.provider.MediaStore.Video +import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource +import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource +import com.futo.platformplayer.helpers.VideoHelper +import java.io.File + +class LocalVideoFileSource: IVideoSource { + + + override val name: String; + override val width: Int; + override val height: Int; + override val container: String; + override val codec: String = "" + override val bitrate: Int = 0 + override val duration: Long; + override val priority: Boolean = false; + + constructor(file: File) { + name = file.name; + width = 0; + height = 0; + container = VideoHelper.videoExtensionToMimetype(file.extension) ?: ""; + duration = 0; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt index 578a5812..06095058 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt @@ -10,7 +10,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.ratings.IRating import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor -import com.futo.platformplayer.api.media.models.streams.LocalVideoMuxedSourceDescriptor +import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource @@ -57,7 +57,7 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem { override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty()) LocalVideoUnMuxedSourceDescriptor(this) else - LocalVideoMuxedSourceDescriptor(this); + DownloadedVideoMuxedSourceDescriptor(this); override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview; override val live: IVideoSource? get() = videoSerialized.live; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt index 440aa235..d402a6e2 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt @@ -229,7 +229,7 @@ class DownloadsFragment : MainFragment() { fun filterDownloads(vids: List): List{ var vidsToReturn = vids; if(!_listDownloadSearch.text.isNullOrEmpty()) - vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) }; + vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) || it.author.name.contains(_listDownloadSearch.text, true) }; if(!ordering.isNullOrEmpty()) { vidsToReturn = when(ordering){ "downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX }; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistsFragment.kt index bcc01ed1..9d188415 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistsFragment.kt @@ -6,12 +6,17 @@ import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.EditText import android.widget.FrameLayout import android.widget.ImageButton import android.widget.LinearLayout +import android.widget.Spinner import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.widget.addTextChangedListener import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -21,11 +26,13 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists +import com.futo.platformplayer.views.SearchView import com.futo.platformplayer.views.adapters.* import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.google.android.material.appbar.AppBarLayout import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.time.OffsetDateTime class PlaylistsFragment : MainFragment() { @@ -65,6 +72,7 @@ class PlaylistsFragment : MainFragment() { private val _fragment: PlaylistsFragment; var watchLater: ArrayList = arrayListOf(); + var allPlaylists: ArrayList = arrayListOf(); var playlists: ArrayList = arrayListOf(); private var _appBar: AppBarLayout; private var _adapterWatchLater: VideoListHorizontalAdapter; @@ -72,12 +80,20 @@ class PlaylistsFragment : MainFragment() { private var _layoutWatchlist: ConstraintLayout; private var _slideUpOverlay: SlideUpMenuOverlay? = null; + private var _listPlaylistsSearch: EditText; + + private var _ordering: String? = null; + + constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) { _fragment = fragment; inflater.inflate(R.layout.fragment_playlists, this); + _listPlaylistsSearch = findViewById(R.id.playlists_search); + watchLater = ArrayList(); playlists = ArrayList(); + allPlaylists = ArrayList(); val recyclerWatchLater = findViewById(R.id.recycler_watch_later); @@ -105,6 +121,7 @@ class PlaylistsFragment : MainFragment() { buttonCreatePlaylist.setOnClickListener { _slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById(R.id.overlay_create_playlist)) { val playlist = Playlist(it, arrayListOf()); + allPlaylists.add(0, playlist); playlists.add(0, playlist); StatePlaylists.instance.createOrUpdatePlaylist(playlist); @@ -120,6 +137,34 @@ class PlaylistsFragment : MainFragment() { _appBar = findViewById(R.id.app_bar); _layoutWatchlist = findViewById(R.id.layout_watchlist); + + _listPlaylistsSearch.addTextChangedListener { + updatePlaylistsFiltering(); + } + val spinnerSortBy: Spinner = findViewById(R.id.spinner_sortby); + spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.playlists_sortby_array)).also { + it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple); + }; + spinnerSortBy.setSelection(0); + spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { + when(pos) { + 0 -> _ordering = "nameAsc" + 1 -> _ordering = "nameDesc" + 2 -> _ordering = "dateEditAsc" + 3 -> _ordering = "dateEditDesc" + 4 -> _ordering = "dateCreateAsc" + 5 -> _ordering = "dateCreateDesc" + 6 -> _ordering = "datePlayAsc" + 7 -> _ordering = "datePlayDesc" + else -> _ordering = null + } + updatePlaylistsFiltering() + } + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + }; + + findViewById(R.id.text_view_all).setOnClickListener { _fragment.navigate(context.getString(R.string.watch_later)); }; StatePlaylists.instance.onWatchLaterChanged.subscribe(this) { fragment.lifecycleScope.launch(Dispatchers.Main) { @@ -134,10 +179,12 @@ class PlaylistsFragment : MainFragment() { @SuppressLint("NotifyDataSetChanged") fun onShown() { + allPlaylists.clear(); playlists.clear() - playlists.addAll( + allPlaylists.addAll( StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) } ); + playlists.addAll(filterPlaylists(allPlaylists)); _adapterPlaylist.notifyDataSetChanged(); updateWatchLater(); @@ -157,6 +204,32 @@ class PlaylistsFragment : MainFragment() { return false; } + private fun updatePlaylistsFiltering() { + val toFilter = allPlaylists ?: return; + playlists.clear(); + playlists.addAll(filterPlaylists(toFilter)); + _adapterPlaylist.notifyDataSetChanged(); + } + private fun filterPlaylists(pls: List): List { + var playlistsToReturn = pls; + if(!_listPlaylistsSearch.text.isNullOrEmpty()) + playlistsToReturn = playlistsToReturn.filter { it.name.contains(_listPlaylistsSearch.text, true) }; + if(!_ordering.isNullOrEmpty()){ + playlistsToReturn = when(_ordering){ + "nameAsc" -> playlistsToReturn.sortedBy { it.name.lowercase() } + "nameDesc" -> playlistsToReturn.sortedByDescending { it.name.lowercase() }; + "dateEditAsc" -> playlistsToReturn.sortedBy { it.dateUpdate ?: OffsetDateTime.MAX }; + "dateEditDesc" -> playlistsToReturn.sortedByDescending { it.dateUpdate ?: OffsetDateTime.MIN } + "dateCreateAsc" -> playlistsToReturn.sortedBy { it.dateCreation ?: OffsetDateTime.MAX }; + "dateCreateDesc" -> playlistsToReturn.sortedByDescending { it.dateCreation ?: OffsetDateTime.MIN } + "datePlayAsc" -> playlistsToReturn.sortedBy { it.datePlayed ?: OffsetDateTime.MAX }; + "datePlayDesc" -> playlistsToReturn.sortedByDescending { it.datePlayed ?: OffsetDateTime.MIN } + else -> playlistsToReturn + } + } + return playlistsToReturn; + } + private fun updateWatchLater() { val watchList = StatePlaylists.instance.getWatchLater(); if (watchList.isNotEmpty()) { @@ -164,7 +237,7 @@ class PlaylistsFragment : MainFragment() { _appBar.let { appBar -> val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams; - layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 230.0f, resources.displayMetrics).toInt(); + layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 315.0f, resources.displayMetrics).toInt(); appBar.layoutParams = layoutParams; } } else { @@ -172,7 +245,7 @@ class PlaylistsFragment : MainFragment() { _appBar.let { appBar -> val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams; - layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25.0f, resources.displayMetrics).toInt(); + layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 110.0f, resources.displayMetrics).toInt(); appBar.layoutParams = layoutParams; }; } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt index b458a093..441f7421 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt @@ -9,6 +9,7 @@ import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.core.view.isVisible import androidx.core.view.setPadding import com.bumptech.glide.Glide import com.futo.platformplayer.R @@ -22,6 +23,7 @@ import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.toHumanDuration import com.futo.platformplayer.toHumanTime +import com.futo.platformplayer.views.SearchView import com.futo.platformplayer.views.lists.VideoListEditorView abstract class VideoListEditorView : LinearLayout { @@ -37,9 +39,15 @@ abstract class VideoListEditorView : LinearLayout { protected var _buttonExport: ImageButton; private var _buttonShare: ImageButton; private var _buttonEdit: ImageButton; + private var _buttonSearch: ImageButton; + + private var _search: SearchView; private var _onShare: (()->Unit)? = null; + private var _loadedVideos: List? = null; + private var _loadedVideosCanEdit: Boolean = false; + constructor(inflater: LayoutInflater) : super(inflater.context) { inflater.inflate(R.layout.fragment_video_list_editor, this); @@ -57,6 +65,26 @@ abstract class VideoListEditorView : LinearLayout { _buttonDownload.visibility = View.GONE; _buttonExport = findViewById(R.id.button_export); _buttonExport.visibility = View.GONE; + _buttonSearch = findViewById(R.id.button_search); + + _search = findViewById(R.id.search_bar); + _search.visibility = View.GONE; + _search.onSearchChanged.subscribe { + updateVideoFilters(); + } + + _buttonSearch.setOnClickListener { + if(_search.isVisible) { + _search.visibility = View.GONE; + _search.textSearch.text = ""; + updateVideoFilters(); + _buttonSearch.setImageResource(R.drawable.ic_search); + } + else { + _search.visibility = View.VISIBLE; + _buttonSearch.setImageResource(R.drawable.ic_search_off); + } + } _buttonShare = findViewById(R.id.button_share); val onShare = _onShare; @@ -171,9 +199,22 @@ abstract class VideoListEditorView : LinearLayout { .load(R.drawable.placeholder_video_thumbnail) .into(_imagePlaylistThumbnail) } - + _loadedVideos = videos; + _loadedVideosCanEdit = canEdit; _videoListEditorView.setVideos(videos, canEdit); } + fun filterVideos(videos: List): List { + var toReturn = videos; + val searchStr = _search.textSearch.text + if(!searchStr.isNullOrBlank()) + toReturn = toReturn.filter { it.name.contains(searchStr, true) || it.author.name.contains(searchStr, true) }; + return toReturn; + } + + fun updateVideoFilters() { + val videos = _loadedVideos ?: return; + _videoListEditorView.setVideos(filterVideos(videos), _loadedVideosCanEdit); + } protected fun setButtonDownloadVisible(isVisible: Boolean) { _buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE; diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt index 522647de..cad49efd 100644 --- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt +++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt @@ -214,5 +214,38 @@ class VideoHelper { } else return 0; } + + fun mediaExtensionToMimetype(extension: String): String? { + return videoExtensionToMimetype(extension) ?: audioExtensionToMimetype(extension); + } + fun videoExtensionToMimetype(extension: String): String? { + val extensionTrimmed = extension.trim('.').lowercase(); + return when (extensionTrimmed) { + "mp4" -> return "video/mp4"; + "webm" -> return "video/webm"; + "m3u8" -> return "video/x-mpegURL"; + "3gp" -> return "video/3gpp"; + "mov" -> return "video/quicktime"; + "mkv" -> return "video/x-matroska"; + "mp4a" -> return "audio/vnd.apple.mpegurl"; + "mpga" -> return "audio/mpga"; + "mp3" -> return "audio/mp3"; + "webm" -> return "audio/webm"; + "3gp" -> return "audio/3gpp"; + else -> null; + } + } + fun audioExtensionToMimetype(extension: String): String? { + val extensionTrimmed = extension.trim('.').lowercase(); + return when (extensionTrimmed) { + "mkv" -> return "audio/x-matroska"; + "mp4a" -> return "audio/vnd.apple.mpegurl"; + "mpga" -> return "audio/mpga"; + "mp3" -> return "audio/mp3"; + "webm" -> return "audio/webm"; + "3gp" -> return "audio/3gpp"; + else -> null; + } + } } } diff --git a/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt b/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt index 00594df7..97fe6408 100644 --- a/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt +++ b/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt @@ -8,6 +8,7 @@ import com.bumptech.glide.Glide import com.futo.platformplayer.PresetImages import com.futo.platformplayer.R import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateSubscriptions import kotlinx.serialization.Contextual import kotlinx.serialization.Transient import java.io.File @@ -35,6 +36,12 @@ data class ImageVariable( } else if(!url.isNullOrEmpty()) { Glide.with(imageView) .load(url) + .error(if(!subscriptionUrl.isNullOrBlank()) StateSubscriptions.instance.getSubscription(subscriptionUrl!!)?.channel?.thumbnail else null) + .placeholder(R.drawable.placeholder_channel_thumbnail) + .into(imageView); + } else if(!subscriptionUrl.isNullOrEmpty()) { + Glide.with(imageView) + .load(StateSubscriptions.instance.getSubscription(subscriptionUrl!!)?.channel?.thumbnail) .placeholder(R.drawable.placeholder_channel_thumbnail) .into(imageView); } else if(!presetName.isNullOrEmpty()) { diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt index 15235017..123b0320 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -30,6 +30,7 @@ import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.subsexchange.ChannelRequest import com.futo.platformplayer.subsexchange.ChannelResolve +import com.futo.platformplayer.subsexchange.ExchangeContract import kotlinx.coroutines.CoroutineScope import java.time.OffsetDateTime import java.util.concurrent.ExecutionException @@ -77,22 +78,31 @@ abstract class SubscriptionsTaskFetchAlgorithm( val exs: ArrayList = arrayListOf(); - - - val contractableTasks = tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) }; - val contract = if(contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map { ChannelRequest(it.url) }.toTypedArray()) else null; - if(contract?.provided?.isNotEmpty() == true) - Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}"); + var contract: ExchangeContract? = null; var providedTasks: MutableList? = null; - if(contract != null && contract.required.isNotEmpty()){ - providedTasks = mutableListOf() - for(task in tasks.toList()){ - if(!task.fromCache && !task.fromPeek && contract.provided.contains(task.url)) { - providedTasks.add(task); - tasks.remove(task); + + try { + val contractableTasks = + tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) }; + contract = + if (contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map { + ChannelRequest(it.url) + }.toTypedArray()) else null; + if (contract?.provided?.isNotEmpty() == true) + Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}"); + if (contract != null && contract.required.isNotEmpty()) { + providedTasks = mutableListOf() + for (task in tasks.toList()) { + if (!task.fromCache && !task.fromPeek && contract.provided.contains(task.url)) { + providedTasks.add(task); + tasks.remove(task); + } } } } + catch(ex: Throwable){ + Logger.e("SubscriptionsTaskFetchAlgorithm", "Failed to retrieve SubsExchange contract due to: " + ex.message, ex); + } val failedPlugins = mutableListOf(); val cachedChannels = mutableListOf() diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt index 5d5a6a50..b8fb8a77 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt @@ -98,7 +98,11 @@ class ImageVariableOverlay: ConstraintLayout { UIDialogs.toast(context, "No thumbnail found"); return@subscribe; } - _selected = ImageVariable(it.channel.thumbnail); + val channelUrl = it.channel.url; + _selected = ImageVariable(it.channel.thumbnail).let { + it.subscriptionUrl = channelUrl; + return@let it; + } updateSelected(); }; }; diff --git a/app/src/main/res/drawable/ic_search_off.xml b/app/src/main/res/drawable/ic_search_off.xml new file mode 100644 index 00000000..08810c6c --- /dev/null +++ b/app/src/main/res/drawable/ic_search_off.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_playlists.xml b/app/src/main/res/layout/fragment_playlists.xml index cd2050b6..b8ca23df 100644 --- a/app/src/main/res/layout/fragment_playlists.xml +++ b/app/src/main/res/layout/fragment_playlists.xml @@ -13,7 +13,7 @@ @@ -87,7 +87,7 @@ - - + + android:gravity="center_vertical"> - + + + + + + + + + + + + + + + + + + - @@ -136,7 +183,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:layout_constraintTop_toBottomOf="@id/text_view_all" + app:layout_constraintTop_toBottomOf="@id/playlists_filter_container" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" android:paddingTop="10dp" diff --git a/app/src/main/res/layout/fragment_video_list_editor.xml b/app/src/main/res/layout/fragment_video_list_editor.xml index a906421b..f86b69e4 100644 --- a/app/src/main/res/layout/fragment_video_list_editor.xml +++ b/app/src/main/res/layout/fragment_video_list_editor.xml @@ -30,7 +30,7 @@ android:orientation="vertical"> + android:layout_height="wrap_content"> + @@ -116,6 +132,8 @@ app:layout_constraintLeft_toLeftOf="@id/container_buttons" app:layout_constraintBottom_toTopOf="@id/container_buttons" /> + + + app:srcCompat="@drawable/ic_search" + app:tint="@color/white" + android:padding="5dp" + android:scaleType="fitCenter" /> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 256fe1ef..9d82ff85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -973,6 +973,16 @@ Release Date (Oldest) Release Date (Newest) + + Name (Ascending) + Name (Descending) + Modified Date (Oldest) + Modified Date (Newest) + Creation Date (Oldest) + Creation Date (Newest) + Play Date (Oldest) + Play Date (Newest) + Preview List From 0a59e04f199946a67edcef6a945999cd495957ea Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Wed, 2 Apr 2025 23:40:37 +0200 Subject: [PATCH 047/330] Fix ui offset issue when opening video through search url --- .../mainactivity/main/ContentSearchResultsFragment.kt | 11 +++++++++-- .../fragment/mainactivity/main/SuggestionsFragment.kt | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt index a078eb0c..b8b0b567 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UISlideOverlays +import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.structures.IPager @@ -160,8 +161,14 @@ class ContentSearchResultsFragment : MainFragment() { navigate(it); else if(StatePlatform.instance.hasEnabledChannelClient(it)) navigate(it); - else - navigate(it); + else { + val url = it; + activity?.let { + close() + if(it is MainActivity) + it.navigate(it.getFragment(), url); + } + } } else setQuery(it, true); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt index 95055d0a..a07de94e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.* +import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment import com.futo.platformplayer.logging.Logger @@ -122,8 +123,14 @@ class SuggestionsFragment : MainFragment { navigate(it); else if(StatePlatform.instance.hasEnabledChannelClient(it)) navigate(it); - else - navigate(it); + else { + val url = it; + activity?.let { + close() + if(it is MainActivity) + it.navigate(it.getFragment(), url); + } + } } else navigate(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl)); From 7d64003d1c97669dceb885847fd11ea27c78d56b Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Fri, 4 Apr 2025 00:37:26 +0200 Subject: [PATCH 048/330] Feed filter loading improved, home filters support, various peripheral stuff --- .../java/com/futo/platformplayer/Settings.kt | 5 +- .../futo/platformplayer/UISlideOverlays.kt | 27 ++-- .../media/models/contents/IPlatformContent.kt | 1 + .../models/video/SerializedPlatformVideo.kt | 2 + .../api/media/structures/IRefreshPager.kt | 2 +- .../api/media/structures/ReusablePager.kt | 122 +++++++++++++++++- .../fragment/mainactivity/main/FeedView.kt | 57 ++++++-- .../mainactivity/main/HomeFragment.kt | 114 +++++++++++++--- .../serializers/PlatformContentSerializer.kt | 4 +- .../states/StateSubscriptions.kt | 2 +- .../stores/StringArrayStorage.kt | 15 +++ .../SubscriptionsTaskFetchAlgorithm.kt | 1 + .../subsexchange/ChannelResult.kt | 9 +- .../subsexchange/SubsExchangeClient.kt | 1 + .../futo/platformplayer/views/ToggleBar.kt | 30 ++++- .../views/others/ToggleTagView.kt | 35 ++++- .../overlays/slideup/SlideUpMenuOverlay.kt | 7 + app/src/main/res/layout/view_toggle_bar.xml | 6 +- app/src/main/res/layout/view_toggle_tag.xml | 31 +++-- app/src/main/res/values/strings.xml | 2 + 20 files changed, 411 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 989997c3..f68a60f9 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -205,7 +205,7 @@ class Settings : FragmentedStorageFileJson() { var home = HomeSettings(); @Serializable class HomeSettings { - @FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5) + @FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3) @DropdownFieldOptionsId(R.array.feed_style) var homeFeedStyle: Int = 1; @@ -216,6 +216,9 @@ class Settings : FragmentedStorageFileJson() { return FeedStyle.THUMBNAIL; } + @FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4) + var showHomeFilters: Boolean = true; + @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) var previewFeedItems: Boolean = true; diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index a1aa71b7..874ffd4f 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -1148,7 +1148,7 @@ class UISlideOverlays { container.context.getString(R.string.decide_which_buttons_should_be_pinned), tag = "", call = { - showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) { + showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, { val selected = it .map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } } .filter { it != null } @@ -1156,7 +1156,7 @@ class UISlideOverlays { .toList(); onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) }); - } + }); }, invokeParent = false )) @@ -1164,29 +1164,40 @@ class UISlideOverlays { return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() }; } - - fun showOrderOverlay(container: ViewGroup, title: String, options: List>, onOrdered: (List)->Unit) { + fun showOrderOverlay(container: ViewGroup, title: String, options: List>, onOrdered: (List)->Unit, description: String? = null) { val selection: MutableList = mutableListOf(); var overlay: SlideUpMenuOverlay? = null; overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true, - options.map { SlideUpMenuItem( + listOf( + if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null, + ).filterNotNull() + + (options.map { SlideUpMenuItem( container.context, R.drawable.ic_move_up, it.first, "", tag = it.second, call = { + val overlayItem = overlay?.getSlideUpItemByTag(it.second); if(overlay!!.selectOption(null, it.second, true, true)) { - if(!selection.contains(it.second)) + if(!selection.contains(it.second)) { selection.add(it.second); - } else + if(overlayItem != null) { + overlayItem.setSubText(selection.indexOf(it.second).toString()); + } + } + } else { selection.remove(it.second); + if(overlayItem != null) { + overlayItem.setSubText(""); + } + } }, invokeParent = false ) - }); + })); overlay.onOK.subscribe { onOrdered.invoke(selection); overlay.hide(); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/contents/IPlatformContent.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/contents/IPlatformContent.kt index edb1caa3..a823316a 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/contents/IPlatformContent.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/contents/IPlatformContent.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.contents import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.models.PlatformAuthorLink import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames import java.time.OffsetDateTime interface IPlatformContent { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/video/SerializedPlatformVideo.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/video/SerializedPlatformVideo.kt index 68bb5cb9..c9e02d92 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/video/SerializedPlatformVideo.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/video/SerializedPlatformVideo.kt @@ -10,6 +10,7 @@ import com.futo.polycentric.core.combineHashCodes import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNames import java.time.OffsetDateTime @kotlinx.serialization.Serializable @@ -20,6 +21,7 @@ open class SerializedPlatformVideo( override val thumbnails: Thumbnails, override val author: PlatformAuthorLink, @kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class) + @JsonNames("datetime", "dateTime") override val datetime: OffsetDateTime? = null, override val url: String, override val shareUrl: String = "", diff --git a/app/src/main/java/com/futo/platformplayer/api/media/structures/IRefreshPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/structures/IRefreshPager.kt index 34a9e41a..375f9343 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/structures/IRefreshPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/structures/IRefreshPager.kt @@ -6,7 +6,7 @@ import com.futo.platformplayer.constructs.Event1 * A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager) * When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager */ -interface IRefreshPager { +interface IRefreshPager: IPager { val onPagerChanged: Event1>; val onPagerError: Event1; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/structures/ReusablePager.kt b/app/src/main/java/com/futo/platformplayer/api/media/structures/ReusablePager.kt index 45f6aea5..ee1b39f2 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/structures/ReusablePager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/structures/ReusablePager.kt @@ -1,5 +1,7 @@ package com.futo.platformplayer.api.media.structures +import com.futo.platformplayer.api.media.structures.ReusablePager.Window +import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.logging.Logger /** @@ -9,8 +11,8 @@ import com.futo.platformplayer.logging.Logger * A "Window" is effectively a pager that just reads previous results from the shared results, but when the end is reached, it will call nextPage on the parent if possible for new results. * This allows multiple Windows to exist of the same pager, without messing with position, or duplicate requests */ -class ReusablePager: INestedPager, IPager { - private val _pager: IPager; +open class ReusablePager: INestedPager, IReusablePager { + protected var _pager: IPager; val previousResults = arrayListOf(); constructor(subPager: IPager) { @@ -44,7 +46,7 @@ class ReusablePager: INestedPager, IPager { return previousResults; } - fun getWindow(): Window { + override fun getWindow(): Window { return Window(this); } @@ -95,4 +97,118 @@ class ReusablePager: INestedPager, IPager { return ReusablePager(this); } } +} + + +public class ReusableRefreshPager: INestedPager, IReusablePager { + protected var _pager: IRefreshPager; + val previousResults = arrayListOf(); + + private var _currentPage: IPager; + + + val onPagerChanged = Event1>() + val onPagerError = Event1() + + constructor(subPager: IRefreshPager) { + this._pager = subPager; + _currentPage = this; + synchronized(previousResults) { + previousResults.addAll(subPager.getResults()); + } + _pager.onPagerError.subscribe(onPagerError::emit); + _pager.onPagerChanged.subscribe { + _currentPage = it; + synchronized(previousResults) { + previousResults.clear(); + previousResults.addAll(it.getResults()); + } + + onPagerChanged.emit(_currentPage); + }; + } + + override fun findPager(query: (IPager) -> Boolean): IPager? { + if(query(_pager)) + return _pager; + else if(_pager is INestedPager<*>) + return (_pager as INestedPager).findPager(query); + return null; + } + + override fun hasMorePages(): Boolean { + return _pager.hasMorePages(); + } + + override fun nextPage() { + _pager.nextPage(); + } + + override fun getResults(): List { + val results = _pager.getResults(); + synchronized(previousResults) { + previousResults.addAll(results); + } + return previousResults; + } + + override fun getWindow(): RefreshWindow { + return RefreshWindow(this); + } + + + class RefreshWindow: IPager, INestedPager, IRefreshPager { + private val _parent: ReusableRefreshPager; + private var _position: Int = 0; + private var _read: Int = 0; + + private var _currentResults: List; + + override val onPagerChanged = Event1>(); + override val onPagerError = Event1(); + + + override fun getCurrentPager(): IPager { + return _parent.getWindow(); + } + + constructor(parent: ReusableRefreshPager) { + _parent = parent; + + synchronized(_parent.previousResults) { + _currentResults = _parent.previousResults.toList(); + _read += _currentResults.size; + } + parent.onPagerChanged.subscribe(onPagerChanged::emit); + parent.onPagerError.subscribe(onPagerError::emit); + } + + + override fun hasMorePages(): Boolean { + return _parent.previousResults.size > _read || _parent.hasMorePages(); + } + + override fun nextPage() { + synchronized(_parent.previousResults) { + if (_parent.previousResults.size <= _read) { + _parent.nextPage(); + _parent.getResults(); + } + _currentResults = _parent.previousResults.drop(_read).toList(); + _read += _currentResults.size; + } + } + + override fun getResults(): List { + return _currentResults; + } + + override fun findPager(query: (IPager) -> Boolean): IPager? { + return _parent.findPager(query); + } + } +} + +interface IReusablePager: IPager { + fun getWindow(): IPager; } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt index af033c51..58868ee4 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt @@ -3,12 +3,15 @@ package com.futo.platformplayer.fragment.mainactivity.main import android.content.Context import android.content.res.Configuration import android.graphics.Color +import android.util.DisplayMetrics +import android.view.Display import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.LayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout @@ -20,6 +23,7 @@ import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.others.ProgressBar import com.futo.platformplayer.views.others.TagsView @@ -28,7 +32,9 @@ import com.futo.platformplayer.views.adapters.InsertedViewHolder import com.futo.platformplayer.views.announcements.AnnouncementView import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.time.OffsetDateTime import kotlin.math.max @@ -68,6 +74,7 @@ abstract class FeedView : L private val _scrollListener: RecyclerView.OnScrollListener; private var _automaticNextPageCounter = 0; + private val _automaticBackoff = arrayOf(0, 500, 1000, 1000, 2000, 5000, 5000, 5000); constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder>? = null) : super(inflater.context) { this.fragment = fragment; @@ -129,6 +136,7 @@ abstract class FeedView : L _toolbarContentView = findViewById(R.id.container_toolbar_content); _nextPageHandler = TaskHandler>({fragment.lifecycleScope}, { + if (it is IAsyncPager<*>) it.nextPageAsync(); else @@ -182,26 +190,53 @@ abstract class FeedView : L private fun ensureEnoughContentVisible(filteredResults: List) { val canScroll = if (recyclerData.results.isEmpty()) false else { + val height = resources.displayMetrics.heightPixels; + val layoutManager = recyclerData.layoutManager val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() - - if (firstVisibleItemPosition != RecyclerView.NO_POSITION) { - val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition) - val itemHeight = firstVisibleView?.height ?: 0 - val occupiedSpace = recyclerData.results.size / recyclerData.layoutManager.spanCount * itemHeight - val recyclerViewHeight = _recyclerResults.height - Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight") - occupiedSpace >= recyclerViewHeight + val firstVisibleItemView = if(firstVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(firstVisibleItemPosition) else null; + val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition(); + val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null; + if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) { + false; + } + else if (firstVisibleItemView != null && height != null && firstVisibleItemView.height * recyclerData.results.size < height) { + false; } else { - false + true; } } + Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter") if (!canScroll || filteredResults.isEmpty()) { _automaticNextPageCounter++ - if(_automaticNextPageCounter <= 4) - loadNextPage() + if(_automaticNextPageCounter < _automaticBackoff.size) { + if(_automaticNextPageCounter > 0) { + val automaticNextPageCounterSaved = _automaticNextPageCounter; + fragment.lifecycleScope.launch(Dispatchers.Default) { + val backoff = _automaticBackoff[Math.min(_automaticBackoff.size - 1, _automaticNextPageCounter)]; + + withContext(Dispatchers.Main) { + setLoading(true); + } + delay(backoff.toLong()); + if(automaticNextPageCounterSaved == _automaticNextPageCounter) { + withContext(Dispatchers.Main) { + loadNextPage(); + } + } + else { + withContext(Dispatchers.Main) { + setLoading(false); + } + } + } + } + else + loadNextPage(); + } } else { + Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset"); _automaticNextPageCounter = 0; } } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt index 9cdac8f9..d5e01461 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt @@ -5,22 +5,32 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.allViews +import androidx.core.view.contains import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import com.futo.platformplayer.* +import com.futo.platformplayer.UISlideOverlays.Companion.showOrderOverlay import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.api.media.structures.IRefreshPager +import com.futo.platformplayer.api.media.structures.IReusablePager +import com.futo.platformplayer.api.media.structures.ReusablePager +import com.futo.platformplayer.api.media.structures.ReusableRefreshPager import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.stores.FragmentedStorage +import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.NoResultsView import com.futo.platformplayer.views.ToggleBar @@ -91,6 +101,7 @@ class HomeFragment : MainFragment() { _view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems); } + @SuppressLint("ViewConstructor") class HomeView : ContentFeedView { override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle(); @@ -100,11 +111,20 @@ class HomeFragment : MainFragment() { private val _taskGetPager: TaskHandler>; override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar + private var _lastPager: IReusablePager? = null; + constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { _taskGetPager = TaskHandler>({ fragment.lifecycleScope }, { StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope) }) - .success { loadedResult(it); } + .success { + val wrappedPager = if(it is IRefreshPager) + ReusableRefreshPager(it); + else + ReusablePager(it); + _lastPager = wrappedPager; + loadedResult(wrappedPager.getWindow()); + } .exception { } .exception { Logger.w(ChannelFragment.TAG, "Plugin failure.", it); @@ -208,21 +228,81 @@ class HomeFragment : MainFragment() { private val _filterLock = Object(); private var _toggleRecent = false; + private var _toggleWatched = false; + private var _togglePluginsDisabled = mutableListOf(); + private var _togglesConfig = FragmentedStorage.get("home_toggles"); fun initializeToolbarContent() { - //Not stable enough with current viewport paging, doesn't work with less results, and reloads content instead of just re-filtering existing - /* - _toggleBar = ToggleBar(context).apply { - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - } - synchronized(_filterLock) { - _toggleBar?.setToggles( - //TODO: loadResults needs to be replaced with an internal reload of the current content - ToggleBar.Toggle("Recent", _toggleRecent) { _toggleRecent = it; loadResults(false) } - ) - } + if(_toolbarContentView.allViews.any { it is ToggleBar }) + _toolbarContentView.removeView(_toolbarContentView.allViews.find { it is ToggleBar }); - _toolbarContentView.addView(_toggleBar, 0); - */ + if(Settings.instance.home.showHomeFilters) { + + if (!_togglesConfig.any()) { + _togglesConfig.set("today", "watched", "plugins"); + _togglesConfig.save(); + } + _toggleBar = ToggleBar(context).apply { + layoutParams = + LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + _togglePluginsDisabled.clear(); + synchronized(_filterLock) { + val buttonsPlugins = (if (_togglesConfig.contains("plugins")) + (StatePlatform.instance.getEnabledClients() + .map { plugin -> + ToggleBar.Toggle(plugin.name, plugin.icon, true, { + if (it) { + if (_togglePluginsDisabled.contains(plugin.id)) + _togglePluginsDisabled.remove(plugin.id); + } else { + if (!_togglePluginsDisabled.contains(plugin.id)) + _togglePluginsDisabled.add(plugin.id); + } + reloadForFilters(); + }).withTag("plugins") + }) + else listOf()) + val buttons = (listOf( + (if (_togglesConfig.contains("today")) + ToggleBar.Toggle("Today", _toggleRecent) { + _toggleRecent = it; reloadForFilters() + } + .withTag("today") else null), + (if (_togglesConfig.contains("watched")) + ToggleBar.Toggle("Unwatched", _toggleWatched) { + _toggleWatched = it; reloadForFilters() + } + .withTag("watched") else null), + ).filterNotNull() + buttonsPlugins) + .sortedBy { _togglesConfig.indexOf(it.tag ?: "") } ?: listOf() + + val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, { + showOrderOverlay(_overlayContainer, + "Visible home filters", + listOf( + Pair("Plugins", "plugins"), + Pair("Today", "today"), + Pair("Watched", "watched") + ), + { + val newArray = it.map { it.toString() }.toTypedArray(); + _togglesConfig.set(*(if (newArray.any()) newArray else arrayOf("none"))); + _togglesConfig.save(); + initializeToolbarContent(); + }, + "Select which toggles you want to see in order. You can also choose to hide filters in the Grayjay Settings" + ); + }).asButton(); + + val buttonsOrder = (buttons + listOf(buttonSettings)).toTypedArray(); + _toggleBar?.setToggles(*buttonsOrder); + } + + _toolbarContentView.addView(_toggleBar, 0); + } + } + fun reloadForFilters() { + _lastPager?.let { loadedResult(it.getWindow()) }; } override fun filterResults(results: List): List { @@ -232,7 +312,11 @@ class HomeFragment : MainFragment() { if(StateMeta.instance.isCreatorHidden(it.author.url)) return@filter false; - if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 23) { + if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 25) + return@filter false; + if(_toggleWatched && StateHistory.instance.isHistoryWatched(it.url, 0)) + return@filter false; + if(_togglePluginsDisabled.any() && it.id.pluginId != null && _togglePluginsDisabled.contains(it.id.pluginId)) { return@filter false; } diff --git a/app/src/main/java/com/futo/platformplayer/serializers/PlatformContentSerializer.kt b/app/src/main/java/com/futo/platformplayer/serializers/PlatformContentSerializer.kt index 02a39160..db540ea1 100644 --- a/app/src/main/java/com/futo/platformplayer/serializers/PlatformContentSerializer.kt +++ b/app/src/main/java/com/futo/platformplayer/serializers/PlatformContentSerializer.kt @@ -19,10 +19,10 @@ import kotlinx.serialization.json.jsonPrimitive class PlatformContentSerializer : JsonContentPolymorphicSerializer(SerializedPlatformContent::class) { override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - val obj = element.jsonObject["contentType"]; + val obj = element.jsonObject["contentType"] ?: element.jsonObject["ContentType"]; //TODO: Remove this temporary fallback..at some point - if(obj == null && element.jsonObject["isLive"]?.jsonPrimitive?.booleanOrNull != null) + if(obj == null && (element.jsonObject["isLive"]?.jsonPrimitive?.booleanOrNull ?: element.jsonObject["IsLive"]?.jsonPrimitive?.booleanOrNull) != null) return SerializedPlatformVideo.serializer(); if(obj?.jsonPrimitive?.isString != false) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index 1d1acff6..c92c80b0 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -69,7 +69,7 @@ class StateSubscriptions { val onSubscriptionsChanged = Event2, Boolean>(); - private val _subsExchangeServer = "https://exchange.grayjay.app/"; + private val _subsExchangeServer = "http://10.10.15.159"//"https://exchange.grayjay.app/"; private val _subscriptionKey = FragmentedStorage.get("sub_exchange_key"); init { diff --git a/app/src/main/java/com/futo/platformplayer/stores/StringArrayStorage.kt b/app/src/main/java/com/futo/platformplayer/stores/StringArrayStorage.kt index be1e69e3..0d072bba 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/StringArrayStorage.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/StringArrayStorage.kt @@ -41,4 +41,19 @@ class StringArrayStorage : FragmentedStorageFileJson() { return values.toList(); } } + fun any(): Boolean { + synchronized(values) { + return values.any(); + } + } + fun contains(v: String): Boolean { + synchronized(values) { + return values.contains(v); + } + } + fun indexOf(v: String): Int { + synchronized(values){ + return values.indexOf(v); + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt index 123b0320..ce0e19c2 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -153,6 +153,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( *resolves ); if (resolve != null) { + val invalids = resolve.filter { it.content.any { it.datetime == null } }; UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})") for(result in resolve){ val task = providedTasks?.find { it.url == result.channelUrl }; diff --git a/app/src/main/java/com/futo/platformplayer/subsexchange/ChannelResult.kt b/app/src/main/java/com/futo/platformplayer/subsexchange/ChannelResult.kt index c13f101c..957d415d 100644 --- a/app/src/main/java/com/futo/platformplayer/subsexchange/ChannelResult.kt +++ b/app/src/main/java/com/futo/platformplayer/subsexchange/ChannelResult.kt @@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.serializers.OffsetDateTimeSerializer +import com.futo.platformplayer.serializers.OffsetDateTimeStringSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.time.OffsetDateTime @@ -12,12 +13,12 @@ import java.time.OffsetDateTime @Serializable class ChannelResult( @kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class) - @SerialName("DateTime") + @SerialName("dateTime") var dateTime: OffsetDateTime, - @SerialName("ChannelUrl") + @SerialName("channelUrl") var channelUrl: String, - @SerialName("Content") + @SerialName("content") var content: List, - @SerialName("Channel") + @SerialName("channel") var channel: IPlatformChannel? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt b/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt index a58e17b0..0a55516f 100644 --- a/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt +++ b/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt @@ -52,6 +52,7 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array { val contractResolve = convertResolves(*resolves) val result = post("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json") + Logger.v("SubsExchangeClient", "Resolve:" + result); return Serializer.json.decodeFromString(result) } suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array { diff --git a/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt b/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt index be3d8df8..ef2eadbc 100644 --- a/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt +++ b/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt @@ -12,6 +12,7 @@ import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.media.models.channels.SerializedChannel import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.states.StateSubscriptionGroups @@ -46,7 +47,12 @@ class ToggleBar : LinearLayout { _tagsContainer.removeAllViews(); for(button in buttons) { _tagsContainer.addView(ToggleTagView(context).apply { - this.setInfo(button.name, button.isActive); + if(button.icon > 0) + this.setInfo(button.icon, button.name, button.isActive, button.isButton); + else if(button.iconVariable != null) + this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton); + else + this.setInfo(button.name, button.isActive, button.isButton); this.onClick.subscribe { button.action(it); }; }); } @@ -55,20 +61,42 @@ class ToggleBar : LinearLayout { class Toggle { val name: String; val icon: Int; + val iconVariable: ImageVariable?; val action: (Boolean)->Unit; val isActive: Boolean; + var isButton: Boolean = false + private set; + var tag: String? = null; + constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (Boolean)->Unit) { + this.name = name; + this.icon = 0; + this.iconVariable = icon; + this.action = action; + this.isActive = isActive; + } constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) { this.name = name; this.icon = icon; + this.iconVariable = null; this.action = action; this.isActive = isActive; } constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) { this.name = name; this.icon = 0; + this.iconVariable = null; this.action = action; this.isActive = isActive; } + + fun asButton(): Toggle{ + isButton = true; + return this; + } + fun withTag(str: String): Toggle { + tag = str; + return this; + } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt b/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt index 27c4e68d..65b13eb0 100644 --- a/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt @@ -4,19 +4,27 @@ import android.content.Context import android.graphics.Color import android.util.AttributeSet import android.view.LayoutInflater +import android.view.View import android.widget.FrameLayout +import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import com.bumptech.glide.Glide import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.images.GlideHelper +import com.futo.platformplayer.models.ImageVariable class ToggleTagView : LinearLayout { private val _root: FrameLayout; private val _textTag: TextView; private var _text: String = ""; + private var _image: ImageView; var isActive: Boolean = false private set; + var isButton: Boolean = false + private set; var onClick = Event1(); @@ -24,7 +32,12 @@ class ToggleTagView : LinearLayout { LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true); _root = findViewById(R.id.root); _textTag = findViewById(R.id.text_tag); - _root.setOnClickListener { setToggle(!isActive); onClick.emit(isActive); } + _image = findViewById(R.id.image_tag); + _root.setOnClickListener { + if(!isButton) + setToggle(!isActive); + onClick.emit(isActive); + } } fun setToggle(isActive: Boolean) { @@ -39,9 +52,27 @@ class ToggleTagView : LinearLayout { } } - fun setInfo(text: String, isActive: Boolean) { + fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false) { _text = text; _textTag.text = text; setToggle(isActive); + _image.setImageResource(imageResource); + _image.visibility = View.VISIBLE; + this.isButton = isButton; + } + fun setInfo(image: ImageVariable, text: String, isActive: Boolean, isButton: Boolean = false) { + _text = text; + _textTag.text = text; + setToggle(isActive); + image.setImageView(_image, R.drawable.ic_error_pred); + _image.visibility = View.VISIBLE; + this.isButton = isButton; + } + fun setInfo(text: String, isActive: Boolean, isButton: Boolean = false) { + _image.visibility = View.GONE; + _text = text; + _textTag.text = text; + setToggle(isActive); + this.isButton = isButton; } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt index 89cf1359..58850998 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt @@ -113,6 +113,13 @@ class SlideUpMenuOverlay : RelativeLayout { _textOK.visibility = View.VISIBLE; } } + fun getSlideUpItemByTag(itemTag: Any?): SlideUpMenuItem? { + for(view in groupItems){ + if(view is SlideUpMenuItem && view.itemTag == itemTag) + return view; + } + return null; + } fun selectOption(groupTag: Any?, itemTag: Any?, multiSelect: Boolean = false, toggle: Boolean = false): Boolean { var didSelect = false; diff --git a/app/src/main/res/layout/view_toggle_bar.xml b/app/src/main/res/layout/view_toggle_bar.xml index 3da2f363..7b99d09f 100644 --- a/app/src/main/res/layout/view_toggle_bar.xml +++ b/app/src/main/res/layout/view_toggle_bar.xml @@ -3,14 +3,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - - + \ No newline at end of file diff --git a/app/src/main/res/layout/view_toggle_tag.xml b/app/src/main/res/layout/view_toggle_tag.xml index 886f2de5..eca7010c 100644 --- a/app/src/main/res/layout/view_toggle_tag.xml +++ b/app/src/main/res/layout/view_toggle_tag.xml @@ -10,16 +10,27 @@ android:layout_marginTop="17dp" android:layout_marginBottom="8dp" android:id="@+id/root"> - - + android:layout_height="match_parent" + android:orientation="horizontal"> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d82ff85..85549f27 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -417,6 +417,8 @@ If subscription groups should be shown above your subscriptions to filter Preview Feed Items When the preview feedstyle is used, if items should auto-preview when scrolling over them + Show Home Filters + If the home filters should be shown above home Log Level Logging Sync Grayjay From b14518edb1e0caade4917d18ad79432c2ff5f2f4 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Sat, 5 Apr 2025 01:02:50 +0200 Subject: [PATCH 049/330] Home filter fixes, persistent sorts, subs exchange fixes, playlist video options --- .../java/com/futo/platformplayer/Settings.kt | 2 + .../mainactivity/main/DownloadsFragment.kt | 28 +++-- .../fragment/mainactivity/main/FeedView.kt | 3 + .../mainactivity/main/HomeFragment.kt | 52 ++++---- .../mainactivity/main/PlaylistFragment.kt | 4 + .../mainactivity/main/PlaylistsFragment.kt | 29 +++-- .../mainactivity/main/VideoDetailView.kt | 3 + .../mainactivity/main/VideoListEditorView.kt | 2 + .../mainactivity/main/WatchLaterFragment.kt | 3 + .../states/StateSubscriptions.kt | 2 +- .../SubscriptionsTaskFetchAlgorithm.kt | 118 +++++++++++------- .../subsexchange/SubsExchangeClient.kt | 8 +- .../views/adapters/VideoListEditorAdapter.kt | 2 + .../adapters/VideoListEditorViewHolder.kt | 7 ++ .../views/lists/VideoListEditorView.kt | 5 + .../views/others/ToggleTagView.kt | 3 + .../views/overlays/QueueEditorOverlay.kt | 6 + app/src/main/res/layout/list_playlist.xml | 42 +++++-- app/src/main/res/layout/view_toggle_tag.xml | 11 +- app/src/main/res/values/strings.xml | 2 + 20 files changed, 218 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index f68a60f9..cedc4316 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -218,6 +218,8 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4) var showHomeFilters: Boolean = true; + @FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5) + var showHomeFiltersPluginNames: Boolean = false; @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) var previewFeedItems: Boolean = true; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt index d402a6e2..217165ae 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt @@ -21,6 +21,8 @@ import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists +import com.futo.platformplayer.stores.FragmentedStorage +import com.futo.platformplayer.stores.StringStorage import com.futo.platformplayer.toHumanBytesSize import com.futo.platformplayer.toHumanDuration import com.futo.platformplayer.views.AnyInsertedAdapterView @@ -103,12 +105,15 @@ class DownloadsFragment : MainFragment() { private val _listDownloaded: AnyInsertedAdapterView; private var lastDownloads: List? = null; - private var ordering: String? = "nameAsc"; + private var ordering = FragmentedStorage.get("downloads_ordering") constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) { inflater.inflate(R.layout.fragment_downloads, this); _frag = frag; + if(ordering.value.isNullOrBlank()) + ordering.value = "nameAsc"; + _usageUsed = findViewById(R.id.downloads_usage_used); _usageAvailable = findViewById(R.id.downloads_usage_available); _usageProgress = findViewById(R.id.downloads_usage_progress); @@ -132,22 +137,23 @@ class DownloadsFragment : MainFragment() { spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also { it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple); }; - spinnerSortBy.setSelection(0); + val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc"); spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { when(pos) { - 0 -> ordering = "nameAsc" - 1 -> ordering = "nameDesc" - 2 -> ordering = "downloadDateAsc" - 3 -> ordering = "downloadDateDesc" - 4 -> ordering = "releasedAsc" - 5 -> ordering = "releasedDesc" - else -> ordering = null + 0 -> ordering.setAndSave("nameAsc") + 1 -> ordering.setAndSave("nameDesc") + 2 -> ordering.setAndSave("downloadDateAsc") + 3 -> ordering.setAndSave("downloadDateDesc") + 4 -> ordering.setAndSave("releasedAsc") + 5 -> ordering.setAndSave("releasedDesc") + else -> ordering.setAndSave("") } updateContentFilters() } override fun onNothingSelected(parent: AdapterView<*>?) = Unit }; + spinnerSortBy.setSelection(Math.max(0, options.indexOf(ordering.value))); _listDownloaded = findViewById(R.id.list_downloaded) .asAnyWithTop(findViewById(R.id.downloads_top)) { @@ -230,8 +236,8 @@ class DownloadsFragment : MainFragment() { var vidsToReturn = vids; if(!_listDownloadSearch.text.isNullOrEmpty()) vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) || it.author.name.contains(_listDownloadSearch.text, true) }; - if(!ordering.isNullOrEmpty()) { - vidsToReturn = when(ordering){ + if(!ordering.value.isNullOrEmpty()) { + vidsToReturn = when(ordering.value){ "downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX }; "downloadDateDesc" -> vidsToReturn.sortedByDescending { it.downloadDate ?: OffsetDateTime.MIN }; "nameAsc" -> vidsToReturn.sortedBy { it.name.lowercase() } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt index 58868ee4..17ca5510 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt @@ -240,6 +240,9 @@ abstract class FeedView : L _automaticNextPageCounter = 0; } } + fun resetAutomaticNextPageCounter(){ + _automaticNextPageCounter = 0; + } protected fun setTextCentered(text: String?) { _textCentered.text = text; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt index d5e01461..a450a2ed 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt @@ -6,7 +6,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.allViews -import androidx.core.view.contains import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import com.futo.platformplayer.* @@ -29,6 +28,7 @@ import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.views.FeedStyle @@ -37,7 +37,6 @@ import com.futo.platformplayer.views.ToggleBar import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewHolder -import com.futo.platformplayer.views.announcements.AnnouncementView import com.futo.platformplayer.views.buttons.BigButton import kotlinx.coroutines.runBlocking import java.time.OffsetDateTime @@ -49,6 +48,12 @@ class HomeFragment : MainFragment() { private var _view: HomeView? = null; private var _cachedRecyclerData: FeedView.RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null; + private var _cachedLastPager: IReusablePager? = null + + private var _toggleRecent = false; + private var _toggleWatched = false; + private var _togglePluginsDisabled = mutableListOf(); + fun reloadFeed() { _view?.reloadFeed() @@ -74,7 +79,7 @@ class HomeFragment : MainFragment() { } override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val view = HomeView(this, inflater, _cachedRecyclerData); + val view = HomeView(this, inflater, _cachedRecyclerData, _cachedLastPager); _view = view; return view; } @@ -92,6 +97,7 @@ class HomeFragment : MainFragment() { val view = _view; if (view != null) { _cachedRecyclerData = view.recyclerData; + _cachedLastPager = view.lastPager; view.cleanup(); _view = null; } @@ -111,9 +117,10 @@ class HomeFragment : MainFragment() { private val _taskGetPager: TaskHandler>; override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar - private var _lastPager: IReusablePager? = null; + var lastPager: IReusablePager? = null; - constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { + constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null, cachedLastPager: IReusablePager? = null) : super(fragment, inflater, cachedRecyclerData) { + lastPager = cachedLastPager _taskGetPager = TaskHandler>({ fragment.lifecycleScope }, { StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope) }) @@ -122,7 +129,8 @@ class HomeFragment : MainFragment() { ReusableRefreshPager(it); else ReusablePager(it); - _lastPager = wrappedPager; + lastPager = wrappedPager; + resetAutomaticNextPageCounter(); loadedResult(wrappedPager.getWindow()); } .exception { } @@ -227,9 +235,6 @@ class HomeFragment : MainFragment() { } private val _filterLock = Object(); - private var _toggleRecent = false; - private var _toggleWatched = false; - private var _togglePluginsDisabled = mutableListOf(); private var _togglesConfig = FragmentedStorage.get("home_toggles"); fun initializeToolbarContent() { if(_toolbarContentView.allViews.any { it is ToggleBar }) @@ -245,18 +250,19 @@ class HomeFragment : MainFragment() { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } - _togglePluginsDisabled.clear(); + fragment._togglePluginsDisabled.clear(); synchronized(_filterLock) { val buttonsPlugins = (if (_togglesConfig.contains("plugins")) (StatePlatform.instance.getEnabledClients() + .filter { it is JSClient && it.enableInHome } .map { plugin -> - ToggleBar.Toggle(plugin.name, plugin.icon, true, { + ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) plugin.name else "", plugin.icon, !fragment._togglePluginsDisabled.contains(plugin.id), { if (it) { - if (_togglePluginsDisabled.contains(plugin.id)) - _togglePluginsDisabled.remove(plugin.id); + if (fragment._togglePluginsDisabled.contains(plugin.id)) + fragment._togglePluginsDisabled.remove(plugin.id); } else { - if (!_togglePluginsDisabled.contains(plugin.id)) - _togglePluginsDisabled.add(plugin.id); + if (!fragment._togglePluginsDisabled.contains(plugin.id)) + fragment._togglePluginsDisabled.add(plugin.id); } reloadForFilters(); }).withTag("plugins") @@ -264,13 +270,13 @@ class HomeFragment : MainFragment() { else listOf()) val buttons = (listOf( (if (_togglesConfig.contains("today")) - ToggleBar.Toggle("Today", _toggleRecent) { - _toggleRecent = it; reloadForFilters() + ToggleBar.Toggle("Today", fragment._toggleRecent) { + fragment._toggleRecent = it; reloadForFilters() } .withTag("today") else null), (if (_togglesConfig.contains("watched")) - ToggleBar.Toggle("Unwatched", _toggleWatched) { - _toggleWatched = it; reloadForFilters() + ToggleBar.Toggle("Unwatched", fragment._toggleWatched) { + fragment._toggleWatched = it; reloadForFilters() } .withTag("watched") else null), ).filterNotNull() + buttonsPlugins) @@ -302,7 +308,7 @@ class HomeFragment : MainFragment() { } } fun reloadForFilters() { - _lastPager?.let { loadedResult(it.getWindow()) }; + lastPager?.let { loadedResult(it.getWindow()) }; } override fun filterResults(results: List): List { @@ -312,11 +318,11 @@ class HomeFragment : MainFragment() { if(StateMeta.instance.isCreatorHidden(it.author.url)) return@filter false; - if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 25) + if(fragment._toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 25) return@filter false; - if(_toggleWatched && StateHistory.instance.isHistoryWatched(it.url, 0)) + if(fragment._toggleWatched && StateHistory.instance.isHistoryWatched(it.url, 0)) return@filter false; - if(_togglePluginsDisabled.any() && it.id.pluginId != null && _togglePluginsDisabled.contains(it.id.pluginId)) { + if(fragment._togglePluginsDisabled.any() && it.id.pluginId != null && fragment._togglePluginsDisabled.contains(it.id.pluginId)) { return@filter false; } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt index b58e3ee2..c56585b0 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt @@ -326,6 +326,10 @@ class PlaylistFragment : MainFragment() { playlist.videos = ArrayList(playlist.videos.filter { it != video }); StatePlaylists.instance.createOrUpdatePlaylist(playlist); } + + override fun onVideoOptions(video: IPlatformVideo) { + UISlideOverlays.showVideoOptionsOverlay(video, overlayContainer); + } override fun onVideoClicked(video: IPlatformVideo) { val playlist = _playlist; if (playlist != null) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistsFragment.kt index 9d188415..58caabe1 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistsFragment.kt @@ -26,6 +26,8 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists +import com.futo.platformplayer.stores.FragmentedStorage +import com.futo.platformplayer.stores.StringStorage import com.futo.platformplayer.views.SearchView import com.futo.platformplayer.views.adapters.* import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay @@ -82,7 +84,7 @@ class PlaylistsFragment : MainFragment() { private var _listPlaylistsSearch: EditText; - private var _ordering: String? = null; + private var _ordering = FragmentedStorage.get("playlists_ordering") constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) { @@ -145,24 +147,25 @@ class PlaylistsFragment : MainFragment() { spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.playlists_sortby_array)).also { it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple); }; - spinnerSortBy.setSelection(0); + val options = listOf("nameAsc", "nameDesc", "dateEditAsc", "dateEditDesc", "dateCreateAsc", "dateCreateDesc", "datePlayAsc", "datePlayDesc"); spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { when(pos) { - 0 -> _ordering = "nameAsc" - 1 -> _ordering = "nameDesc" - 2 -> _ordering = "dateEditAsc" - 3 -> _ordering = "dateEditDesc" - 4 -> _ordering = "dateCreateAsc" - 5 -> _ordering = "dateCreateDesc" - 6 -> _ordering = "datePlayAsc" - 7 -> _ordering = "datePlayDesc" - else -> _ordering = null + 0 -> _ordering.setAndSave("nameAsc") + 1 -> _ordering.setAndSave("nameDesc") + 2 -> _ordering.setAndSave("dateEditAsc") + 3 -> _ordering.setAndSave("dateEditDesc") + 4 -> _ordering.setAndSave("dateCreateAsc") + 5 -> _ordering.setAndSave("dateCreateDesc") + 6 -> _ordering.setAndSave("datePlayAsc") + 7 -> _ordering.setAndSave("datePlayDesc") + else -> _ordering.setAndSave("") } updatePlaylistsFiltering() } override fun onNothingSelected(parent: AdapterView<*>?) = Unit }; + spinnerSortBy.setSelection(Math.max(0, options.indexOf(_ordering.value))); findViewById(R.id.text_view_all).setOnClickListener { _fragment.navigate(context.getString(R.string.watch_later)); }; @@ -214,8 +217,8 @@ class PlaylistsFragment : MainFragment() { var playlistsToReturn = pls; if(!_listPlaylistsSearch.text.isNullOrEmpty()) playlistsToReturn = playlistsToReturn.filter { it.name.contains(_listPlaylistsSearch.text, true) }; - if(!_ordering.isNullOrEmpty()){ - playlistsToReturn = when(_ordering){ + if(!_ordering.value.isNullOrEmpty()){ + playlistsToReturn = when(_ordering.value){ "nameAsc" -> playlistsToReturn.sortedBy { it.name.lowercase() } "nameDesc" -> playlistsToReturn.sortedByDescending { it.name.lowercase() }; "dateEditAsc" -> playlistsToReturn.sortedBy { it.dateUpdate ?: OffsetDateTime.MAX }; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 25b03cb1..7176d125 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -693,6 +693,9 @@ class VideoDetailView : ConstraintLayout { _container_content_description.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_queue.onClose.subscribe { switchContentView(_container_content_main); }; + _container_content_queue.onOptions.subscribe { + UISlideOverlays.showVideoOptionsOverlay(it, _overlayContainer); + } _container_content_replies.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_support.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_browser.onClose.subscribe { switchContentView(_container_content_main); }; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt index 441f7421..c0383b89 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt @@ -104,6 +104,7 @@ abstract class VideoListEditorView : LinearLayout { videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged); videoListEditorView.onVideoRemoved.subscribe(::onVideoRemoved); + videoListEditorView.onVideoOptions.subscribe(::onVideoOptions); videoListEditorView.onVideoClicked.subscribe(::onVideoClicked); _videoListEditorView = videoListEditorView; @@ -122,6 +123,7 @@ abstract class VideoListEditorView : LinearLayout { open fun onShuffleClick() { } open fun onEditClick() { } open fun onVideoRemoved(video: IPlatformVideo) {} + open fun onVideoOptions(video: IPlatformVideo) {} open fun onVideoOrderChanged(videos : List) {} open fun onVideoClicked(video: IPlatformVideo) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/WatchLaterFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/WatchLaterFragment.kt index 4d3c65bd..9d66c3f7 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/WatchLaterFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/WatchLaterFragment.kt @@ -103,6 +103,9 @@ class WatchLaterFragment : MainFragment() { StatePlaylists.instance.removeFromWatchLater(video, true); } } + override fun onVideoOptions(video: IPlatformVideo) { + UISlideOverlays.showVideoOptionsOverlay(video, overlayContainer); + } override fun onVideoClicked(video: IPlatformVideo) { val watchLater = StatePlaylists.instance.getWatchLater(); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index c92c80b0..1d1acff6 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -69,7 +69,7 @@ class StateSubscriptions { val onSubscriptionsChanged = Event2, Boolean>(); - private val _subsExchangeServer = "http://10.10.15.159"//"https://exchange.grayjay.app/"; + private val _subsExchangeServer = "https://exchange.grayjay.app/"; private val _subscriptionKey = FragmentedStorage.get("sub_exchange_key"); init { diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt index ce0e19c2..1a1d503b 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -82,23 +82,30 @@ abstract class SubscriptionsTaskFetchAlgorithm( var providedTasks: MutableList? = null; try { - val contractableTasks = - tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) }; - contract = - if (contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map { - ChannelRequest(it.url) - }.toTypedArray()) else null; - if (contract?.provided?.isNotEmpty() == true) - Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}"); - if (contract != null && contract.required.isNotEmpty()) { - providedTasks = mutableListOf() - for (task in tasks.toList()) { - if (!task.fromCache && !task.fromPeek && contract.provided.contains(task.url)) { - providedTasks.add(task); - tasks.remove(task); + val contractingTime = measureTimeMillis { + val contractableTasks = + tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) }; + contract = + if (contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map { + ChannelRequest(it.url) + }.toTypedArray()) else null; + if (contract?.provided?.isNotEmpty() == true) + Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}"); + if (contract != null && contract!!.required.isNotEmpty()) { + providedTasks = mutableListOf() + for (task in tasks.toList()) { + if (!task.fromCache && !task.fromPeek && contract!!.provided.contains(task.url)) { + providedTasks!!.add(task); + tasks.remove(task); + } } } } + if(contract != null) + Logger.i(TAG, "Subscription Exchange contract received in ${contractingTime}ms"); + else if(contractingTime > 100) + Logger.i(TAG, "Subscription Exchange contract failed to received in${contractingTime}ms"); + } catch(ex: Throwable){ Logger.e("SubscriptionsTaskFetchAlgorithm", "Failed to retrieve SubsExchange contract due to: " + ex.message, ex); @@ -109,6 +116,8 @@ abstract class SubscriptionsTaskFetchAlgorithm( val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels); val taskResults = arrayListOf(); + var resolveCount = 0; + var resolveTime = 0L; val timeTotal = measureTimeMillis { for(task in forkTasks) { try { @@ -137,51 +146,68 @@ abstract class SubscriptionsTaskFetchAlgorithm( } }; } - } - //Resolve Subscription Exchange - if(contract != null) { - try { - val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract.required.contains(it.task.url) }.map { - ChannelResolve( - it.task.url, - it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) } - ) - }.toTypedArray() - val resolve = subsExchangeClient?.resolveContract( - contract, - *resolves - ); - if (resolve != null) { - val invalids = resolve.filter { it.content.any { it.datetime == null } }; - UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})") - for(result in resolve){ - val task = providedTasks?.find { it.url == result.channelUrl }; - if(task != null) { - taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null)); - providedTasks?.remove(task); + //Resolve Subscription Exchange + if(contract != null) { + try { + resolveTime = measureTimeMillis { + val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract!!.required.contains(it.task.url) }.map { + ChannelResolve( + it.task.url, + it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) } + ) + }.toTypedArray() + val resolve = subsExchangeClient?.resolveContract( + contract!!, + *resolves + ); + if (resolve != null) { + resolveCount = resolves.size; + UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}") + for(result in resolve){ + val task = providedTasks?.find { it.url == result.channelUrl }; + if(task != null) { + taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null)); + providedTasks?.remove(task); + } + } + } + if (providedTasks != null) { + for(task in providedTasks!!) { + taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange"))); + } } } + Logger.i(TAG, "Subscription Exchange contract resolved in ${resolveTime}ms"); + } - if (providedTasks != null) { - for(task in providedTasks) { - taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange"))); - } + catch(ex: Throwable) { + //TODO: fetch remainder after all? + Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex); } } - catch(ex: Throwable) { - //TODO: fetch remainder after all? - Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex); - } } - Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms") + Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms"); + if(resolveCount > 0) { + val selfFetchTime = timeTotal - resolveTime; + val selfFetchCount = tasks.count { !it.fromPeek && !it.fromCache }; + if(selfFetchCount > 0) { + val selfResolvePercentage = resolveCount.toDouble() / selfFetchCount; + val estimateSelfFetchTime = selfFetchTime + selfFetchTime * selfResolvePercentage; + val selfFetchDelta = timeTotal - estimateSelfFetchTime; + if(selfFetchDelta > 0) + UIDialogs.appToast("Subscription Exchange lost ${selfFetchDelta}ms (out of ${timeTotal}ms)", true); + else + UIDialogs.appToast("Subscription Exchange saved ${(selfFetchDelta * -1).toInt()}ms (out of ${timeTotal}ms)", true); + } + } //Cache pagers grouped by channel val groupedPagers = taskResults.groupBy { it.task.sub.channel.url } .map { entry -> val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null; - val liveTasks = entry.value.filter { !it.task.fromCache }; + val liveTasks = entry.value.filter { !it.task.fromCache && it.pager != null }; val cachedTasks = entry.value.filter { it.task.fromCache }; val livePager = if(liveTasks.isNotEmpty()) StateCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }) { onNewCacheHit.emit(sub!!, it); diff --git a/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt b/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt index 0a55516f..6f38014d 100644 --- a/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt +++ b/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt @@ -28,7 +28,7 @@ import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.RSAPublicKeySpec -class SubsExchangeClient(private val server: String, private val privateKey: String) { +class SubsExchangeClient(private val server: String, private val privateKey: String, private val contractTimeout: Int = 1000) { private val json = Json { ignoreUnknownKeys = true @@ -40,7 +40,7 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str // Endpoint: Contract fun requestContract(vararg channels: ChannelRequest): ExchangeContract { - val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json") + val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json", contractTimeout) return Json.decodeFromString(data) } suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract { @@ -74,9 +74,11 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str } // IO methods - private fun post(query: String, body: String, contentType: String): String { + private fun post(query: String, body: String, contentType: String, timeout: Int = 0): String { val url = URL("${server.trim('/')}$query") with(url.openConnection() as HttpURLConnection) { + if(timeout > 0) + this.connectTimeout = timeout requestMethod = "POST" setRequestProperty("Content-Type", contentType) doOutput = true diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorAdapter.kt index aa4ee66f..f7c313f5 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorAdapter.kt @@ -14,6 +14,7 @@ class VideoListEditorAdapter : RecyclerView.Adapter { val onClick = Event1(); val onRemove = Event1(); + val onOptions = Event1(); var canEdit = false private set; @@ -28,6 +29,7 @@ class VideoListEditorAdapter : RecyclerView.Adapter { val holder = VideoListEditorViewHolder(view, _touchHelper); holder.onRemove.subscribe { v -> onRemove.emit(v); }; + holder.onOptions.subscribe { v -> onOptions.emit(v); }; holder.onClick.subscribe { v -> onClick.emit(v); }; return holder; diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt index 3cf3194b..77df0665 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt @@ -32,6 +32,7 @@ class VideoListEditorViewHolder : ViewHolder { private val _containerDuration: LinearLayout; private val _containerLive: LinearLayout; private val _imageRemove: ImageButton; + private val _imageOptions: ImageButton; private val _imageDragDrop: ImageButton; private val _platformIndicator: PlatformIndicator; private val _layoutDownloaded: FrameLayout; @@ -41,6 +42,7 @@ class VideoListEditorViewHolder : ViewHolder { val onClick = Event1(); val onRemove = Event1(); + val onOptions = Event1(); @SuppressLint("ClickableViewAccessibility") constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) { @@ -54,6 +56,7 @@ class VideoListEditorViewHolder : ViewHolder { _containerDuration = view.findViewById(R.id.thumbnail_duration_container); _containerLive = view.findViewById(R.id.thumbnail_live_container); _imageRemove = view.findViewById(R.id.image_trash); + _imageOptions = view.findViewById(R.id.image_settings); _imageDragDrop = view.findViewById(R.id.image_drag_drop); _platformIndicator = view.findViewById(R.id.thumbnail_platform); _layoutDownloaded = view.findViewById(R.id.layout_downloaded); @@ -74,6 +77,10 @@ class VideoListEditorViewHolder : ViewHolder { val v = video ?: return@setOnClickListener; onRemove.emit(v); }; + _imageOptions?.setOnClickListener { + val v = video ?: return@setOnClickListener; + onOptions.emit(v); + } } fun bind(v: IPlatformVideo, canEdit: Boolean) { diff --git a/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt b/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt index 3bfce0be..08d32ac3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt @@ -8,6 +8,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event2 @@ -22,6 +23,7 @@ class VideoListEditorView : FrameLayout { val onVideoOrderChanged = Event1>() val onVideoRemoved = Event1(); + val onVideoOptions = Event1(); val onVideoClicked = Event1(); val isEmpty get() = _videos.isEmpty(); @@ -54,6 +56,9 @@ class VideoListEditorView : FrameLayout { } }; + adapterVideos.onOptions.subscribe { v -> + onVideoOptions?.emit(v); + } adapterVideos.onRemove.subscribe { v -> val executeDelete = { synchronized(_videos) { diff --git a/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt b/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt index 65b13eb0..059405ad 100644 --- a/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt @@ -58,6 +58,7 @@ class ToggleTagView : LinearLayout { setToggle(isActive); _image.setImageResource(imageResource); _image.visibility = View.VISIBLE; + _textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE; this.isButton = isButton; } fun setInfo(image: ImageVariable, text: String, isActive: Boolean, isButton: Boolean = false) { @@ -66,12 +67,14 @@ class ToggleTagView : LinearLayout { setToggle(isActive); image.setImageView(_image, R.drawable.ic_error_pred); _image.visibility = View.VISIBLE; + _textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE; this.isButton = isButton; } fun setInfo(text: String, isActive: Boolean, isButton: Boolean = false) { _image.visibility = View.GONE; _text = text; _textTag.text = text; + _textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE; setToggle(isActive); this.isButton = isButton; } diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt index a7181e90..53982097 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/QueueEditorOverlay.kt @@ -8,7 +8,9 @@ import android.widget.LinearLayout import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.R import com.futo.platformplayer.UISlideOverlays +import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.views.lists.VideoListEditorView import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay @@ -23,6 +25,7 @@ class QueueEditorOverlay : LinearLayout { private val _overlayContainer: FrameLayout; + val onOptions = Event1(); val onClose = Event0(); constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { @@ -35,6 +38,9 @@ class QueueEditorOverlay : LinearLayout { _topbar.onClose.subscribe(this, onClose::emit); _editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) } + _editor.onVideoOptions.subscribe { v -> + onOptions?.emit(v); + } _editor.onVideoRemoved.subscribe { v -> StatePlayer.instance.removeFromQueue(v); _topbar.setInfo(context.getString(R.string.queue), "${StatePlayer.instance.queueSize} " + context.getString(R.string.videos)); diff --git a/app/src/main/res/layout/list_playlist.xml b/app/src/main/res/layout/list_playlist.xml index d51cdfc5..c9ea9927 100644 --- a/app/src/main/res/layout/list_playlist.xml +++ b/app/src/main/res/layout/list_playlist.xml @@ -135,7 +135,7 @@ android:ellipsize="end" app:layout_constraintLeft_toRightOf="@id/layout_video_thumbnail" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintRight_toLeftOf="@id/image_trash" + app:layout_constraintRight_toLeftOf="@id/buttons" app:layout_constraintBottom_toTopOf="@id/text_author" android:layout_marginStart="10dp" /> @@ -152,7 +152,7 @@ android:ellipsize="end" app:layout_constraintLeft_toRightOf="@id/layout_video_thumbnail" app:layout_constraintTop_toBottomOf="@id/text_video_name" - app:layout_constraintRight_toLeftOf="@id/image_trash" + app:layout_constraintRight_toLeftOf="@id/buttons" app:layout_constraintBottom_toTopOf="@id/text_video_metadata" android:layout_marginStart="10dp" /> @@ -169,19 +169,35 @@ android:ellipsize="end" app:layout_constraintLeft_toRightOf="@id/layout_video_thumbnail" app:layout_constraintTop_toBottomOf="@id/text_author" - app:layout_constraintRight_toLeftOf="@id/image_trash" + app:layout_constraintRight_toLeftOf="@id/buttons" android:layout_marginStart="10dp" /> - + app:layout_constraintBottom_toBottomOf="@id/layout_video_thumbnail" > + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_toggle_tag.xml b/app/src/main/res/layout/view_toggle_tag.xml index eca7010c..5f285bd2 100644 --- a/app/src/main/res/layout/view_toggle_tag.xml +++ b/app/src/main/res/layout/view_toggle_tag.xml @@ -3,8 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="32dp" - android:paddingStart="15dp" - android:paddingEnd="15dp" + android:paddingStart="12dp" + android:paddingEnd="12dp" android:background="@drawable/background_pill" android:layout_marginEnd="6dp" android:layout_marginTop="17dp" @@ -19,12 +19,15 @@ android:visibility="gone" android:layout_width="24dp" android:layout_height="24dp" - android:layout_marginRight="5dp" - android:layout_marginTop="4dp" /> + android:layout_gravity="center" + android:layout_marginLeft="2.5dp" + android:layout_marginRight="2.5dp" /> When the preview feedstyle is used, if items should auto-preview when scrolling over them Show Home Filters If the home filters should be shown above home + Home filter Plugin Names + If home filters should show full plugin names or just icons Log Level Logging Sync Grayjay From 7b355139fb9fbc79578af84c5b65aabec41faa07 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Mon, 7 Apr 2025 23:31:00 +0200 Subject: [PATCH 050/330] Subscription persistence fixes, home toggle fixes, subs exchange gzip, etc --- .../java/com/futo/platformplayer/Utility.kt | 35 ++++++++ .../mainactivity/main/HomeFragment.kt | 38 ++++++--- .../main/SubscriptionsFeedFragment.kt | 41 ++++++---- .../SubscriptionsTaskFetchAlgorithm.kt | 80 ++++++++++++------- .../subsexchange/SubsExchangeClient.kt | 35 +++++--- .../futo/platformplayer/views/ToggleBar.kt | 10 +-- .../views/others/ToggleTagView.kt | 24 +++++- .../views/subscriptions/SubscriptionBar.kt | 8 +- 8 files changed, 191 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt index 64efb992..5cd5d26f 100644 --- a/app/src/main/java/com/futo/platformplayer/Utility.kt +++ b/app/src/main/java/com/futo/platformplayer/Utility.kt @@ -27,14 +27,18 @@ import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.PlatformVideoWithTime import com.futo.platformplayer.others.PlatformLinkMovementMethod import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.nio.ByteBuffer import java.nio.ByteOrder +import java.time.OffsetDateTime import java.util.* import java.util.concurrent.ThreadLocalRandom +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "; fun getRandomString(sizeOfRandomString: Int): String { @@ -279,3 +283,34 @@ fun ByteBuffer.toUtf8String(): String { get(remainingBytes) return String(remainingBytes, Charsets.UTF_8) } + + +fun ByteArray.toGzip(): ByteArray { + if (this == null || this.isEmpty()) return ByteArray(0) + + val gzipTimeStart = OffsetDateTime.now(); + + val outputStream = ByteArrayOutputStream() + GZIPOutputStream(outputStream).use { gzip -> + gzip.write(this) + } + val result = outputStream.toByteArray(); + Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms"); + return result; +} + +fun ByteArray.fromGzip(): ByteArray { + if (this == null || this.isEmpty()) return ByteArray(0) + + val inputStream = ByteArrayInputStream(this) + val outputStream = ByteArrayOutputStream() + + GZIPInputStream(inputStream).use { gzip -> + val buffer = ByteArray(1024) + var bytesRead: Int + while (gzip.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + } + } + return outputStream.toByteArray() +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt index a450a2ed..988d7a3f 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt @@ -250,39 +250,53 @@ class HomeFragment : MainFragment() { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } - fragment._togglePluginsDisabled.clear(); + synchronized(_filterLock) { - val buttonsPlugins = (if (_togglesConfig.contains("plugins")) + var buttonsPlugins: List = listOf() + buttonsPlugins = (if (_togglesConfig.contains("plugins")) (StatePlatform.instance.getEnabledClients() .filter { it is JSClient && it.enableInHome } .map { plugin -> - ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) plugin.name else "", plugin.icon, !fragment._togglePluginsDisabled.contains(plugin.id), { - if (it) { + ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) plugin.name else "", plugin.icon, !fragment._togglePluginsDisabled.contains(plugin.id), { view, active -> + var dontSwap = false; + if (active) { if (fragment._togglePluginsDisabled.contains(plugin.id)) fragment._togglePluginsDisabled.remove(plugin.id); } else { - if (!fragment._togglePluginsDisabled.contains(plugin.id)) - fragment._togglePluginsDisabled.add(plugin.id); + if (!fragment._togglePluginsDisabled.contains(plugin.id)) { + val enabledClients = StatePlatform.instance.getEnabledClients(); + val availableAfterDisable = enabledClients.count { !fragment._togglePluginsDisabled.contains(it.id) && it.id != plugin.id }; + if(availableAfterDisable > 0) + fragment._togglePluginsDisabled.add(plugin.id); + else { + UIDialogs.appToast("Home needs atleast 1 plugin active"); + dontSwap = true; + } + } + } + if(!dontSwap) + reloadForFilters(); + else { + view.setToggle(!active); } - reloadForFilters(); }).withTag("plugins") }) else listOf()) val buttons = (listOf( (if (_togglesConfig.contains("today")) - ToggleBar.Toggle("Today", fragment._toggleRecent) { - fragment._toggleRecent = it; reloadForFilters() + ToggleBar.Toggle("Today", fragment._toggleRecent) { view, active -> + fragment._toggleRecent = active; reloadForFilters() } .withTag("today") else null), (if (_togglesConfig.contains("watched")) - ToggleBar.Toggle("Unwatched", fragment._toggleWatched) { - fragment._toggleWatched = it; reloadForFilters() + ToggleBar.Toggle("Unwatched", fragment._toggleWatched) { view, active -> + fragment._toggleWatched = active; reloadForFilters() } .withTag("watched") else null), ).filterNotNull() + buttonsPlugins) .sortedBy { _togglesConfig.indexOf(it.tag ?: "") } ?: listOf() - val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, { + val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, { view, active -> showOrderOverlay(_overlayContainer, "Visible home filters", listOf( diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index b4f51b14..83e39a88 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -18,6 +18,7 @@ import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.exceptions.RateLimitException +import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment.SubscriptionsFeedView.FeedFilterSettings import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.SearchType import com.futo.platformplayer.models.SubscriptionGroup @@ -56,6 +57,9 @@ class SubscriptionsFeedFragment : MainFragment() { private var _group: SubscriptionGroup? = null; private var _cachedRecyclerData: FeedView.RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null; + private val _filterLock = Object(); + private val _filterSettings = FragmentedStorage.get("subFeedFilter"); + override fun onShownWithView(parameter: Any?, isBack: Boolean) { super.onShownWithView(parameter, isBack); _view?.onShown(); @@ -184,8 +188,6 @@ class SubscriptionsFeedFragment : MainFragment() { return Json.encodeToString(this); } } - private val _filterLock = Object(); - private val _filterSettings = FragmentedStorage.get("subFeedFilter"); private var _bypassRateLimit = false; private val _lastExceptions: List? = null; @@ -284,13 +286,18 @@ class SubscriptionsFeedFragment : MainFragment() { fragment.navigate(g); }; - synchronized(_filterLock) { + synchronized(fragment._filterLock) { _subscriptionBar?.setToggles( - SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); }, - SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); }, - SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); }, - SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); }, - SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); } + SubscriptionBar.Toggle(context.getString(R.string.videos), fragment._filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { view, active -> + toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), active); }, + SubscriptionBar.Toggle(context.getString(R.string.posts), fragment._filterSettings.allowContentTypes.contains(ContentType.POST)) { view, active -> + toggleFilterContentType(ContentType.POST, active); }, + SubscriptionBar.Toggle(context.getString(R.string.live), fragment._filterSettings.allowLive) { view, active -> + fragment._filterSettings.allowLive = active; fragment._filterSettings.save(); loadResults(false); }, + SubscriptionBar.Toggle(context.getString(R.string.planned), fragment._filterSettings.allowPlanned) { view, active -> + fragment._filterSettings.allowPlanned = active; fragment._filterSettings.save(); loadResults(false); }, + SubscriptionBar.Toggle(context.getString(R.string.watched), fragment._filterSettings.allowWatched) { view, active -> + fragment._filterSettings.allowWatched = active; fragment._filterSettings.save(); loadResults(false); } ); } @@ -301,13 +308,13 @@ class SubscriptionsFeedFragment : MainFragment() { toggleFilterContentType(contentType, isTrue); } private fun toggleFilterContentType(contentType: ContentType, isTrue: Boolean) { - synchronized(_filterLock) { + synchronized(fragment._filterLock) { if(!isTrue) { - _filterSettings.allowContentTypes.remove(contentType); - } else if(!_filterSettings.allowContentTypes.contains(contentType)) { - _filterSettings.allowContentTypes.add(contentType) + fragment._filterSettings.allowContentTypes.remove(contentType); + } else if(!fragment._filterSettings.allowContentTypes.contains(contentType)) { + fragment._filterSettings.allowContentTypes.add(contentType) } - _filterSettings.save(); + fragment._filterSettings.save(); }; if(Settings.instance.subscriptions.fetchOnTabOpen) { //TODO: Do this different, temporary workaround loadResults(false); @@ -320,9 +327,9 @@ class SubscriptionsFeedFragment : MainFragment() { val nowSoon = OffsetDateTime.now().plusMinutes(5); val filterGroup = subGroup; return results.filter { - val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType); + val allowedContentType = fragment._filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType); - if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration)) + if(it is IPlatformVideo && it.duration > 0 && !fragment._filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration)) return@filter false; //TODO: Check against a sub cache @@ -331,11 +338,11 @@ class SubscriptionsFeedFragment : MainFragment() { if(it.datetime?.isAfter(nowSoon) == true) { - if(!_filterSettings.allowPlanned) + if(!fragment._filterSettings.allowPlanned) return@filter false; } - if(_filterSettings.allowLive) { //If allowLive, always show live + if(fragment._filterSettings.allowLive) { //If allowLive, always show live if(it is IPlatformVideo && it.isLive) return@filter true; } diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt index 1a1d503b..b72e840c 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -15,12 +15,14 @@ import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.api.media.structures.PlatformContentPager +import com.futo.platformplayer.debug.Stopwatch import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCriticalException import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.findNonRuntimeException import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment +import com.futo.platformplayer.getNowDiffMiliseconds import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.states.StateApp @@ -32,6 +34,8 @@ import com.futo.platformplayer.subsexchange.ChannelRequest import com.futo.platformplayer.subsexchange.ChannelResolve import com.futo.platformplayer.subsexchange.ExchangeContract import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.time.OffsetDateTime import java.util.concurrent.ExecutionException import java.util.concurrent.ForkJoinPool @@ -149,42 +153,56 @@ abstract class SubscriptionsTaskFetchAlgorithm( //Resolve Subscription Exchange if(contract != null) { - try { - resolveTime = measureTimeMillis { - val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract!!.required.contains(it.task.url) }.map { - ChannelResolve( - it.task.url, - it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) } - ) - }.toTypedArray() - val resolve = subsExchangeClient?.resolveContract( - contract!!, - *resolves - ); - if (resolve != null) { - resolveCount = resolves.size; - UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}") - for(result in resolve){ - val task = providedTasks?.find { it.url == result.channelUrl }; - if(task != null) { - taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null)); - providedTasks?.remove(task); + fun resolve() { + try { + resolveTime = measureTimeMillis { + val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract!!.required.contains(it.task.url) }.map { + ChannelResolve( + it.task.url, + it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) } + ) + }.toTypedArray() + + val resolveRequestStart = OffsetDateTime.now(); + + val resolve = subsExchangeClient?.resolveContract( + contract!!, + *resolves + ); + + Logger.i(TAG, "Subscription Exchange contract resolved request in ${resolveRequestStart.getNowDiffMiliseconds()}ms"); + + if (resolve != null) { + resolveCount = resolves.size; + UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}") + for(result in resolve){ + val task = providedTasks?.find { it.url == result.channelUrl }; + if(task != null) { + taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null)); + providedTasks?.remove(task); + } + } + } + if (providedTasks != null) { + for(task in providedTasks!!) { + taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange"))); } } } - if (providedTasks != null) { - for(task in providedTasks!!) { - taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange"))); - } - } - } - Logger.i(TAG, "Subscription Exchange contract resolved in ${resolveTime}ms"); + Logger.i(TAG, "Subscription Exchange contract resolved in ${resolveTime}ms"); + } + catch(ex: Throwable) { + //TODO: fetch remainder after all? + Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex); + } } - catch(ex: Throwable) { - //TODO: fetch remainder after all? - Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex); - } + if(providedTasks?.size ?: 0 == 0) + scope.launch(Dispatchers.IO) { + resolve(); + } + else + resolve(); } } diff --git a/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt b/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt index 6f38014d..b0357f56 100644 --- a/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt +++ b/app/src/main/java/com/futo/platformplayer/subsexchange/SubsExchangeClient.kt @@ -1,10 +1,14 @@ import com.futo.platformplayer.api.media.Serializer +import com.futo.platformplayer.getNowDiffMiliseconds import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm.Companion.TAG import com.futo.platformplayer.subsexchange.ChannelRequest import com.futo.platformplayer.subsexchange.ChannelResolve import com.futo.platformplayer.subsexchange.ChannelResult import com.futo.platformplayer.subsexchange.ExchangeContract import com.futo.platformplayer.subsexchange.ExchangeContractResolve +import com.futo.platformplayer.toGzip +import com.futo.platformplayer.toHumanBytesSize import kotlinx.serialization.* import kotlinx.serialization.json.* import kotlinx.coroutines.Dispatchers @@ -26,6 +30,7 @@ import java.nio.charset.StandardCharsets import java.security.KeyPairGenerator import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.RSAPublicKeySpec +import java.time.OffsetDateTime class SubsExchangeClient(private val server: String, private val privateKey: String, private val contractTimeout: Int = 1000) { @@ -40,24 +45,27 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str // Endpoint: Contract fun requestContract(vararg channels: ChannelRequest): ExchangeContract { - val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json", contractTimeout) + val data = post("/api/Channel/Contract", Json.encodeToString(channels).toByteArray(Charsets.UTF_8), "application/json", contractTimeout) return Json.decodeFromString(data) } suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract { - val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels), "application/json") + val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels).toByteArray(Charsets.UTF_8), "application/json") return Json.decodeFromString(data) } // Endpoint: Resolve fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array { val contractResolve = convertResolves(*resolves) - val result = post("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json") - Logger.v("SubsExchangeClient", "Resolve:" + result); + val contractResolveJson = Serializer.json.encodeToString(contractResolve); + val contractResolveTimeStart = OffsetDateTime.now(); + val result = post("/api/Channel/Resolve?contractId=${contract.id}", contractResolveJson.toByteArray(Charsets.UTF_8), "application/json", 0, true) + val contractResolveTime = contractResolveTimeStart.getNowDiffMiliseconds(); + Logger.v("SubsExchangeClient", "Subscription Exchange Resolve Request [${contractResolveTime}ms]:" + result); return Serializer.json.decodeFromString(result) } suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array { val contractResolve = convertResolves(*resolves) - val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json") + val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve).toByteArray(Charsets.UTF_8), "application/json", true) return Serializer.json.decodeFromString(result) } @@ -74,7 +82,7 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str } // IO methods - private fun post(query: String, body: String, contentType: String, timeout: Int = 0): String { + private fun post(query: String, body: ByteArray, contentType: String, timeout: Int = 0, gzip: Boolean = false): String { val url = URL("${server.trim('/')}$query") with(url.openConnection() as HttpURLConnection) { if(timeout > 0) @@ -82,7 +90,16 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str requestMethod = "POST" setRequestProperty("Content-Type", contentType) doOutput = true - OutputStreamWriter(outputStream, StandardCharsets.UTF_8).use { it.write(body); it.flush() } + + + if(gzip) { + val gzipData = body.toGzip(); + setRequestProperty("Content-Encoding", "gzip"); + outputStream.write(gzipData); + Logger.i("SubsExchangeClient", "SubsExchange using gzip (${body.size.toHumanBytesSize()} => ${gzipData.size.toHumanBytesSize()}"); + } + else + outputStream.write(body); val status = responseCode; Logger.i("SubsExchangeClient", "POST [${url}]: ${status}"); @@ -105,9 +122,9 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str } } } - private suspend fun postAsync(query: String, body: String, contentType: String): String { + private suspend fun postAsync(query: String, body: ByteArray, contentType: String, gzip: Boolean = false): String { return withContext(Dispatchers.IO) { - post(query, body, contentType) + post(query, body, contentType, 0, gzip) } } diff --git a/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt b/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt index ef2eadbc..4a545a26 100644 --- a/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt +++ b/app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt @@ -53,7 +53,7 @@ class ToggleBar : LinearLayout { this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton); else this.setInfo(button.name, button.isActive, button.isButton); - this.onClick.subscribe { button.action(it); }; + this.onClick.subscribe({ view, enabled -> button.action(view, enabled); }); }); } } @@ -62,27 +62,27 @@ class ToggleBar : LinearLayout { val name: String; val icon: Int; val iconVariable: ImageVariable?; - val action: (Boolean)->Unit; + val action: (ToggleTagView, Boolean)->Unit; val isActive: Boolean; var isButton: Boolean = false private set; var tag: String? = null; - constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (Boolean)->Unit) { + constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) { this.name = name; this.icon = 0; this.iconVariable = icon; this.action = action; this.isActive = isActive; } - constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) { + constructor(name: String, icon: Int, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) { this.name = name; this.icon = icon; this.iconVariable = null; this.action = action; this.isActive = isActive; } - constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) { + constructor(name: String, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) { this.name = name; this.icon = 0; this.iconVariable = null; diff --git a/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt b/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt index 059405ad..3ba65413 100644 --- a/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/others/ToggleTagView.kt @@ -12,8 +12,10 @@ import android.widget.TextView import com.bumptech.glide.Glide import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.images.GlideHelper import com.futo.platformplayer.models.ImageVariable +import com.futo.platformplayer.views.ToggleBar class ToggleTagView : LinearLayout { private val _root: FrameLayout; @@ -26,7 +28,7 @@ class ToggleTagView : LinearLayout { var isButton: Boolean = false private set; - var onClick = Event1(); + var onClick = Event2(); constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true); @@ -36,7 +38,7 @@ class ToggleTagView : LinearLayout { _root.setOnClickListener { if(!isButton) setToggle(!isActive); - onClick.emit(isActive); + onClick.emit(this, isActive); } } @@ -52,6 +54,24 @@ class ToggleTagView : LinearLayout { } } + fun setInfo(toggle: ToggleBar.Toggle){ + _text = toggle.name; + _textTag.text = toggle.name; + setToggle(toggle.isActive); + if(toggle.iconVariable != null) { + toggle.iconVariable.setImageView(_image, R.drawable.ic_error_pred); + _image.visibility = View.GONE; + } + else if(toggle.icon > 0) { + _image.setImageResource(toggle.icon); + _image.visibility = View.GONE; + } + else + _image.visibility = View.VISIBLE; + _textTag.visibility = if(!toggle.name.isNullOrEmpty()) View.VISIBLE else View.GONE; + this.isButton = isButton; + } + fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false) { _text = text; _textTag.text = text; diff --git a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt index 97eecac4..5cd2ad85 100644 --- a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt +++ b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt @@ -158,7 +158,7 @@ class SubscriptionBar : LinearLayout { for(button in buttons) { _tagsContainer.addView(ToggleTagView(context).apply { this.setInfo(button.name, button.isActive); - this.onClick.subscribe { button.action(it); }; + this.onClick.subscribe({ view, value -> button.action(view, value); }); }); } } @@ -166,16 +166,16 @@ class SubscriptionBar : LinearLayout { class Toggle { val name: String; val icon: Int; - val action: (Boolean)->Unit; + val action: (ToggleTagView, Boolean)->Unit; val isActive: Boolean; - constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) { + constructor(name: String, icon: Int, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) { this.name = name; this.icon = icon; this.action = action; this.isActive = isActive; } - constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) { + constructor(name: String, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) { this.name = name; this.icon = 0; this.action = action; From ce2a2f85823ca90ca5e07780b8f74b2fb5c23bf1 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Mon, 7 Apr 2025 23:32:57 +0200 Subject: [PATCH 051/330] submods --- app/src/stable/assets/sources/odysee | 2 +- app/src/stable/assets/sources/soundcloud | 2 +- app/src/stable/assets/sources/spotify | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/odysee | 2 +- app/src/unstable/assets/sources/soundcloud | 2 +- app/src/unstable/assets/sources/spotify | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/stable/assets/sources/odysee b/app/src/stable/assets/sources/odysee index f2f83344..215cd9bd 160000 --- a/app/src/stable/assets/sources/odysee +++ b/app/src/stable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit f2f83344ebc905b36c0689bfef407bb95e6d9af0 +Subproject commit 215cd9bd70d3cc68e25441f7696dcbe5beee2709 diff --git a/app/src/stable/assets/sources/soundcloud b/app/src/stable/assets/sources/soundcloud index bff981c3..f8234d6a 160000 --- a/app/src/stable/assets/sources/soundcloud +++ b/app/src/stable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit bff981c3ce7abad363e79705214a4710fb347f7d +Subproject commit f8234d6af8573414d07fd364bc136aa67ad0e379 diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify index 331dd929..b61095ec 160000 --- a/app/src/stable/assets/sources/spotify +++ b/app/src/stable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 331dd929293614875af80e3ab4cb162dc6183410 +Subproject commit b61095ec200284a686edb8f3b2a595599ad8b5ed diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index ae7b62f4..6f1266a0 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit ae7b62f4d85cf398d9566a6ed07a6afc193f6b48 +Subproject commit 6f1266a038d11998fef429ae0eac0798b3280d75 diff --git a/app/src/unstable/assets/sources/odysee b/app/src/unstable/assets/sources/odysee index f2f83344..215cd9bd 160000 --- a/app/src/unstable/assets/sources/odysee +++ b/app/src/unstable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit f2f83344ebc905b36c0689bfef407bb95e6d9af0 +Subproject commit 215cd9bd70d3cc68e25441f7696dcbe5beee2709 diff --git a/app/src/unstable/assets/sources/soundcloud b/app/src/unstable/assets/sources/soundcloud index bff981c3..f8234d6a 160000 --- a/app/src/unstable/assets/sources/soundcloud +++ b/app/src/unstable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit bff981c3ce7abad363e79705214a4710fb347f7d +Subproject commit f8234d6af8573414d07fd364bc136aa67ad0e379 diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify index 331dd929..b61095ec 160000 --- a/app/src/unstable/assets/sources/spotify +++ b/app/src/unstable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 331dd929293614875af80e3ab4cb162dc6183410 +Subproject commit b61095ec200284a686edb8f3b2a595599ad8b5ed diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index ae7b62f4..6f1266a0 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit ae7b62f4d85cf398d9566a6ed07a6afc193f6b48 +Subproject commit 6f1266a038d11998fef429ae0eac0798b3280d75 From 869b1fc15e4ab3665b9951f6460194e74ab2f372 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Tue, 8 Apr 2025 00:34:52 +0200 Subject: [PATCH 052/330] Fix pager for landscape --- .../platformplayer/fragment/mainactivity/main/FeedView.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt index 17ca5510..3c915ebe 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt @@ -197,10 +197,12 @@ abstract class FeedView : L val firstVisibleItemView = if(firstVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(firstVisibleItemPosition) else null; val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition(); val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null; + val rows = if(recyclerData.layoutManager is GridLayoutManager) Math.max(1, recyclerData.results.size / recyclerData.layoutManager.spanCount) else 1; + val rowsHeight = (firstVisibleItemView?.height ?: 0) * rows; if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) { false; } - else if (firstVisibleItemView != null && height != null && firstVisibleItemView.height * recyclerData.results.size < height) { + else if (firstVisibleItemView != null && height != null && rowsHeight < height) { false; } else { true; From 1755d03a6bd402ba9980e4683ee0899e60be0b4a Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Wed, 9 Apr 2025 00:56:49 +0200 Subject: [PATCH 053/330] Fcast clearer connection/reconnection overlay, disable ipv6 by default --- .../futo/platformplayer/Extensions_Network.kt | 7 +++- .../java/com/futo/platformplayer/Settings.kt | 7 +++- .../java/com/futo/platformplayer/UIDialogs.kt | 11 ++++- .../casting/FCastCastingDevice.kt | 2 + .../platformplayer/casting/StateCasting.kt | 40 +++++++++++++++++-- .../dialogs/ConnectCastingDialog.kt | 4 +- app/src/main/res/values/strings.xml | 2 + 7 files changed, 64 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt index 52d8a663..00f47885 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt @@ -216,9 +216,14 @@ private fun ByteArray.toInetAddress(): InetAddress { return InetAddress.getByAddress(this); } -fun getConnectedSocket(addresses: List, port: Int): Socket? { +fun getConnectedSocket(attemptAddresses: List, port: Int): Socket? { val timeout = 2000 + + val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance() else attemptAddresses; + if(addresses.isEmpty()) + throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})"); + if (addresses.isEmpty()) { return null; } diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index cedc4316..2bd95905 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -583,10 +583,15 @@ class Settings : FragmentedStorageFileJson() { @Serializable(with = FlexibleBooleanSerializer::class) var keepScreenOn: Boolean = true; - @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1) + @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3) @Serializable(with = FlexibleBooleanSerializer::class) var alwaysProxyRequests: Boolean = false; + + @FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4) + @Serializable(with = FlexibleBooleanSerializer::class) + var allowIpv6: Boolean = false; + /*TODO: Should we have a different casting quality? @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @DropdownFieldOptionsId(R.array.preferred_quality_array) diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt index e84002da..8034854d 100644 --- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt +++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt @@ -5,6 +5,7 @@ import android.app.AlertDialog import android.content.Context import android.content.Intent import android.graphics.Color +import android.graphics.drawable.Animatable import android.net.Uri import android.text.Layout import android.text.method.ScrollingMovementMethod @@ -199,16 +200,21 @@ class UIDialogs { dialog.show(); } - fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) { + fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog { + return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions); + } + fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog { val builder = AlertDialog.Builder(context); val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null); builder.setView(view); - + builder.setCancelable(defaultCloseAction > -2); val dialog = builder.create(); registerDialogOpened(dialog); view.findViewById(R.id.dialog_icon).apply { this.setImageResource(icon); + if(animated) + this.drawable.assume { it.start() }; } view.findViewById(R.id.dialog_text).apply { this.text = text; @@ -275,6 +281,7 @@ class UIDialogs { registerDialogClosed(dialog); } dialog.show(); + return dialog; } fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) { diff --git a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt index 9e12f78c..85b928c2 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.casting import android.os.Looper import android.util.Base64 import android.util.Log +import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.casting.models.FCastDecryptedMessage import com.futo.platformplayer.casting.models.FCastEncryptedMessage @@ -32,6 +33,7 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.math.BigInteger +import java.net.Inet4Address import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index e329a495..90177050 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -1,5 +1,6 @@ package com.futo.platformplayer.casting +import android.app.AlertDialog import android.content.ContentResolver import android.content.Context import android.net.Uri @@ -9,6 +10,7 @@ import android.util.Log import android.util.Xml import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi +import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.http.ManagedHttpClient @@ -239,6 +241,9 @@ class StateCasting { Logger.i(TAG, "CastingService stopped.") } + private val _castingDialogLock = Any(); + private var _currentDialog: AlertDialog? = null; + @Synchronized fun connectDevice(device: CastingDevice) { if (activeDevice == device) @@ -272,10 +277,39 @@ class StateCasting { invokeInMainScopeIfRequired { StateApp.withContext(false) { context -> context.let { + Logger.i(TAG, "Casting state changed to ${castConnectionState}"); when (castConnectionState) { - CastConnectionState.CONNECTED -> UIDialogs.toast(it, "Connected to device") - CastConnectionState.CONNECTING -> UIDialogs.toast(it, "Connecting to device...") - CastConnectionState.DISCONNECTED -> UIDialogs.toast(it, "Disconnected from device") + CastConnectionState.CONNECTED -> { + Logger.i(TAG, "Casting connected to [${device.name}]"); + UIDialogs.appToast("Connected to device") + synchronized(_castingDialogLock) { + if(_currentDialog != null) { + _currentDialog?.hide(); + _currentDialog = null; + } + } + } + CastConnectionState.CONNECTING -> { + Logger.i(TAG, "Casting connecting to [${device.name}]"); + UIDialogs.toast(it, "Connecting to device...") + synchronized(_castingDialogLock) { + if(_currentDialog == null) { + _currentDialog = UIDialogs.showDialog(context, R.drawable.ic_loader_animated, true, "Connecting to [${device.name}]", "Make sure you are on the same network", null, -2, + UIDialogs.Action("Disconnect", { + device.stop(); + })); + } + } + } + CastConnectionState.DISCONNECTED -> { + UIDialogs.toast(it, "Disconnected from device") + synchronized(_castingDialogLock) { + if(_currentDialog != null) { + _currentDialog?.hide(); + _currentDialog = null; + } + } + } } } }; diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt index bd5da2ea..8f3b836c 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt @@ -73,11 +73,11 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) { }; _rememberedAdapter.onConnect.subscribe { _ -> dismiss() - UIDialogs.showCastingDialog(context) + //UIDialogs.showCastingDialog(context) } _adapter.onConnect.subscribe { _ -> dismiss() - UIDialogs.showCastingDialog(context) + //UIDialogs.showCastingDialog(context) } _recyclerRememberedDevices.adapter = _rememberedAdapter; _recyclerRememberedDevices.layoutManager = LinearLayoutManager(context); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af450660..e7c101c9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -72,6 +72,8 @@ Keep screen on while casting Always proxy requests Always proxy requests when casting data through the device. + Allow IPV6 + If casting over IPV6 is allowed, can cause issues on some networks Discover Find new video sources to add These sources have been disabled From a1c2d19daf1a6e859598421e27464e4e98388d4f Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 8 Apr 2025 17:58:31 -0500 Subject: [PATCH 054/330] refactor shorts code Changelog: changed --- .../fragment/mainactivity/main/ShortView.kt | 599 +++++++---------- .../mainactivity/main/ShortsFragment.kt | 32 +- .../views/overlays/WebviewOverlay.kt | 4 +- .../views/video/FutoShortPlayer.kt | 72 +- .../views/video/FutoVideoPlayerBase.kt | 2 +- app/src/main/res/layout/fragment_shorts.xml | 4 +- app/src/main/res/layout/modal_comments.xml | 619 ++++++++---------- app/src/main/res/layout/view_short.xml | 6 +- app/src/main/res/layout/view_short_player.xml | 6 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 2 +- 11 files changed, 545 insertions(+), 802 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt index 2a9f954f..1bef3548 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -12,11 +12,9 @@ import android.os.Bundle import android.text.Spanned import android.util.AttributeSet import android.util.TypedValue -import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.SoundEffectConstants import android.view.View -import android.view.ViewGroup import android.widget.Button import android.widget.FrameLayout import android.widget.ImageView @@ -29,16 +27,13 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -51,7 +46,6 @@ import androidx.compose.material.icons.filled.ThumbDownOffAlt import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material.icons.filled.ThumbUpOffAlt import androidx.compose.material.icons.outlined.Share -import androidx.compose.material.icons.outlined.ThumbUp import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.ripple.RippleAlpha @@ -74,7 +68,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.platform.ComposeView @@ -101,6 +94,7 @@ import com.bumptech.glide.request.transition.Transition import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink @@ -120,11 +114,10 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig -import com.futo.platformplayer.casting.CastConnectionState -import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event3 +import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.dp import com.futo.platformplayer.engine.exceptions.ScriptAgeException @@ -173,7 +166,6 @@ import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.google.android.material.theme.overlay.MaterialThemeOverlay import com.google.protobuf.ByteString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -184,7 +176,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import userpackage.Protocol import java.time.OffsetDateTime -import kotlin.contracts.Effect import kotlin.coroutines.cancellation.CancellationException @OptIn(ExperimentalMaterial3Api::class) @@ -207,8 +198,6 @@ class ShortView : ConstraintLayout { private var playWhenReady = false -// private var playerAttached = false - private var _lastVideoSource: IVideoSource? = null private var _lastAudioSource: IAudioSource? = null private var _lastSubtitleSource: ISubtitleSource? = null @@ -285,54 +274,55 @@ class ShortView : ConstraintLayout { setupComposeView() } + // TODO merge this with the updateQualitySourcesOverlay for the normal video player @androidx.annotation.OptIn(UnstableApi::class) private fun updateQualitySourcesOverlay(videoDetails: IPlatformVideoDetails?, videoLocal: VideoLocal? = null, liveStreamVideoFormats: List? = null, liveStreamAudioFormats: List? = null) { - Logger.i(TAG, "updateQualitySourcesOverlay"); + Logger.i(TAG, "updateQualitySourcesOverlay") - val video: IPlatformVideoDetails?; - val localVideoSources: List?; - val localAudioSource: List?; - val localSubtitleSources: List?; + val video: IPlatformVideoDetails? + val localVideoSources: List? + val localAudioSource: List? + val localSubtitleSources: List? - val videoSources: List?; - val audioSources: List?; + val videoSources: List? + val audioSources: List? if (videoDetails is VideoLocal) { - video = videoLocal?.videoSerialized; - localVideoSources = videoDetails.videoSource.toList(); - localAudioSource = videoDetails.audioSource.toList(); - localSubtitleSources = videoDetails.subtitlesSources.toList(); + video = videoLocal?.videoSerialized + localVideoSources = videoDetails.videoSource.toList() + localAudioSource = videoDetails.audioSource.toList() + localSubtitleSources = videoDetails.subtitlesSources.toList() videoSources = null - audioSources = null; + audioSources = null } else { - video = videoDetails; - videoSources = video?.video?.videoSources?.toList(); + video = videoDetails + videoSources = video?.video?.videoSources?.toList() audioSources = if (video?.video?.isUnMuxed == true) (video.video as VideoUnMuxedSourceDescriptor).audioSources.toList() else null if (videoLocal != null) { - localVideoSources = videoLocal.videoSource.toList(); - localAudioSource = videoLocal.audioSource.toList(); - localSubtitleSources = videoLocal.subtitlesSources.toList(); + localVideoSources = videoLocal.videoSource.toList() + localAudioSource = videoLocal.audioSource.toList() + localSubtitleSources = videoLocal.subtitlesSources.toList() } else { - localVideoSources = null; - localAudioSource = null; - localSubtitleSources = null; + localVideoSources = null + localAudioSource = null + localSubtitleSources = null } } - val doDedup = Settings.instance.playback.simplifySources; + val doDedup = Settings.instance.playback.simplifySources val bestVideoSources = if (doDedup) (videoSources?.map { it.height * it.width }?.distinct() ?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) } ?.plus(videoSources.filter { it is IHLSManifestSource || it is IDashManifestSource }))?.distinct() ?.filterNotNull()?.toList() ?: listOf() else videoSources?.toList() ?: listOf() val bestAudioContainer = - audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container }; + audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container } val bestAudioSources = if (doDedup) audioSources?.filter { it.container == bestAudioContainer } ?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource }) - ?.distinct()?.toList() ?: listOf() else audioSources?.toList() ?: listOf(); + ?.distinct()?.toList() ?: listOf() else audioSources?.toList() ?: listOf() val canSetSpeed = true val currentPlaybackRate = player.getPlaybackRate() @@ -340,117 +330,94 @@ class ShortView : ConstraintLayout { SlideUpMenuOverlay(this.context, overlayQualityContainer, context.getString( R.string.quality ), null, true, if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null, if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply { - setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate!!.toString()); + setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate.toString()) onClick.subscribe { v -> - player.setPlaybackRate(v.toFloat()); - setSelected(v); + player.setPlaybackRate(v.toFloat()) + setSelected(v) - }; - } else null, - - if (localVideoSources?.isNotEmpty() == true) SlideUpMenuGroup( - this.context, context.getString(R.string.offline_video), "video", *localVideoSources.map { - SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", tag = it, call = { handleSelectVideoTrack(it) }); - }.toList().toTypedArray() - ) - else null, if (localAudioSource?.isNotEmpty() == true) SlideUpMenuGroup( - this.context, context.getString(R.string.offline_audio), "audio", *localAudioSource.map { - SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), tag = it, call = { handleSelectAudioTrack(it) }); - }.toList().toTypedArray() - ) - else null, if (localSubtitleSources?.isNotEmpty() == true) SlideUpMenuGroup( - this.context, context.getString(R.string.offline_subtitles), "subtitles", *localSubtitleSources.map { - SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) }) - }.toList().toTypedArray() - ) - else null, if (liveStreamVideoFormats?.isEmpty() == false) SlideUpMenuGroup( - this.context, context.getString(R.string.stream_video), "video", (listOf( - SlideUpMenuItem(this.context, R.drawable.ic_movie, "Auto", tag = "auto", call = { player.selectVideoTrack(-1) }) - ) + (liveStreamVideoFormats.map { - SlideUpMenuItem(this.context, R.drawable.ic_movie, it.label - ?: it.containerMimeType - ?: it.bitrate.toString(), "${it.width}x${it.height}", tag = it, call = { player.selectVideoTrack(it.height) }); - })) - ) - else null, if (liveStreamAudioFormats?.isEmpty() == false) SlideUpMenuGroup( - this.context, context.getString(R.string.stream_audio), "audio", *liveStreamAudioFormats.map { - SlideUpMenuItem(this.context, R.drawable.ic_music, "${it.label ?: it.containerMimeType} ${it.bitrate}", "", tag = it, call = { player.selectAudioTrack(it.bitrate) }); - }.toList().toTypedArray() - ) - else null, - - if (bestVideoSources.isNotEmpty()) SlideUpMenuGroup( - this.context, context.getString(R.string.video), "video", *bestVideoSources.map { - val estSize = VideoHelper.estimateSourceSize(it); - val prefix = - if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; - SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectVideoTrack(it) }); - }.toList().toTypedArray() - ) - else null, if (bestAudioSources.isNotEmpty()) SlideUpMenuGroup( - this.context, context.getString(R.string.audio), "audio", *bestAudioSources.map { - val estSize = VideoHelper.estimateSourceSize(it); - val prefix = - if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; - SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectAudioTrack(it) }); - }.toList().toTypedArray() - ) - else null, if (video?.subtitles?.isNotEmpty() == true) SlideUpMenuGroup( - this.context, context.getString(R.string.subtitles), "subtitles", *video.subtitles.map { - SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) }) - }.toList().toTypedArray() - ) - else null - ); + } + } else null, if (localVideoSources?.isNotEmpty() == true) SlideUpMenuGroup( + this.context, context.getString(R.string.offline_video), "video", *localVideoSources.map { + SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", tag = it, call = { handleSelectVideoTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (localAudioSource?.isNotEmpty() == true) SlideUpMenuGroup( + this.context, context.getString(R.string.offline_audio), "audio", *localAudioSource.map { + SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), tag = it, call = { handleSelectAudioTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (localSubtitleSources?.isNotEmpty() == true) SlideUpMenuGroup( + this.context, context.getString(R.string.offline_subtitles), "subtitles", *localSubtitleSources.map { + SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (liveStreamVideoFormats?.isEmpty() == false) SlideUpMenuGroup( + this.context, context.getString(R.string.stream_video), "video", (listOf( + SlideUpMenuItem(this.context, R.drawable.ic_movie, "Auto", tag = "auto", call = { player.selectVideoTrack(-1) }) + ) + (liveStreamVideoFormats.map { + SlideUpMenuItem(this.context, R.drawable.ic_movie, it.label + ?: it.containerMimeType + ?: it.bitrate.toString(), "${it.width}x${it.height}", tag = it, call = { player.selectVideoTrack(it.height) }) + })) + ) + else null, if (liveStreamAudioFormats?.isEmpty() == false) SlideUpMenuGroup( + this.context, context.getString(R.string.stream_audio), "audio", *liveStreamAudioFormats.map { + SlideUpMenuItem(this.context, R.drawable.ic_music, "${it.label ?: it.containerMimeType} ${it.bitrate}", "", tag = it, call = { player.selectAudioTrack(it.bitrate) }) + }.toList().toTypedArray() + ) + else null, if (bestVideoSources.isNotEmpty()) SlideUpMenuGroup( + this.context, context.getString(R.string.video), "video", *bestVideoSources.map { + val estSize = VideoHelper.estimateSourceSize(it) + val prefix = if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "" + SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectVideoTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (bestAudioSources.isNotEmpty()) SlideUpMenuGroup( + this.context, context.getString(R.string.audio), "audio", *bestAudioSources.map { + val estSize = VideoHelper.estimateSourceSize(it) + val prefix = if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "" + SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectAudioTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (video?.subtitles?.isNotEmpty() == true) SlideUpMenuGroup( + this.context, context.getString(R.string.subtitles), "subtitles", *video.subtitles.map { + SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) }) + }.toList().toTypedArray() + ) + else null + ) } private fun handleSelectVideoTrack(videoSource: IVideoSource) { Logger.i(TAG, "handleSelectAudioTrack(videoSource=$videoSource)") - val video = videoDetails ?: return; + if (_lastVideoSource == videoSource) return - if (_lastVideoSource == videoSource) return; - - val d = StateCasting.instance.activeDevice; - if (d != null && d.connectionState == CastConnectionState.CONNECTED) StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); - else if (!player.swapSources(videoSource, _lastAudioSource, true, true, true)) player.hideControls(false); //TODO: Disable player? - - _lastVideoSource = videoSource; + _lastVideoSource = videoSource } private fun handleSelectAudioTrack(audioSource: IAudioSource) { Logger.i(TAG, "handleSelectAudioTrack(audioSource=$audioSource)") - val video = videoDetails ?: return; + if (_lastAudioSource == audioSource) return - if (_lastAudioSource == audioSource) return; - - val d = StateCasting.instance.activeDevice; - if (d != null && d.connectionState == CastConnectionState.CONNECTED) StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); - else (!player.swapSources(_lastVideoSource, audioSource, true, true, true)) - player.hideControls(false); //TODO: Disable player? - - _lastAudioSource = audioSource; + _lastAudioSource = audioSource } private fun handleSelectSubtitleTrack(subtitleSource: ISubtitleSource) { Logger.i(TAG, "handleSelectSubtitleTrack(subtitleSource=$subtitleSource)") - val video = videoDetails ?: return; - var toSet: ISubtitleSource? = subtitleSource - if (_lastSubtitleSource == subtitleSource) toSet = null; + if (_lastSubtitleSource == subtitleSource) toSet = null - val d = StateCasting.instance.activeDevice; - if (d != null && d.connectionState == CastConnectionState.CONNECTED) StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); - else player.swapSubtitles(mainFragment!!.lifecycleScope, toSet); + player.swapSubtitles(mainFragment.lifecycleScope, toSet) - _lastSubtitleSource = toSet; + _lastSubtitleSource = toSet } private fun showVideoSettings() { Logger.i(TAG, "showVideoSettings") - overlayQualitySelector?.selectOption("video", _lastVideoSource); - overlayQualitySelector?.selectOption("audio", _lastAudioSource); - overlayQualitySelector?.selectOption("subtitles", _lastSubtitleSource); + overlayQualitySelector?.selectOption("video", _lastVideoSource) + overlayQualitySelector?.selectOption("audio", _lastAudioSource) + overlayQualitySelector?.selectOption("subtitles", _lastSubtitleSource) if (_lastVideoSource is IDashManifestSource || _lastVideoSource is IHLSManifestSource) { @@ -484,19 +451,18 @@ class ShortView : ConstraintLayout { } } - val currentPlaybackRate = player.getPlaybackRate() ?: 1.0 + val currentPlaybackRate = player.getPlaybackRate() overlayQualitySelector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" } ?.let { (it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString()) - }; + } - overlayQualitySelector?.show(); -// _slideUpOverlay = overlayQualitySelector; + overlayQualitySelector?.show() } @OptIn(ExperimentalGlideComposeApi::class) private fun setupComposeView() { - val composeView: ComposeView = findViewById(R.id.compose_view_test_button) + val composeView: ComposeView = findViewById(R.id.shorts_overlay_content_compose_view) composeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { @@ -554,8 +520,7 @@ class ShortView : ConstraintLayout { val tint = Color.White val buttonTextStyle = TextStyle( - fontSize = 12.sp, - shadow = Shadow( + fontSize = 12.sp, shadow = Shadow( color = Color.Black, blurRadius = 3f ) ) @@ -571,27 +536,21 @@ class ShortView : ConstraintLayout { ConstraintLayout(modifier = Modifier.fillMaxSize()) { val (title, buttons) = createRefs() -// val horizontalChain = createHorizontalChain(title, buttons, chainStyle = ChainStyle.SpreadInside) Box(modifier = Modifier.constrainAs(title) { -// top.linkTo(parent.top) bottom.linkTo(parent.bottom, margin = 16.dp) start.linkTo(parent.start, margin = 8.dp) end.linkTo(buttons.start) width = Dimension.fillToConstraints - } -// .fillMaxWidth() - ) { + }) { Column( - modifier = Modifier - .align(Alignment.BottomStart), - verticalArrangement = Arrangement.spacedBy(4.dp) + modifier = Modifier.align(Alignment.BottomStart), verticalArrangement = Arrangement.spacedBy(4.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.clickable(onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) - mainFragment!!.navigate(currentVideo?.author) + mainFragment.navigate(currentVideo?.author) }), ) { @@ -601,27 +560,23 @@ class ShortView : ConstraintLayout { .clip(CircleShape) ) Text( - currentVideo?.author?.name ?: "", - color = tint, - fontSize = 14.sp + currentVideo?.author?.name + ?: "", color = tint, fontSize = 14.sp ) } Text( currentVideo?.name - ?: "", color = tint, maxLines = 1, overflow = TextOverflow.Ellipsis, - fontSize = 14.sp + ?: "", color = tint, maxLines = 1, overflow = TextOverflow.Ellipsis, fontSize = 14.sp ) } } Box(modifier = Modifier.constrainAs(buttons) { -// top.linkTo(parent.top) bottom.linkTo(parent.bottom, margin = 16.dp) start.linkTo(title.end, margin = 12.dp) end.linkTo(parent.end, margin = 4.dp) marginBottom - }) { CompositionLocalProvider(LocalRippleConfiguration provides rippleConfiguration) { Column( @@ -667,10 +622,7 @@ class ShortView : ConstraintLayout { } Text( likes.toString(), color = tint, - modifier = Modifier - .align(Alignment.BottomCenter) -// .offset(y = buttonOffset) - , + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } @@ -713,30 +665,30 @@ class ShortView : ConstraintLayout { } Text( dislikes.toString(), color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } } Box { IconButton( - modifier = Modifier.padding(bottom = buttonOffset).align(Alignment.TopCenter), + modifier = Modifier + .padding(bottom = buttonOffset) + .align(Alignment.TopCenter), onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) - bottomSheet.show(mainFragment!!.childFragmentManager, CommentsModalBottomSheet.TAG) + if (!bottomSheet.isAdded) { + bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG) + } }, ) { Icon( Icons.AutoMirrored.Outlined.Comment, contentDescription = "View Comments", tint = tint, - ) + ) } Text( - "Comments", color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), - style = buttonTextStyle + "Comments", color = tint, modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle ) } Box { @@ -744,12 +696,13 @@ class ShortView : ConstraintLayout { modifier = Modifier.padding(bottom = buttonOffset), onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) - val url = currentVideo?.shareUrl ?: currentVideo?.url - mainFragment!!.startActivity(Intent.createChooser(Intent().apply { - action = Intent.ACTION_SEND; + val url = + currentVideo?.shareUrl ?: currentVideo?.url + mainFragment.startActivity(Intent.createChooser(Intent().apply { + action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, url) type = "text/plain" - }, null)); + }, null)) }, ) { Icon( @@ -758,8 +711,7 @@ class ShortView : ConstraintLayout { } Text( "Share", color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } @@ -778,8 +730,7 @@ class ShortView : ConstraintLayout { } Text( "Refresh", color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } @@ -797,8 +748,7 @@ class ShortView : ConstraintLayout { } Text( "Quality", color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } @@ -822,9 +772,6 @@ class ShortView : ConstraintLayout { modifier = Modifier .size(94.dp) .background(color = Color.Black.copy(alpha = 0.7f), shape = CircleShape), contentAlignment = Alignment.Center -// .padding(32.dp), // Pad the entire box - - ) { Icon( imageVector = if (isPlaying) Icons.Rounded.PlayArrow @@ -836,7 +783,7 @@ class ShortView : ConstraintLayout { // Auto-hide the icon after a short delay LaunchedEffect(showPlayPauseIcon) { if (showPlayPauseIcon) { - delay(1500) // Icon visible for 1.5 seconds + delay(1500) showPlayPauseIcon = false } } @@ -849,6 +796,7 @@ class ShortView : ConstraintLayout { } } + @Suppress("unused") fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) { this.mainFragment = fragment this.bottomSheet.mainFragment = fragment @@ -864,6 +812,7 @@ class ShortView : ConstraintLayout { loadVideo(video.url) } + @Suppress("unused") fun changeVideo(videoDetails: IPlatformVideoDetails) { if (video?.url == videoDetails.url) { return @@ -874,31 +823,16 @@ class ShortView : ConstraintLayout { } fun play() { -// if (playerAttached){ -// throw Exception() -// } -// playerAttached = true loadLikes(this.video!!) player.attach() -// if (_lastVideoSource != null || _lastAudioSource != null) { -// if (!player.activelyPlaying) { -// player.play() -// } -// } else { playVideo() -// } } - // fun pause() { player.pause() } fun stop() { -// if (!playerAttached) { -// return -// } -// playerAttached = false playWhenReady = false player.clear() @@ -928,7 +862,7 @@ class ShortView : ConstraintLayout { val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null } - mainFragment!!.lifecycleScope.launch(Dispatchers.IO) { + mainFragment.lifecycleScope.launch(Dispatchers.IO) { try { val queryReferencesResponse = ApiMethods.getQueryReferences( ApiMethods.SERVER, ref, null, null, arrayListOf( @@ -941,31 +875,29 @@ class ShortView : ConstraintLayout { ByteString.copyFrom(Opinion.dislike.data) ).build() ), extraByteReferences = listOfNotNull(extraBytesRef) - ); + ) - val likes = queryReferencesResponse.countsList[0]; - val dislikes = queryReferencesResponse.countsList[1]; - val hasLiked = - StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/; - val hasDisliked = - StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/; + val likes = queryReferencesResponse.countsList[0] + val dislikes = queryReferencesResponse.countsList[1] + val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray()) + val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray()) withContext(Dispatchers.Main) { onLikesLoaded.emit(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked) onLikeDislikeUpdated.subscribe(this) { args -> if (args.hasLiked) { - args.processHandle.opinion(ref, Opinion.like); + args.processHandle.opinion(ref, Opinion.like) } else if (args.hasDisliked) { - args.processHandle.opinion(ref, Opinion.dislike); + args.processHandle.opinion(ref, Opinion.dislike) } else { - args.processHandle.opinion(ref, Opinion.neutral); + args.processHandle.opinion(ref, Opinion.neutral) } - mainFragment!!.lifecycleScope.launch(Dispatchers.IO) { + mainFragment.lifecycleScope.launch(Dispatchers.IO) { try { - Logger.i(CommentsModalBottomSheet.Companion.TAG, "Started backfill"); - args.processHandle.fullyBackfillServersAnnounceExceptions(); - Logger.i(CommentsModalBottomSheet.Companion.TAG, "Finished backfill"); + Logger.i(CommentsModalBottomSheet.Companion.TAG, "Started backfill") + args.processHandle.fullyBackfillServersAnnounceExceptions() + Logger.i(CommentsModalBottomSheet.Companion.TAG, "Finished backfill") } catch (e: Throwable) { Logger.e(CommentsModalBottomSheet.Companion.TAG, "Failed to backfill servers", e) } @@ -974,10 +906,10 @@ class ShortView : ConstraintLayout { StatePolycentric.instance.updateLikeMap( ref, args.hasLiked, args.hasDisliked ) - }; + } } } catch (e: Throwable) { - Logger.e(CommentsModalBottomSheet.Companion.TAG, "Failed to get polycentric likes/dislikes.", e); + Logger.e(CommentsModalBottomSheet.Companion.TAG, "Failed to get polycentric likes/dislikes.", e) } } } @@ -1120,7 +1052,7 @@ class ShortView : ConstraintLayout { }) else player.setArtwork(null) player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0) - if (subtitleSource != null) player.swapSubtitles(mainFragment!!.lifecycleScope, subtitleSource) + if (subtitleSource != null) player.swapSubtitles(mainFragment.lifecycleScope, subtitleSource) player.seekTo(resumePositionMs) _lastVideoSource = videoSource @@ -1149,19 +1081,18 @@ class ShortView : ConstraintLayout { private lateinit var containerContentSupport: SupportOverlay private lateinit var title: TextView - private lateinit var subTitle: TextView; + private lateinit var subTitle: TextView private lateinit var channelName: TextView private lateinit var channelMeta: TextView private lateinit var creatorThumbnail: CreatorThumbnail - private lateinit var channelButton: LinearLayout; + private lateinit var channelButton: LinearLayout private lateinit var monetization: MonetizationView - private lateinit var platform: PlatformIndicator; - private lateinit var textLikes: TextView; - private lateinit var textDislikes: TextView; - private lateinit var layoutRating: LinearLayout; - private lateinit var imageDislikeIcon: ImageView; - private lateinit var imageLikeIcon: ImageView; -// private lateinit var buttonSubscribe: SubscribeButton + private lateinit var platform: PlatformIndicator + private lateinit var textLikes: TextView + private lateinit var textDislikes: TextView + private lateinit var layoutRating: LinearLayout + private lateinit var imageDislikeIcon: ImageView + private lateinit var imageLikeIcon: ImageView private lateinit var description: TextView private lateinit var descriptionContainer: LinearLayout @@ -1183,49 +1114,22 @@ class ShortView : ConstraintLayout { private lateinit var behavior: BottomSheetBehavior -// override fun getTheme(): Int { -// return R.style.CustomBottomSheetDialog -// } - -// override fun getTheme(): Int = R.style.CustomBottomSheetTheme - -// override fun onCreateView( -// inflater: LayoutInflater, -// container: ViewGroup?, -// savedInstanceState: Bundle? -// ): View? { -// -//// container. -// -// val bottomSheetDialog = inflater.inflate(R.layout.modal_comments, container, false) + private val _taskLoadPolycentricProfile = + TaskHandler(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }).success { it -> setPolycentricProfile(it, animate = true) } + .exception { + Logger.w(TAG, "Failed to load claims.", it) + } override fun onCreateDialog( savedInstanceState: Bundle?, ): Dialog { - val bottomSheetDialog = BottomSheetDialog( -// mainFragment!!.context!!, -// MaterialThemeOverlay.wrap(requireContext()) -// ContextThemeWrapper(requireContext(), ), -// ContextThemeWrapper( requireContext(), R.style.ThemeOverlay_App_Material3_BottomSheetDialog) - requireContext(),R.style.Custom_BottomSheetDialog_Theme -// MaterialThemeOverlay.wrap(requireContext()) -// ContextThemeWrapper(requireContext(), R.style.BottomSheetDialog_Rounded) -// R.style.ThemeOverlay_App_Material3_BottomSheetDialog -// BottomSheet -// R.style.CustomBottomSheetTheme -// R.style.ThemeOverlay_Cata -// R.style.ThemeOverlay_Catalog_BottomSheetDialog_Scrollable - //com.google.android.material.R.style.Animation_Design_BottomSheetDialog - ) + val bottomSheetDialog = + BottomSheetDialog(requireContext(), R.style.Custom_BottomSheetDialog_Theme) bottomSheetDialog.setContentView(R.layout.modal_comments) -// bottomSheetDialog.behavior. - behavior = bottomSheetDialog.behavior -// val composeView = bottomSheetDialog.findViewById(R.id.compose_view) - - containerContent = bottomSheetDialog.findViewById(R.id.contentContainer)!! + containerContent = bottomSheetDialog.findViewById(R.id.content_container)!! containerContentMain = bottomSheetDialog.findViewById(R.id.videodetail_container_main)!! containerContentReplies = bottomSheetDialog.findViewById(R.id.videodetail_container_replies)!! @@ -1248,8 +1152,6 @@ class ShortView : ConstraintLayout { imageLikeIcon = bottomSheetDialog.findViewById(R.id.image_like_icon)!! imageDislikeIcon = bottomSheetDialog.findViewById(R.id.image_dislike_icon)!! -// buttonSubscribe = bottomSheetDialog.findViewById(R.id.button_subscribe)!! - description = bottomSheetDialog.findViewById(R.id.videodetail_description)!! descriptionContainer = bottomSheetDialog.findViewById(R.id.videodetail_description_container)!! @@ -1263,45 +1165,42 @@ class ShortView : ConstraintLayout { commentsList.onAuthorClick.subscribe { c -> if (c !is PolycentricPlatformComment) { - return@subscribe; + return@subscribe } + val id = c.author.id.value - Logger.i(TAG, "onAuthorClick: " + c.author.id.value); - if (c.author.id.value?.startsWith("polycentric://") ?: false) { - val navUrl = - "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length); - //val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length); + Logger.i(TAG, "onAuthorClick: $id") + if (id != null && id.startsWith("polycentric://") == true) { + val navUrl = "https://harbor.social/" + id.substring("polycentric://".length) mainFragment!!.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl))) - //_container_content_browser.goto(navUrl); - //switchContentView(_container_content_browser); } } commentsList.onRepliesClick.subscribe { c -> - val replyCount = c.replyCount ?: 0; - var metadata = ""; + val replyCount = c.replyCount ?: 0 + var metadata = "" if (replyCount > 0) { - metadata += "$replyCount " + requireContext().getString(R.string.replies); + metadata += "$replyCount " + requireContext().getString(R.string.replies) } if (c is PolycentricPlatformComment) { - var parentComment: PolycentricPlatformComment = c; + var parentComment: PolycentricPlatformComment = c containerContentReplies.load(tabIndex!! != 0, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, { val newComment = parentComment.cloneWithUpdatedReplyCount( (parentComment.replyCount ?: 0) + 1 - ); - commentsList.replaceComment(parentComment, newComment); - parentComment = newComment; - }); + ) + commentsList.replaceComment(parentComment, newComment) + parentComment = newComment + }) } else { - containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) }); + containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) }) } - animateOpenOverlayView(containerContentReplies); + animateOpenOverlayView(containerContentReplies) } if (StatePolycentric.instance.enabled) { buttonPolycentric.setOnClickListener { - setTabIndex(0); - StateMeta.instance.setLastCommentSection(0); + setTabIndex(0) + StateMeta.instance.setLastCommentSection(0) } } else { buttonPolycentric.visibility = GONE @@ -1309,7 +1208,7 @@ class ShortView : ConstraintLayout { buttonPlatform.setOnClickListener { setTabIndex(1) - StateMeta.instance.setLastCommentSection(1); + StateMeta.instance.setLastCommentSection(1) } val ref = Models.referenceFromBuffer(video.url.toByteArray()) @@ -1325,10 +1224,6 @@ class ShortView : ConstraintLayout { } } -// val layoutTop: LinearLayout = bottomSheetDialog.findViewById(R.id.layout_top)!! -// containerContentMain.removeView(layoutTop) -// commentsList.setPrependedView(layoutTop) - containerContentDescription.onClose.subscribe { animateCloseOverlayView() } containerContentReplies.onClose.subscribe { animateCloseOverlayView() } @@ -1344,89 +1239,89 @@ class ShortView : ConstraintLayout { TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics) //UI - title.text = video.name; - channelName.text = video.author.name; + title.text = video.name + channelName.text = video.author.name if (video.author.subscribers != null) { channelMeta.text = if ((video.author.subscribers ?: 0) > 0 - ) video.author.subscribers!!.toHumanNumber() + " " + requireContext().getString(R.string.subscribers) else ""; + ) video.author.subscribers!!.toHumanNumber() + " " + requireContext().getString(R.string.subscribers) else "" (channelName.layoutParams as MarginLayoutParams).setMargins( 0, (dp5 * -1).toInt(), 0, 0 - ); + ) } else { - channelMeta.text = ""; - (channelName.layoutParams as MarginLayoutParams).setMargins(0, (dp2).toInt(), 0, 0); + channelMeta.text = "" + (channelName.layoutParams as MarginLayoutParams).setMargins(0, (dp2).toInt(), 0, 0) } - video.author.let { - if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty()) monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl); - else monetization.setPlatformMembership(null, null); + if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty()) monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl) + else monetization.setPlatformMembership(null, null) } - val subTitleSegments: ArrayList = ArrayList(); - if (video.viewCount > 0) subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if (video.isLive) requireContext().getString(R.string.watching_now) else requireContext().getString(R.string.views)}"); + val subTitleSegments: ArrayList = ArrayList() + if (video.viewCount > 0) subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if (video.isLive) requireContext().getString(R.string.watching_now) else requireContext().getString(R.string.views)}") if (video.datetime != null) { - val diff = video.datetime?.getNowDiffSeconds() ?: 0; + val diff = video.datetime?.getNowDiffSeconds() ?: 0 val ago = video.datetime?.toHumanNowDiffString(true) - if (diff >= 0) subTitleSegments.add("${ago} ago"); - else subTitleSegments.add("available in ${ago}"); + if (diff >= 0) subTitleSegments.add("$ago ago") + else subTitleSegments.add("available in $ago") } - platform.setPlatformFromClientID(video.id.pluginId); - subTitle.text = subTitleSegments.joinToString(" • "); - creatorThumbnail.setThumbnail(video.author.thumbnail, false); + platform.setPlatformFromClientID(video.id.pluginId) + subTitle.text = subTitleSegments.joinToString(" • ") + creatorThumbnail.setThumbnail(video.author.thumbnail, false) - setPolycentricProfile(null, animate = false); + setPolycentricProfile(null, animate = false) + _taskLoadPolycentricProfile.run(video.author.id) when (video.rating) { is RatingLikeDislikes -> { - val r = video.rating as RatingLikeDislikes; - layoutRating.visibility = View.VISIBLE; + val r = video.rating as RatingLikeDislikes + layoutRating.visibility = VISIBLE - textLikes.visibility = View.VISIBLE; - imageLikeIcon.visibility = View.VISIBLE; - textLikes.text = r.likes.toHumanNumber(); + textLikes.visibility = VISIBLE + imageLikeIcon.visibility = VISIBLE + textLikes.text = r.likes.toHumanNumber() - imageDislikeIcon.visibility = View.VISIBLE; - textDislikes.visibility = View.VISIBLE; - textDislikes.text = r.dislikes.toHumanNumber(); + imageDislikeIcon.visibility = VISIBLE + textDislikes.visibility = VISIBLE + textDislikes.text = r.dislikes.toHumanNumber() } is RatingLikes -> { - val r = video.rating as RatingLikes; - layoutRating.visibility = View.VISIBLE; + val r = video.rating as RatingLikes + layoutRating.visibility = VISIBLE - textLikes.visibility = View.VISIBLE; - imageLikeIcon.visibility = View.VISIBLE; - textLikes.text = r.likes.toHumanNumber(); + textLikes.visibility = VISIBLE + imageLikeIcon.visibility = VISIBLE + textLikes.text = r.likes.toHumanNumber() - imageDislikeIcon.visibility = View.GONE; - textDislikes.visibility = View.GONE; + imageDislikeIcon.visibility = GONE + textDislikes.visibility = GONE } else -> { - layoutRating.visibility = View.GONE; + layoutRating.visibility = GONE } } monetization.onSupportTap.subscribe { - containerContentSupport.setPolycentricProfile(polycentricProfile); + containerContentSupport.setPolycentricProfile(polycentricProfile) animateOpenOverlayView(containerContentSupport) - }; + } monetization.onStoreTap.subscribe { polycentricProfile?.systemState?.store?.let { try { - val uri = Uri.parse(it); - val intent = Intent(Intent.ACTION_VIEW); - intent.data = uri; - requireContext().startActivity(intent); + val uri = Uri.parse(it) + val intent = Intent(Intent.ACTION_VIEW) + intent.data = uri + requireContext().startActivity(intent) } catch (e: Throwable) { - Logger.e(TAG, "Failed to open URI: '${it}'.", e); + Logger.e(TAG, "Failed to open URI: '${it}'.", e) } } - }; + } monetization.onUrlTap.subscribe { mainFragment!!.navigate(it) } @@ -1436,25 +1331,9 @@ class ShortView : ConstraintLayout { } channelButton.setOnClickListener { - mainFragment!!.navigate(video.author); - }; + mainFragment!!.navigate(video.author) + } -// composeView?.apply { -// setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) -// setContent { -// // In Compose world -// MaterialTheme { -// val view = LocalView.current -// IconButton(onClick = { -// view.playSoundEffect(SoundEffectConstants.CLICK) -// }) { -// Icon( -// Icons.Outlined.ThumbUp, contentDescription = "Close Bottom Sheet" -// ) -// } -// } -// } -// } return bottomSheetDialog } @@ -1466,15 +1345,15 @@ class ShortView : ConstraintLayout { private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) { polycentricProfile = profile - val dp_35 = 35.dp(requireContext().resources) - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35) + val dp35 = 35.dp(requireContext().resources) + val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35) ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) } if (avatar != null) { - creatorThumbnail.setThumbnail(avatar, animate); + creatorThumbnail.setThumbnail(avatar, animate) } else { - creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate); - creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); + creatorThumbnail.setThumbnail(video.author.thumbnail, animate) + creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()) } val username = profile?.systemState?.username @@ -1482,7 +1361,7 @@ class ShortView : ConstraintLayout { channelName.text = username } - monetization.setPolycentricProfile(profile); + monetization.setPolycentricProfile(profile) } private fun setTabIndex(index: Int?, forceReload: Boolean = false) { @@ -1493,35 +1372,19 @@ class ShortView : ConstraintLayout { } tabIndex = index -// _buttonRecommended.setTextColor(resources.getColor(if (index == 2) R.color.white else R.color.gray_ac)) - buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac)) - buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac)) -// layoutRecommended.removeAllViews() + buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac, null)) + buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac, null)) if (index == null) { - addCommentView.visibility = View.GONE + addCommentView.visibility = GONE commentsList.clear() -// _layoutRecommended.visibility = View.GONE } else if (index == 0) { - addCommentView.visibility = View.VISIBLE -// _layoutRecommended.visibility = View.GONE + addCommentView.visibility = VISIBLE fetchPolycentricComments() } else if (index == 1) { - addCommentView.visibility = View.GONE -// _layoutRecommended.visibility = View.GONE + addCommentView.visibility = GONE fetchComments() } -// else if (index == 2) { -// _addCommentView.visibility = View.GONE -// _layoutRecommended.visibility = View.VISIBLE -// _commentsList.clear() -// -// _layoutRecommended.addView(LoaderView(context).apply { -// layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources)) -// start() -// }) -// _taskLoadRecommendations.run(null) -// } } private fun fetchComments() { @@ -1533,9 +1396,9 @@ class ShortView : ConstraintLayout { private fun fetchPolycentricComments() { Logger.i(TAG, "fetchPolycentricComments") - val video = video; - val idValue = video?.id?.value - if (video?.url?.isEmpty() != false) { + val video = video + val idValue = video.id.value + if (video.url.isEmpty() != false) { Logger.w(TAG, "Failed to fetch polycentric comments because url was null") commentsList.clear() return @@ -1543,7 +1406,7 @@ class ShortView : ConstraintLayout { val ref = Models.referenceFromBuffer(video.url.toByteArray()) val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null } - commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); }; + commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); } } private fun updateDescriptionUI(text: Spanned) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt index 80322e5e..04eb3cba 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt @@ -16,11 +16,11 @@ import com.futo.platformplayer.R import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StatePlatform import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -43,9 +43,6 @@ class ShortsFragment : MainFragment() { private lateinit var overlayLoadingSpinner: ImageView private lateinit var overlayQualityContainer: FrameLayout private lateinit var customViewAdapter: CustomViewAdapter - private val urls = listOf( - "https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://www.youtube.com/watch?v=MXHSS-7XcBc", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra", "https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra" - ) init { loadPager() @@ -60,10 +57,10 @@ class ShortsFragment : MainFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewPager = view.findViewById(R.id.viewPager) + viewPager = view.findViewById(R.id.view_pager) overlayLoading = view.findViewById(R.id.short_view_loading_overlay) overlayLoadingSpinner = view.findViewById(R.id.short_view_loader) - overlayQualityContainer = view.findViewById(R.id.videodetail_quality_overview) + overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview) setLoading(true) @@ -72,12 +69,14 @@ class ShortsFragment : MainFragment() { } loadPagerJob!!.invokeOnCompletion { - customViewAdapter = CustomViewAdapter(videos, layoutInflater, this@ShortsFragment, overlayQualityContainer) { - if (!shortsPager!!.hasMorePages()) { - return@CustomViewAdapter + Logger.i(TAG, "Creating adapter") + customViewAdapter = + CustomViewAdapter(videos, layoutInflater, this@ShortsFragment, overlayQualityContainer) { + if (!shortsPager!!.hasMorePages()) { + return@CustomViewAdapter + } + nextPage() } - nextPage() - } customViewAdapter.onResetTriggered.subscribe { setLoading(true) loadPager() @@ -88,7 +87,6 @@ class ShortsFragment : MainFragment() { val viewPager = viewPager!! viewPager.adapter = customViewAdapter - // TODO something is laggy sometimes when swiping between videos viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { @OptIn(UnstableApi::class) override fun onPageSelected(position: Int) { @@ -96,7 +94,8 @@ class ShortsFragment : MainFragment() { adapter.previousShownView?.stop() adapter.previousShownView = null -// viewPager.post { + // the post prevents lag when swiping + viewPager.post { val recycler = (viewPager.getChildAt(0) as RecyclerView) val viewHolder = recycler.findViewHolderForAdapterPosition(position) as CustomViewHolder? @@ -108,10 +107,8 @@ class ShortsFragment : MainFragment() { focusedView.play() adapter.previousShownView = focusedView } -// } + } } - - }) setLoading(false) } @@ -154,12 +151,9 @@ class ShortsFragment : MainFragment() { viewPager?.currentItem = 0 loadPagerJob = CoroutineScope(Dispatchers.Main).launch { -// delay(5000) val pager = try { withContext(Dispatchers.IO) { StatePlatform.instance.getShorts() -// StatePlatform.instance.getHome() - // as IPager } } catch (_: CancellationException) { return@launch diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt index 27befb1e..0dba3c4f 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt @@ -19,7 +19,9 @@ class WebviewOverlay : LinearLayout { inflate(context, R.layout.overlay_webview, this) _topbar = findViewById(R.id.topbar); _webview = findViewById(R.id.webview); - _webview.settings.javaScriptEnabled = true; + if (!isInEditMode){ + _webview.settings.javaScriptEnabled = true; + } _topbar.onClose.subscribe(this, onClose::emit); } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt index 8ff562b6..61cf886a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt @@ -2,28 +2,25 @@ package com.futo.platformplayer.views.video import android.animation.ValueAnimator import android.content.Context -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater import android.view.animation.LinearInterpolator import androidx.annotation.OptIn +import androidx.media3.common.C import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.upstream.DefaultAllocator import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerView import androidx.media3.ui.TimeBar -import com.bumptech.glide.Glide -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition import com.futo.platformplayer.R -import com.futo.platformplayer.Settings -import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails -import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StatePlayer +import com.futo.platformplayer.video.PlayerManager @UnstableApi class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : @@ -35,23 +32,9 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : } private var playerAttached = false -// private set; - private val videoView: PlayerView private val progressBar: DefaultTimeBar - - private val loadArtwork = object : CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - setArtwork(BitmapDrawable(resources, resource)) - } - - override fun onLoadCleared(placeholder: Drawable?) { - setArtwork(null) - } - } - - private val player = StatePlayer.instance.getShortPlayerOrCreate(context) - + private lateinit var player: PlayerManager private var progressAnimator: ValueAnimator = createProgressBarAnimator() private var playerEventListener = object : Player.Listener { @@ -67,10 +50,9 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : } if (player.isPlaying) { - if (progressAnimator.isPaused){ + if (progressAnimator.isPaused) { progressAnimator.resume() - } - else if (!progressAnimator.isStarted) { + } else if (!progressAnimator.isStarted) { progressAnimator.start() } } else { @@ -84,10 +66,13 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : init { LayoutInflater.from(context).inflate(R.layout.view_short_player, this, true) - videoView = findViewById(R.id.video_player) - progressBar = findViewById(R.id.video_player_progress_bar) + videoView = findViewById(R.id.short_player_view) + progressBar = findViewById(R.id.short_player_progress_bar) - player.player.repeatMode = Player.REPEAT_MODE_ONE + if (!isInEditMode) { + player = StatePlayer.instance.getShortPlayerOrCreate(context) + player.player.repeatMode = Player.REPEAT_MODE_ONE + } progressBar.addListener(object : TimeBar.OnScrubListener { override fun onScrubStart(timeBar: TimeBar, position: Long) { @@ -148,30 +133,6 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : player.detach() } - fun setPreview(video: IPlatformVideoDetails) { - if (video.live != null) { - setSource(video.live, null, play = true, keepSubtitles = false) - } else { - val videoSource = - VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS) - val audioSource = - VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context)) - if (videoSource == null && audioSource != null) { - val thumbnail = video.thumbnails.getHQThumbnail() - if (!thumbnail.isNullOrBlank()) { - Glide.with(videoView).asBitmap().load(thumbnail).into(loadArtwork) - } else { - Glide.with(videoView).clear(loadArtwork) - setArtwork(null) - } - } else { - Glide.with(videoView).clear(loadArtwork) - } - - setSource(videoSource, audioSource, play = true, keepSubtitles = false) - } - } - @OptIn(UnstableApi::class) fun setArtwork(drawable: Drawable?) { if (drawable != null) { @@ -194,9 +155,4 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : val param = PlaybackParameters(playbackRate) exoPlayer?.playbackParameters = param } - - // TODO remove stub - fun hideControls(stub: Boolean) { - - } } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index c3eddb12..a7cf04e8 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -72,7 +72,7 @@ import kotlin.math.abs abstract class FutoVideoPlayerBase : ConstraintLayout { private val TAG = "FutoVideoPlayerBase" - private val TEMP_DIRECTORY = StateApp.instance.getTempDirectory(); +// private val TEMP_DIRECTORY = StateApp.instance.getTempDirectory(); private var _mediaSource: MediaSource? = null; diff --git a/app/src/main/res/layout/fragment_shorts.xml b/app/src/main/res/layout/fragment_shorts.xml index 7d6086da..25119232 100644 --- a/app/src/main/res/layout/fragment_shorts.xml +++ b/app/src/main/res/layout/fragment_shorts.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent"> @@ -29,7 +29,7 @@ - - + android:background="@color/black" + android:orientation="vertical" + tools:ignore="SpeakableTextPresentCheck"> + android:layout_height="wrap_content" + android:orientation="vertical"> + + android:orientation="horizontal"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + android:layout_marginStart="14dp" + android:layout_marginEnd="14dp"> - + + + + + - + - + android:orientation="horizontal"> - + + + + + + android:orientation="horizontal"> + + + + - - - - - - - - - - - - - - - - -