Compare commits

...

33 Commits

Author SHA1 Message Date
Koen 898637a616 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-06 23:07:50 +01:00
Koen f1860126a7 - Changed casting connection timeout to 2 seconds.
- Added ping pong to FCast.
- Removal of DataInputStream and just using raw InputStream.
- Fix slider position crash.
2024-01-06 23:07:34 +01:00
Kelvin f8402676d7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-06 22:19:38 +01:00
Kelvin cf86ce1ab3 Minor leak fix, login warning support, refs 2024-01-06 22:19:29 +01:00
Koen f4cb1719e0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-04 13:38:37 +01:00
Koen 4898cb53ae Fixed tint color. 2024-01-04 13:38:30 +01:00
Kelvin 0f60d4737e Plugin update appToast, Refs 2024-01-03 19:47:38 +01:00
Kelvin 0dc33e1f2b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-03 17:59:11 +01:00
Kelvin d86a997a88 appToast system, VideoToOpen changed 2024-01-03 17:59:01 +01:00
Koen 34d4d92289 Casting stability fixes to ChromeCast connection thread. 2024-01-03 11:24:55 +01:00
Koen 4cb1bf268f Updated YouTube submodule. 2024-01-03 11:09:57 +01:00
Kelvin 8488706ff9 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-02 20:14:36 +01:00
Kelvin a348bb2662 Fix crash download livestream, refs 2024-01-02 20:14:28 +01:00
Koen 60a17b3c67 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-02 19:48:51 +01:00
Koen 386c58d4ad Added timeouts to casting flow. 2024-01-02 19:48:43 +01:00
Kelvin 356ba01dc1 Fix default comment section 2024-01-02 16:28:08 +01:00
Kelvin ed2aa848da Fix clear playback notification, Update V8 Library, Close modified requests after usage 2024-01-02 15:55:29 +01:00
Kelvin c5dd90048f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-02 14:36:46 +01:00
Kelvin ab04f334dc Download cleanup after cancel/failure and on startup, auto-select subtitles if downloaded, refs 2024-01-02 14:36:41 +01:00
Koen 0d44f8a416 Fixed issue where some videos would not play in Odysee. 2024-01-02 13:36:43 +01:00
Koen d01a1545e2 Allow large heap to fix Rumble failing to load large videos. 2024-01-02 13:35:48 +01:00
Kelvin e599729ba1 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-27 15:21:52 +01:00
Kelvin 3ac043740e Fix RequestModifier not being applied, and add default option to add pre-existing headers 2023-12-27 15:21:43 +01:00
Koen 89603d0ff3 changed typeface when update is available. 2023-12-27 14:31:26 +01:00
Koen 05b6cd7c97 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-27 11:11:36 +01:00
Koen ea5aad0631 Implemented check for updates. 2023-12-27 11:11:29 +01:00
Kelvin 96e034b9bf Fix plugin.d.ts and update 2023-12-22 19:29:38 +01:00
Kelvin 6141c36855 Refs 2023-12-21 20:04:53 +01:00
Kelvin 4084ab3ed0 refs 2023-12-21 20:01:51 +01:00
Kelvin 34e733823a Refs 2023-12-21 19:27:13 +01:00
Kelvin f1d01642cd Refs, ui fixes 2023-12-21 19:24:30 +01:00
Kelvin d5551d7118 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 17:24:18 +01:00
Kelvin d079a1e8e4 Add missing channel name setter 2023-12-21 17:24:11 +01:00
58 changed files with 813 additions and 371 deletions
+1 -1
View File
@@ -169,7 +169,7 @@ dependencies {
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject) implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
//JS //JS
implementation("com.caoccao.javet:javet-android:2.2.1") implementation("com.caoccao.javet:javet-android:3.0.2")
//Exoplayer //Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.0' implementation 'androidx.media3:media3-exoplayer:1.2.0'
+2 -1
View File
@@ -24,7 +24,8 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.FutoVideo" android:theme="@style/Theme.FutoVideo"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31"
android:largeHeap="true">
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="@string/authority" android:authorities="@string/authority"
+120 -46
View File
@@ -1,13 +1,37 @@
declare class ScriptException extends Error { declare class ScriptException extends Error {
//If only one parameter is provided, acts as msg
constructor(type: string, msg: string); constructor(type: string, msg: string);
} }
declare class TimeoutException extends ScriptException {
declare class LoginRequiredException extends ScriptException {
constructor(msg: string); constructor(msg: string);
} }
//Alias
declare class ScriptLoginRequiredException extends ScriptException {
constructor(msg: string);
}
declare class CaptchaRequiredException extends ScriptException {
constructor(url: string, body: string);
}
declare class CriticalException extends ScriptException {
constructor(msg: string);
}
declare class UnavailableException extends ScriptException { declare class UnavailableException extends ScriptException {
constructor(msg: string); constructor(msg: string);
} }
declare class AgeException extends ScriptException {
constructor(msg: string);
}
declare class TimeoutException extends ScriptException {
constructor(msg: string);
}
declare class ScriptImplementationException extends ScriptException { declare class ScriptImplementationException extends ScriptException {
constructor(msg: string); constructor(msg: string);
} }
@@ -38,16 +62,23 @@ declare class FilterCapability {
declare class PlatformAuthorLink { declare class PlatformAuthorLink {
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?); constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
}
declare class PlatformAuthorMembershipLink {
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
} }
declare interface PlatformContentDef { declare interface PlatformContentDef {
id: PlatformID, id: PlatformID,
name: string, name: string,
thumbnails: Thumbnails,
author: PlatformAuthorLink, author: PlatformAuthorLink,
datetime: integer, datetime: integer,
url: string url: string
} }
declare interface PlatformContent {}
declare interface PlatformNestedMediaContentDef extends PlatformContentDef { declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
contentUrl: string, contentUrl: string,
contentName: string?, contentName: string?,
@@ -59,16 +90,26 @@ declare class PlatformNestedMediaContent {
constructor(obj: PlatformNestedMediaContentDef); constructor(obj: PlatformNestedMediaContentDef);
} }
declare interface PlatformLockedContentDef extends PlatformContentDef {
contentName: string?,
contentThumbnails: Thumbnails?,
unlockUrl: string,
lockDescription: string?,
}
declare class PlatformLockedContent {
constructor(obj: PlatformLockedContentDef);
}
declare interface PlatformVideoDef extends PlatformContentDef { declare interface PlatformVideoDef extends PlatformContentDef {
thumbnails: Thumbnails, thumbnails: Thumbnails,
author: PlatformAuthorLink, author: PlatformAuthorLink,
duration: int, duration: int,
viewCount: long, viewCount: long,
isLive: boolean isLive: boolean,
shareUrl: string?
} }
declare interface PlatformContent {}
declare class PlatformVideo implements PlatformContent { declare class PlatformVideo implements PlatformContent {
constructor(obj: PlatformVideoDef); constructor(obj: PlatformVideoDef);
} }
@@ -77,14 +118,15 @@ declare class PlatformVideo implements PlatformContent {
declare interface PlatformVideoDetailsDef extends PlatformVideoDef { declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
description: string, description: string,
video: VideoSourceDescriptor, video: VideoSourceDescriptor,
live: SubtitleSource[], live: IVideoSource,
rating: IRating rating: IRating,
subtitles: SubtitleSource[]
} }
declare class PlatformVideoDetails extends PlatformVideo { declare class PlatformVideoDetails extends PlatformVideo {
constructor(obj: PlatformVideoDetailsDef); constructor(obj: PlatformVideoDetailsDef);
} }
declare class PlatformPostDef extends PlatformContentDef { declare interface PlatformPostDef extends PlatformContentDef {
thumbnails: string[], thumbnails: string[],
images: string[], images: string[],
description: string description: string
@@ -93,7 +135,7 @@ declare class PlatformPost extends PlatformContent {
constructor(obj: PlatformPostDef) constructor(obj: PlatformPostDef)
} }
declare class PlatformPostDetailsDef extends PlatformPostDef { declare interface PlatformPostDetailsDef extends PlatformPostDef {
rating: IRating, rating: IRating,
textType: int, textType: int,
content: String content: String
@@ -110,8 +152,8 @@ declare interface MuxVideoSourceDescriptorDef {
isUnMuxed: boolean, isUnMuxed: boolean,
videoSources: VideoSource[] videoSources: VideoSource[]
} }
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor { declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
constructor(obj: VideoSourceDescriptorDef); constructor(videoSourcesOrObj: VideoSource[]);
} }
declare interface UnMuxVideoSourceDescriptorDef { declare interface UnMuxVideoSourceDescriptorDef {
@@ -129,7 +171,7 @@ declare interface IVideoSource {
declare interface IAudioSource { declare interface IAudioSource {
} }
interface VideoUrlSourceDef implements IVideoSource { declare interface VideoUrlSourceDef implements IVideoSource {
width: integer, width: integer,
height: integer, height: integer,
container: string, container: string,
@@ -139,22 +181,22 @@ interface VideoUrlSourceDef implements IVideoSource {
duration: integer, duration: integer,
url: string url: string
} }
class VideoUrlSource { declare class VideoUrlSource {
constructor(obj: VideoUrlSourceDef); constructor(obj: VideoUrlSourceDef);
getRequestModifier(): RequestModifier?; getRequestModifier(): RequestModifier?;
} }
interface VideoUrlRangeSourceDef extends VideoUrlSource { declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
itagId: integer, itagId: integer,
initStart: integer, initStart: integer,
initEnd: integer, initEnd: integer,
indexStart: integer, indexStart: integer,
indexEnd: integer, indexEnd: integer,
} }
class VideoUrlRangeSource extends VideoUrlSource { declare class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj: YTVideoSourceDef); constructor(obj: YTVideoSourceDef);
} }
interface AudioUrlSourceDef { declare interface AudioUrlSourceDef {
name: string, name: string,
bitrate: integer, bitrate: integer,
container: string, container: string,
@@ -163,24 +205,12 @@ interface AudioUrlSourceDef {
url: string, url: string,
language: string language: string
} }
class AudioUrlSource implements IAudioSource { declare class AudioUrlSource implements IAudioSource {
constructor(obj: AudioUrlSourceDef); constructor(obj: AudioUrlSourceDef);
getRequestModifier(): RequestModifier?; getRequestModifier(): RequestModifier?;
} }
interface IRequest { declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
url: string,
headers: Map<string, string>
}
interface IRequestModifierDef {
allowByteSkip: boolean
}
class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
}
interface AudioUrlRangeSourceDef extends AudioUrlSource {
itagId: integer, itagId: integer,
initStart: integer, initStart: integer,
initEnd: integer, initEnd: integer,
@@ -188,28 +218,44 @@ interface AudioUrlRangeSourceDef extends AudioUrlSource {
indexEnd: integer, indexEnd: integer,
audioChannels: integer audioChannels: integer
} }
class AudioUrlRangeSource extends AudioUrlSource { declare class AudioUrlRangeSource extends AudioUrlSource {
constructor(obj: AudioUrlRangeSourceDef); constructor(obj: AudioUrlRangeSourceDef);
} }
interface HLSSourceDef { declare interface HLSSourceDef {
name: string, name: string,
duration: integer, duration: integer,
url: string url: string,
priority: boolean?,
language: string?
} }
class HLSSource implements IVideoSource { declare class HLSSource implements IVideoSource {
constructor(obj: HLSSourceDef); constructor(obj: HLSSourceDef);
} }
interface DashSourceDef { declare interface DashSourceDef {
name: string, name: string,
duration: integer, duration: integer,
url: string url: string,
language: string?
} }
class DashSource implements IVideoSource { declare class DashSource implements IVideoSource {
constructor(obj: DashSourceDef) constructor(obj: DashSourceDef)
} }
declare interface IRequest {
url: string,
headers: Map<string, string>
}
declare interface IRequestModifierDef {
allowByteSkip: boolean
}
declare class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
}
//Channel //Channel
interface PlatformChannelDef { declare interface PlatformChannelDef {
id: PlatformID, id: PlatformID,
name: string, name: string,
thumbnail: string, thumbnail: string,
@@ -217,12 +263,29 @@ interface PlatformChannelDef {
subscribers: integer, subscribers: integer,
description: string, description: string,
url: string, url: string,
urlAlternatives: string[],
links: Map<string>? links: Map<string>?
} }
class PlatformChannel { declare class PlatformChannel {
constructor(obj: PlatformChannelDef); constructor(obj: PlatformChannelDef);
} }
//Playlist
declare interface PlatformPlaylistDef implements PlatformContent {
videoCount: integer,
thumbnail: string
}
declare class PlatformPlaylist extends PlatformContent {
constructor(obj: PlatformPlaylistDef);
}
declare interface PlatformPlaylistDetailsDef implements PlatformPlaylistDef {
contents: ContentPager
}
declare class PlatformPlaylistDetails extends PlatformContent {
constructor(obj: PlatformPlaylistDetailsDef);
}
//Ratings //Ratings
interface IRating { interface IRating {
type: integer type: integer
@@ -250,7 +313,11 @@ declare class PlatformComment {
constructor(obj: CommentDef); constructor(obj: CommentDef);
} }
declare class PlaybackTracker {
constructor(interval: integer);
setProgress(seconds: integer);
}
declare class LiveEventPager { declare class LiveEventPager {
nextRequest = 4000; nextRequest = 4000;
@@ -261,8 +328,8 @@ declare class LiveEventPager {
nextPage(): LiveEventPager; //Could be self nextPage(): LiveEventPager; //Could be self
} }
class LiveEvent { declare class LiveEvent {
type: String constructor(type: integer);
} }
declare class LiveEventComment extends LiveEvent { declare class LiveEventComment extends LiveEvent {
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]); constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
@@ -287,25 +354,31 @@ declare class ContentPager {
constructor(results: PlatformContent[], hasMore: boolean); constructor(results: PlatformContent[], hasMore: boolean);
hasMorePagers(): boolean hasMorePagers(): boolean
nextPage(): VideoPager; //Could be self nextPage(): ContentPager?; //Could be self
} }
declare class VideoPager { declare class VideoPager {
constructor(results: PlatformVideo[], hasMore: boolean); constructor(results: PlatformVideo[], hasMore: boolean);
hasMorePagers(): boolean hasMorePagers(): boolean
nextPage(): VideoPager; //Could be self nextPage(): VideoPager?; //Could be self
} }
declare class ChannelPager { declare class ChannelPager {
constructor(results: PlatformChannel[], hasMore: boolean); constructor(results: PlatformChannel[], hasMore: boolean);
hasMorePagers(): boolean; hasMorePagers(): boolean;
nextPage(): ChannelPager; //Could be self nextPage(): ChannelPager?; //Could be self
}
declare class PlaylistPager {
constructor(results: PlatformPlaylist[], hasMore: boolean);
hasMorePagers(): boolean;
nextPage(): PlaylistPager?;
} }
declare class CommentPager { declare class CommentPager {
constructor(results: PlatformComment[], hasMore: boolean); constructor(results: PlatformComment[], hasMore: boolean);
hasMorePagers(): boolean hasMorePagers(): boolean
nextPage(): CommentPager; //Could be self nextPage(): CommentPager?; //Could be self
} }
interface Map<T> { interface Map<T> {
@@ -341,8 +414,9 @@ interface Source {
getChannelCapabilities(): ResultCapabilities; getChannelCapabilities(): ResultCapabilities;
isContentDetailsUrl(url: string): boolean; isContentDetailsUrl(url: string): boolean;
getContentDetails(url: string): PlatformVideoDetails; getContentDetails(url: string): PlatformContentDetails;
//Optional
getLiveEvents(url: string): LiveEventPager; getLiveEvents(url: string): LiveEventPager;
//Optional //Optional
+16 -3
View File
@@ -78,6 +78,11 @@ class ScriptLoginRequiredException extends ScriptException {
super("ScriptLoginRequiredException", msg); super("ScriptLoginRequiredException", msg);
} }
} }
class LoginRequiredException extends ScriptException {
constructor(msg) {
super("ScriptLoginRequiredException", msg);
}
}
class CaptchaRequiredException extends Error { class CaptchaRequiredException extends Error {
constructor(url, body) { constructor(url, body) {
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body })); super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
@@ -249,8 +254,8 @@ class PlatformVideoDetails extends PlatformVideo {
this.description = obj.description ?? "";//String this.description = obj.description ?? "";//String
this.video = obj.video ?? {}; //VideoSourceDescriptor this.video = obj.video ?? {}; //VideoSourceDescriptor
this.dash = obj.dash ?? null; //DashSource this.dash = obj.dash ?? null; //DashSource, deprecated
this.hls = obj.hls ?? null; //HLSSource this.hls = obj.hls ?? null; //HLSSource, deprecated
this.live = obj.live ?? null; //VideoSource this.live = obj.live ?? null; //VideoSource
this.rating = obj.rating ?? null; //IRating this.rating = obj.rating ?? null; //IRating
@@ -321,6 +326,8 @@ class VideoUrlSource {
this.bitrate = obj.bitrate ?? 0; this.bitrate = obj.bitrate ?? 0;
this.duration = obj.duration ?? 0; this.duration = obj.duration ?? 0;
this.url = obj.url; this.url = obj.url;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class VideoUrlRangeSource extends VideoUrlSource { class VideoUrlRangeSource extends VideoUrlSource {
@@ -346,6 +353,8 @@ class AudioUrlSource {
this.duration = obj.duration ?? 0; this.duration = obj.duration ?? 0;
this.url = obj.url; this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN; this.language = obj.language ?? Language.UNKNOWN;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class AudioUrlRangeSource extends AudioUrlSource { class AudioUrlRangeSource extends AudioUrlSource {
@@ -371,6 +380,8 @@ class HLSSource {
this.priority = obj.priority ?? false; this.priority = obj.priority ?? false;
if(obj.language) if(obj.language)
this.language = obj.language; this.language = obj.language;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class DashSource { class DashSource {
@@ -382,13 +393,15 @@ class DashSource {
this.url = obj.url; this.url = obj.url;
if(obj.language) if(obj.language)
this.language = obj.language; this.language = obj.language;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class RequestModifier { class RequestModifier {
constructor(obj) { constructor(obj) {
obj = obj ?? {}; obj = obj ?? {};
this.allowByteSkip = obj.allowByteSkip; this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip
} }
} }
@@ -1,5 +1,6 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.util.Log
import com.google.common.base.CharMatcher import com.google.common.base.CharMatcher
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
@@ -9,7 +10,6 @@ import java.net.InetAddress
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Socket import java.net.Socket
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.charset.Charset
private const val IPV4_PART_COUNT = 4; private const val IPV4_PART_COUNT = 4;
@@ -216,15 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
} }
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? { fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
val timeout = 2000
if (addresses.isEmpty()) { if (addresses.isEmpty()) {
return null; return null;
} }
if (addresses.size == 1) { if (addresses.size == 1) {
val socket = Socket()
try { try {
return Socket(addresses[0], port); return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
} catch (e: Throwable) { } catch (e: Throwable) {
//Ignored. Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
socket.close()
} }
return null; return null;
@@ -249,7 +254,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
} }
} }
socket.connect(InetSocketAddress(address, port)); socket.connect(InetSocketAddress(address, port), timeout);
synchronized(syncObject) { synchronized(syncObject) {
if (connectedSocket == null) { if (connectedSocket == null) {
@@ -263,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
} }
} }
} catch (e: Throwable) { } catch (e: Throwable) {
//Ignore Log.i("getConnectedSocket", "Failed to connect to: $address", e)
} }
}; };
@@ -685,7 +685,9 @@ class Settings : FragmentedStorageFileJson() {
fun manualCheck() { fun manualCheck() {
if (!BuildConfig.IS_PLAYSTORE_BUILD) { if (!BuildConfig.IS_PLAYSTORE_BUILD) {
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
StateUpdate.instance.checkForUpdates(it, true); StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(it, true)
}
} }
} else { } else {
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
@@ -37,6 +37,7 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -398,13 +399,28 @@ class UIDialogs {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try { try {
StateApp.withContext { StateApp.withContext {
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show(); toast(it, text, long);
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e); Logger.e(TAG, "Failed to show toast.", e);
} }
} }
} }
fun appToast(text: String, long: Boolean = false) {
appToast(ToastView.Toast(text, long))
}
fun appToastError(text: String, long: Boolean) {
StateApp.withContext {
appToast(ToastView.Toast(text, long, it.getColor(R.color.pastel_red)));
};
}
fun appToast(toast: ToastView.Toast) {
StateApp.withContext {
if(it is MainActivity) {
it.showAppToast(toast);
}
}
}
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) { fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
//TODO: Is not actually clickable... //TODO: Is not actually clickable...
@@ -343,7 +343,7 @@ class UISlideOverlays {
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(), videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
Settings.instance.downloads.getDefaultVideoQualityPixels(), Settings.instance.downloads.getDefaultVideoQualityPixels(),
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
) as IVideoUrlSource; ) as IVideoUrlSource?;
} }
if (audioSources != null) { if (audioSources != null) {
@@ -216,8 +216,10 @@ class AddSourceActivity : AppCompatActivity() {
fun install(config: SourcePluginConfig, script: String) { fun install(config: SourcePluginConfig, script: String) {
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) { StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
if(it) if(it) {
StatePlatform.instance.clearUpdateAvailable(config)
backToSources(); backToSources();
}
} }
} }
@@ -9,9 +9,11 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.LogLevel import com.futo.platformplayer.logging.LogLevel
import com.futo.platformplayer.logging.Logging import com.futo.platformplayer.logging.Logging
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -10,6 +10,7 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
@@ -39,6 +40,7 @@ class LoginActivity : AppCompatActivity() {
_textUrl = findViewById(R.id.text_url); _textUrl = findViewById(R.id.text_url);
_buttonClose = findViewById(R.id.button_close); _buttonClose = findViewById(R.id.button_close);
_buttonClose.setOnClickListener { _buttonClose.setOnClickListener {
UIDialogs.toast("Login cancelled", false);
finish(); finish();
} }
@@ -1,7 +1,6 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
@@ -24,6 +23,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -45,6 +45,7 @@ import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SubscriptionStorage import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -54,6 +55,7 @@ import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class MainActivity : AppCompatActivity, IWithResultLauncher { class MainActivity : AppCompatActivity, IWithResultLauncher {
@@ -65,6 +67,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var rootView : MotionLayout; lateinit var rootView : MotionLayout;
private lateinit var _overlayContainer: FrameLayout; private lateinit var _overlayContainer: FrameLayout;
private lateinit var _toastView: ToastView;
//Segment Containers //Segment Containers
private lateinit var _fragContainerTopBar: FragmentContainerView; private lateinit var _fragContainerTopBar: FragmentContainerView;
@@ -207,7 +210,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay); _fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container); _fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
_overlayContainer = findViewById(R.id.overlay_container); _overlayContainer = findViewById(R.id.overlay_container);
//_overlayContainer.visibility = View.GONE; _toastView = findViewById(R.id.toast_view);
//Initialize fragments //Initialize fragments
@@ -478,21 +481,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
_isVisible = true; _isVisible = true;
val videoToOpen = StateSaved.instance.videoToOpen;
if (_wasStopped) {
_wasStopped = false;
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
_fragVideoDetail.maximizeVideoDetail(true);
}
StateSaved.instance.setVideoToOpenNonBlocking(null);
}
}
} }
override fun onPause() { override fun onPause() {
@@ -864,7 +852,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_orientationManager.disable(); _orientationManager.disable();
StateApp.instance.mainAppDestroyed(this); StateApp.instance.mainAppDestroyed(this);
StateSaved.instance.setVideoToOpenBlocking(null);
} }
inline fun <reified T> isFragmentActive(): Boolean { inline fun <reified T> isFragmentActive(): Boolean {
@@ -1052,6 +1039,43 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
private val _toastQueue = ConcurrentLinkedQueue<ToastView.Toast>();
private var _toastJob: Job? = null;
fun showAppToast(toast: ToastView.Toast) {
synchronized(_toastQueue) {
_toastQueue.add(toast);
if(_toastJob?.isActive != true)
_toastJob = lifecycleScope.launch(Dispatchers.Default) {
launchAppToastJob();
};
}
}
private suspend fun launchAppToastJob() {
Logger.i(TAG, "Starting appToast loop");
while(!_toastQueue.isEmpty()) {
val toast = _toastQueue.poll() ?: continue;
Logger.i(TAG, "Showing next toast (${toast.msg})");
lifecycleScope.launch(Dispatchers.Main) {
if (!_toastView.isVisible) {
Logger.i(TAG, "First showing toast");
_toastView.setToast(toast);
_toastView.show(true);
} else {
_toastView.setToastAnimated(toast);
}
}
if(toast.long)
delay(5000);
else
delay(3000);
}
Logger.i(TAG, "Ending appToast loop");
lifecycleScope.launch(Dispatchers.Main) {
_toastView.hide(true) {
};
}
}
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers. //TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
@@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.modifier
interface IModifierOptions { interface IModifierOptions {
val applyAuthClient: String?; val applyAuthClient: String?;
val applyCookieClient: String?; val applyCookieClient: String?;
val applyOtherHeaders: Boolean;
} }
@@ -11,4 +11,5 @@ class SourcePluginAuthConfig(
val userAgent: String? = null, val userAgent: String? = null,
val loginButton: String? = null, val loginButton: String? = null,
val domainHeadersToFind: Map<String, List<String>>? = null, val domainHeadersToFind: Map<String, List<String>>? = null,
val loginWarning: String? = null
) { } ) { }
@@ -23,21 +23,31 @@ class JSRequest : IRequest {
_v8Options = options; _v8Options = options;
initialize(plugin, originalUrl, originalHeaders); initialize(plugin, originalUrl, originalHeaders);
} }
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?) { constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, applyOtherHeadersByDefault: Boolean = false) {
val contextName = "ModifyRequestResponse"; val contextName = "ModifyRequestResponse";
val config = plugin.config; val config = plugin.config;
_v8Url = obj.getOrDefault<String>(config, "url", contextName, null); _v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
_v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null); _v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
_v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let { _v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
Options(config, it); Options(config, it, applyOtherHeadersByDefault);
} } ?: Options(null, null, applyOtherHeadersByDefault);
initialize(plugin, originalUrl, originalHeaders); initialize(plugin, originalUrl, originalHeaders);
} }
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) { private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
val config = plugin.config; val config = plugin.config;
url = _v8Url ?: originalUrl; url = _v8Url ?: originalUrl;
headers = _v8Headers ?: originalHeaders ?: mapOf();
if(_v8Options?.applyOtherHeaders ?: false) {
val headersToSet = _v8Headers?.toMutableMap() ?: mutableMapOf();
if (originalHeaders != null)
for (head in originalHeaders)
if (!headersToSet.containsKey(head.key))
headersToSet[head.key] = head.value;
headers = headersToSet;
}
else
headers = _v8Headers ?: originalHeaders ?: mapOf();
if(_v8Options != null) { if(_v8Options != null) {
if(_v8Options.applyCookieClient != null && url != null) { if(_v8Options.applyCookieClient != null && url != null) {
@@ -68,10 +78,18 @@ class JSRequest : IRequest {
class Options: IModifierOptions { class Options: IModifierOptions {
override val applyAuthClient: String?; override val applyAuthClient: String?;
override val applyCookieClient: String?; override val applyCookieClient: String?;
override val applyOtherHeaders: Boolean;
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
constructor(config: IV8PluginConfig, obj: V8ValueObject, applyOtherHeadersByDefault: Boolean = false) {
applyAuthClient = obj.getOrDefault(config, "applyAuthClient", "JSRequestModifier.options.applyAuthClient", null); applyAuthClient = obj.getOrDefault(config, "applyAuthClient", "JSRequestModifier.options.applyAuthClient", null);
applyCookieClient = obj.getOrDefault(config, "applyCookieClient", "JSRequestModifier.options.applyCookieClient", null); applyCookieClient = obj.getOrDefault(config, "applyCookieClient", "JSRequestModifier.options.applyCookieClient", null);
applyOtherHeaders = obj.getOrDefault(config, "applyOtherHeaders", "JSRequestModifier.options.applyOtherHeaders", applyOtherHeadersByDefault) ?: applyOtherHeadersByDefault;
}
constructor(applyAuthClient: String? = null, applyCookieClient: String? = null, applyOtherHeaders: Boolean = false) {
this.applyAuthClient = applyAuthClient;
this.applyCookieClient = applyCookieClient;
this.applyOtherHeaders = applyOtherHeaders;
} }
} }
@@ -40,6 +40,7 @@ class JSRequestModifier: IRequestModifier {
} as V8ValueObject; } as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers); val req = JSRequest(_plugin, result, url, headers);
result.close();
return req; return req;
} }
@@ -33,7 +33,7 @@ abstract class JSSource {
this.type = type; this.type = type;
_requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let { _requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
JSRequest(plugin, it, null, null); JSRequest(plugin, it, null, null, true);
} }
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier"); hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
} }
@@ -138,7 +138,7 @@ class AirPlayCastingDevice : CastingDevice {
try { try {
val connectedSocket = getConnectedSocket(adrs.toList(), port); val connectedSocket = getConnectedSocket(adrs.toList(), port);
if (connectedSocket == null) { if (connectedSocket == null) {
delay(3000); delay(1000);
continue; continue;
} }
@@ -17,6 +17,8 @@ import org.json.JSONObject
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.net.InetAddress import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocket
@@ -303,17 +305,18 @@ class ChromecastCastingDevice : CastingDevice {
_thread = Thread { _thread = Thread {
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
var connectedSocket: Socket? = null
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
try { try {
val connectedSocket = getConnectedSocket(adrs.toList(), port); val resultSocket = getConnectedSocket(adrs.toList(), port);
if (connectedSocket == null) { if (resultSocket == null) {
Thread.sleep(3000); Thread.sleep(1000);
continue; continue;
} }
connectedSocket = resultSocket
usedRemoteAddress = connectedSocket.inetAddress; usedRemoteAddress = connectedSocket.inetAddress;
localAddress = connectedSocket.localAddress; localAddress = connectedSocket.localAddress;
connectedSocket.close();
break; break;
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e) Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e)
@@ -325,6 +328,8 @@ class ChromecastCastingDevice : CastingDevice {
val factory = sslContext.socketFactory; val factory = sslContext.socketFactory;
val address = InetSocketAddress(usedRemoteAddress, port)
//Connection loop //Connection loop
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
Logger.i(TAG, "Connecting to Chromecast."); Logger.i(TAG, "Connecting to Chromecast.");
@@ -332,7 +337,16 @@ class ChromecastCastingDevice : CastingDevice {
try { try {
_socket?.close() _socket?.close()
_socket = factory.createSocket(usedRemoteAddress, port) as SSLSocket; if (connectedSocket != null) {
Logger.i(TAG, "Using connected socket.")
_socket = factory.createSocket(connectedSocket, connectedSocket.inetAddress.hostAddress, connectedSocket.port, true) as SSLSocket
connectedSocket = null
} else {
Logger.i(TAG, "Using new socket.")
val s = Socket().apply { this.connect(address, 2000) }
_socket = factory.createSocket(s, s.inetAddress.hostAddress, s.port, true) as SSLSocket
}
_socket?.startHandshake(); _socket?.startHandshake();
Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port"); Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port");
@@ -347,7 +361,7 @@ class ChromecastCastingDevice : CastingDevice {
Logger.i(TAG, "Failed to connect to Chromecast.", e); Logger.i(TAG, "Failed to connect to Chromecast.", e);
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
Thread.sleep(3000); Thread.sleep(1000);
continue; continue;
} }
@@ -363,7 +377,7 @@ class ChromecastCastingDevice : CastingDevice {
_socket?.close(); _socket?.close();
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
Thread.sleep(3000); Thread.sleep(1000);
continue; continue;
} }
@@ -415,7 +429,7 @@ class ChromecastCastingDevice : CastingDevice {
Logger.i(TAG, "Socket disconnected."); Logger.i(TAG, "Socket disconnected.");
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
Thread.sleep(3000); Thread.sleep(1000);
} }
Logger.i(TAG, "Stopped connection loop."); Logger.i(TAG, "Stopped connection loop.");
@@ -432,10 +446,11 @@ class ChromecastCastingDevice : CastingDevice {
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
try { try {
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString()); sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
Thread.sleep(5000);
} catch (e: Throwable) { } catch (e: Throwable) {
Log.w(TAG, "Failed to send ping."); Log.w(TAG, "Failed to send ping.");
} }
Thread.sleep(5000);
} }
Logger.i(TAG, "Stopped ping loop."); Logger.i(TAG, "Stopped ping loop.");
@@ -27,11 +27,12 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.math.BigInteger import java.math.BigInteger
import java.net.InetAddress import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket import java.net.Socket
import java.security.KeyFactory import java.security.KeyFactory
import java.security.KeyPair import java.security.KeyPair
@@ -81,12 +82,13 @@ class FCastCastingDevice : CastingDevice {
var port: Int = 0; var port: Int = 0;
private var _socket: Socket? = null; private var _socket: Socket? = null;
private var _outputStream: DataOutputStream? = null; private var _outputStream: OutputStream? = null;
private var _inputStream: DataInputStream? = null; private var _inputStream: InputStream? = null;
private var _scopeIO: CoroutineScope? = null; private var _scopeIO: CoroutineScope? = null;
private var _started: Boolean = false; private var _started: Boolean = false;
private var _version: Long = 1; private var _version: Long = 1;
private var _thread: Thread? = null private var _thread: Thread? = null
private var _pingThread: Thread? = null
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() { constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
this.name = name; this.name = name;
@@ -240,50 +242,72 @@ class FCastCastingDevice : CastingDevice {
val adrs = addresses ?: return; val adrs = addresses ?: return;
val thread = _thread val thread = _thread
if (thread == null || !thread.isAlive) { val pingThread = _pingThread
Log.i(TAG, "Restarting thread because the thread has died") if (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive) {
Log.i(TAG, "(Re)starting thread because the thread has died")
_scopeIO?.let {
it.cancel()
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
}
_scopeIO?.cancel();
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
_scopeIO = CoroutineScope(Dispatchers.IO); _scopeIO = CoroutineScope(Dispatchers.IO);
_thread = Thread { _thread = Thread {
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
Log.i(TAG, "Connection thread started.")
var connectedSocket: Socket? = null
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
try { try {
val connectedSocket = getConnectedSocket(adrs.toList(), port); Log.i(TAG, "getConnectedSocket (adrs = [ ${adrs.joinToString(", ")} ], port = ${port}).")
if (connectedSocket == null) {
Thread.sleep(3000); val resultSocket = getConnectedSocket(adrs.toList(), port);
if (resultSocket == null) {
Log.i(TAG, "Connection failed, waiting 1 seconds.")
Thread.sleep(1000);
continue; continue;
} }
usedRemoteAddress = connectedSocket.inetAddress; Log.i(TAG, "Connection succeeded.")
localAddress = connectedSocket.localAddress;
connectedSocket.close(); connectedSocket = resultSocket
usedRemoteAddress = connectedSocket.inetAddress
localAddress = connectedSocket.localAddress
break; break;
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(ChromecastCastingDevice.TAG, "Failed to get setup initial connection to FastCast device.", e) Logger.w(TAG, "Failed to get setup initial connection to FastCast device.", e)
} }
} }
val address = InetSocketAddress(usedRemoteAddress, port)
//Connection loop //Connection loop
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
Logger.i(TAG, "Connecting to FastCast."); Logger.i(TAG, "Connecting to FastCast.");
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
try { try {
_socket = Socket(usedRemoteAddress, port); _socket?.close()
if (connectedSocket != null) {
Logger.i(TAG, "Using connected socket.");
_socket = connectedSocket
connectedSocket = null
} else {
Logger.i(TAG, "Using new socket.");
_socket = Socket().apply { this.connect(address, 2000) };
}
Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port"); Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port");
_outputStream = DataOutputStream(_socket?.outputStream); _outputStream = _socket?.outputStream;
_inputStream = DataInputStream(_socket?.inputStream); _inputStream = _socket?.inputStream;
} catch (e: IOException) { } catch (e: IOException) {
_socket?.close(); _socket?.close();
Logger.i(TAG, "Failed to connect to FastCast.", e); Logger.i(TAG, "Failed to connect to FastCast.", e);
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
Thread.sleep(3000); Thread.sleep(1000);
continue; continue;
} }
@@ -298,11 +322,13 @@ class FCastCastingDevice : CastingDevice {
try { try {
val inputStream = _inputStream ?: break; val inputStream = _inputStream ?: break;
Log.d(TAG, "Receiving next packet..."); Log.d(TAG, "Receiving next packet...");
val b1 = inputStream.readUnsignedByte();
val b2 = inputStream.readUnsignedByte(); var headerBytesRead = 0
val b3 = inputStream.readUnsignedByte(); while (headerBytesRead < 4) {
val b4 = inputStream.readUnsignedByte(); headerBytesRead += inputStream.read(buffer, headerBytesRead, 4 - headerBytesRead)
val size = ((b4.toLong() shl 24) or (b3.toLong() shl 16) or (b2.toLong() shl 8) or b1.toLong()).toInt(); }
val size = ((buffer[3].toLong() shl 24) or (buffer[2].toLong() shl 16) or (buffer[1].toLong() shl 8) or buffer[0].toLong()).toInt();
if (size > buffer.size) { if (size > buffer.size) {
Logger.w(TAG, "Skipping packet that is too large $size bytes.") Logger.w(TAG, "Skipping packet that is too large $size bytes.")
inputStream.skip(size.toLong()); inputStream.skip(size.toLong());
@@ -310,7 +336,10 @@ class FCastCastingDevice : CastingDevice {
} }
Log.d(TAG, "Received header indicating $size bytes. Waiting for message."); Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
inputStream.read(buffer, 0, size); var bytesRead = 0
while (bytesRead < size) {
bytesRead += inputStream.read(buffer, bytesRead, size - bytesRead)
}
val messageBytes = buffer.sliceArray(IntRange(0, size)); val messageBytes = buffer.sliceArray(IntRange(0, size));
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}."); Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
@@ -343,12 +372,28 @@ class FCastCastingDevice : CastingDevice {
} }
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
Thread.sleep(3000); Thread.sleep(1000);
} }
Logger.i(TAG, "Stopped connection loop."); Logger.i(TAG, "Stopped connection loop.");
connectionState = CastConnectionState.DISCONNECTED; connectionState = CastConnectionState.DISCONNECTED;
}.apply { start() }; }.apply { start() }
_pingThread = Thread {
Logger.i(TAG, "Started ping loop.")
while (_scopeIO?.isActive == true) {
try {
send(Opcode.Ping)
} catch (e: Throwable) {
Log.w(TAG, "Failed to send ping.");
}
Thread.sleep(5000);
}
Logger.i(TAG, "Stopped ping loop.");
}.apply { start() }
} else { } else {
Log.i(TAG, "Thread was still alive, not restarted") Log.i(TAG, "Thread was still alive, not restarted")
} }
@@ -453,6 +498,7 @@ class FCastCastingDevice : CastingDevice {
_started = false; _started = false;
//TODO: Kill and/or join thread? //TODO: Kill and/or join thread?
_thread = null; _thread = null;
_pingThread = null;
val socket = _socket; val socket = _socket;
val scopeIO = _scopeIO; val scopeIO = _scopeIO;
@@ -143,7 +143,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
StateCasting.instance.onActiveDeviceDurationChanged.remove(this); StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
StateCasting.instance.onActiveDeviceDurationChanged.subscribe { StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
_sliderPosition.valueTo = it.toFloat().coerceAtLeast(1.0f); val dur = it.toFloat().coerceAtLeast(1.0f)
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur);
_sliderPosition.valueTo = dur
}; };
_device = StateCasting.instance.activeDevice; _device = StateCasting.instance.activeDevice;
@@ -185,8 +187,10 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_sliderPosition.valueFrom = 0.0f; _sliderPosition.valueFrom = 0.0f;
_sliderVolume.valueFrom = 0.0f; _sliderVolume.valueFrom = 0.0f;
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo); _sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
_sliderPosition.valueTo = d.duration.toFloat().coerceAtLeast(1.0f);
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo); val dur = d.duration.toFloat().coerceAtLeast(1.0f)
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
_sliderPosition.valueTo = dur
if (d.canSetVolume) { if (d.canSetVolume) {
_layoutVolumeAdjustable.visibility = View.VISIBLE; _layoutVolumeAdjustable.visibility = View.VISIBLE;
@@ -337,8 +337,10 @@ class VideoDownload {
}); });
} }
var wasSuccesful = false;
try { try {
awaitAll(*sourcesToDownload.toTypedArray()); awaitAll(*sourcesToDownload.toTypedArray());
wasSuccesful = true;
} }
catch(runtimeEx: RuntimeException) { catch(runtimeEx: RuntimeException) {
if(runtimeEx.cause != null) if(runtimeEx.cause != null)
@@ -349,6 +351,29 @@ class VideoDownload {
catch(ex: Throwable) { catch(ex: Throwable) {
throw ex; throw ex;
} }
finally {
if(!wasSuccesful) {
try {
if(videoFilePath != null) {
val remainingVideo = File(videoFilePath!!);
if (remainingVideo.exists()) {
Logger.i(TAG, "Deleting remaining video file");
remainingVideo.delete();
}
}
if(audioFilePath != null) {
val remainingAudio = File(audioFilePath!!);
if (remainingAudio.exists()) {
Logger.i(TAG, "Deleting remaining audio file");
remainingAudio.delete();
}
}
}
catch(iex: Throwable) {
Logger.e(TAG, "Failed to delete files after failure:\n${iex.message}", iex);
}
}
}
} }
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long { private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
@@ -418,6 +418,7 @@ class ChannelFragment : MainFragment() {
_buttonSubscribe.setSubscribeChannel(channel); _buttonSubscribe.setSubscribeChannel(channel);
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
_textChannel.text = channel.name;
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else ""; _textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else "";
//TODO: Find a better way to access the adapter fragments.. //TODO: Find a better way to access the adapter fragments..
@@ -354,11 +354,22 @@ class SourceDetailFragment : MainFragment() {
if(config.authentication == null) if(config.authentication == null)
return; return;
LoginActivity.showLogin(StateApp.instance.context, config) { if(config.authentication.loginWarning != null) {
StatePlugins.instance.setPluginAuth(config.id, it); UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Login Warning",
config.authentication.loginWarning, null, 0,
reloadSource(config.id); UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
}; UIDialogs.Action("Login", {
LoginActivity.showLogin(StateApp.instance.context, config) {
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
};
}, UIDialogs.ActionStyle.PRIMARY))
}
else
LoginActivity.showLogin(StateApp.instance.context, config) {
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
};
} }
private fun logoutSource(clear: Boolean = true) { private fun logoutSource(clear: Boolean = true) {
val config = _config ?: return; val config = _config ?: return;
@@ -454,6 +465,7 @@ class SourceDetailFragment : MainFragment() {
} }
}); });
} }
private fun checkForUpdatesSource() { private fun checkForUpdatesSource() {
val c = _config ?: return; val c = _config ?: return;
val sourceUrl = c.sourceUrl ?: return; val sourceUrl = c.sourceUrl ?: return;
@@ -10,6 +10,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.contents.ContentType 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.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@@ -108,16 +109,6 @@ class SubscriptionsFeedFragment : MainFragment() {
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) { constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
Logger.i(TAG, "SubscriptionsFeedFragment constructor()"); Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
if(subGroup?.id == id)
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
setProgress(progress, total);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set progress", e);
}
}
}
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total -> StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
}; };
@@ -173,12 +164,24 @@ class SubscriptionsFeedFragment : MainFragment() {
if (!StateSubscriptions.instance.global.isGlobalUpdating) { if (!StateSubscriptions.instance.global.isGlobalUpdating) {
finishRefreshLayoutLoader(); finishRefreshLayoutLoader();
} }
StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
if(subGroup?.id == id)
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
setProgress(progress, total);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set progress", e);
}
}
}
} }
override fun cleanup() { override fun cleanup() {
super.cleanup() super.cleanup()
StateSubscriptions.instance.global.onUpdateProgress.remove(this); StateSubscriptions.instance.global.onUpdateProgress.remove(this);
StateSubscriptions.instance.onSubscriptionsChanged.remove(this); StateSubscriptions.instance.onSubscriptionsChanged.remove(this);
StateSubscriptions.instance.onFeedProgress.remove(this);
} }
override val feedStyle: FeedStyle get() = Settings.instance.subscriptions.getSubscriptionsFeedStyle(); override val feedStyle: FeedStyle get() = Settings.instance.subscriptions.getSubscriptionsFeedStyle();
@@ -427,7 +430,7 @@ class SubscriptionsFeedFragment : MainFragment() {
context?.let { context?.let {
fragment.lifecycleScope.launch(Dispatchers.Main) { fragment.lifecycleScope.launch(Dispatchers.Main) {
try { try {
if (exs.size <= 8) { if (exs.size <= 3) {
for (ex in exs) { for (ex in exs) {
var toShow = ex; var toShow = ex;
var channel: String? = null; var channel: String? = null;
@@ -437,12 +440,11 @@ class SubscriptionsFeedFragment : MainFragment() {
} }
Logger.e(TAG, "Channel [${channel}] failed", ex); Logger.e(TAG, "Channel [${channel}] failed", ex);
if (toShow is PluginException) if (toShow is PluginException)
UIDialogs.toast( UIDialogs.appToast(
it,
context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "") context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
); );
else else
UIDialogs.toast(it, ex.message ?: ""); UIDialogs.appToast(ex.message ?: "");
} }
} }
else { else {
@@ -453,7 +455,7 @@ class SubscriptionsFeedFragment : MainFragment() {
.map { it!! } .map { it!! }
.toList(); .toList();
for(distinctPluginFail in failedPlugins) for(distinctPluginFail in failedPlugins)
UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: "")); UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to handle exceptions", e) Logger.e(TAG, "Failed to handle exceptions", e)
@@ -25,8 +25,6 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.models.UrlVideoWithTime import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StateSaved
import com.futo.platformplayer.states.VideoToOpen
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
class VideoDetailFragment : MainFragment { class VideoDetailFragment : MainFragment {
@@ -372,11 +370,6 @@ class VideoDetailFragment : MainFragment {
Logger.v(TAG, "shouldStop: $shouldStop"); Logger.v(TAG, "shouldStop: $shouldStop");
if(shouldStop) { if(shouldStop) {
_viewDetail?.let {
val v = it.video ?: return@let;
StateSaved.instance.setVideoToOpenBlocking(VideoToOpen(v.url, (it.lastPositionMilliseconds / 1000.0f).toLong()));
}
_viewDetail?.onStop(); _viewDetail?.onStop();
StateCasting.instance.onStop(); StateCasting.instance.onStop();
Logger.v(TAG, "called onStop() shouldStop: $shouldStop"); Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
@@ -149,6 +149,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -853,14 +855,19 @@ class VideoDetailView : ConstraintLayout {
} }
} }
} }
private val _historyIndexLock = Mutex(false);
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){ suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
val current = _historyIndex; _historyIndexLock.withLock {
if(current == null || current.url != video.url) { val current = _historyIndex;
val index = StateHistory.instance.getHistoryByVideo(video, true)!!; if(current == null || current.url != video.url) {
_historyIndex = index; val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
return@withContext index; _historyIndex = index;
return@withContext index;
}
return@withContext current;
} }
return@withContext current;
} }
@@ -1121,7 +1128,7 @@ class VideoDetailView : ConstraintLayout {
switchContentView(_container_content_main); switchContentView(_container_content_main);
} }
@OptIn(ExperimentalCoroutinesApi::class) //@OptIn(ExperimentalCoroutinesApi::class)
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) { fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
Logger.i(TAG, "setVideoDetails (${videoDetail.name})") Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
@@ -1217,7 +1224,7 @@ class VideoDetailView : ConstraintLayout {
_addCommentView.setContext(video.url, ref) _addCommentView.setContext(video.url, ref)
_player.setMetadata(video.name, video.author.name); _player.setMetadata(video.name, video.author.name);
if (video !is TutorialFragment.TutorialVideo) { if (video is TutorialFragment.TutorialVideo) {
_toggleCommentType.setValue(false, false); _toggleCommentType.setValue(false, false);
} else { } else {
_toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false); _toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false);
@@ -1484,12 +1491,12 @@ class VideoDetailView : ConstraintLayout {
private fun loadCurrentVideo(resumePositionMs: Long = 0) { private fun loadCurrentVideo(resumePositionMs: Long = 0) {
_didStop = false; _didStop = false;
val video = video ?: return; val video = (videoLocal ?: video) ?: return;
try { try {
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount()); val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context)); val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
val subtitleSource = _lastSubtitleSource; val subtitleSource = _lastSubtitleSource ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)") Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
if(videoSource == null && audioSource == null) { if(videoSource == null && audioSource == null) {
@@ -1517,6 +1524,8 @@ class VideoDetailView : ConstraintLayout {
_player.setArtwork(null); _player.setArtwork(null);
_player.setSource(videoSource, audioSource, _playWhenReady, false); _player.setSource(videoSource, audioSource, _playWhenReady, false);
if(subtitleSource != null)
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
_player.seekTo(resumePositionMs); _player.seekTo(resumePositionMs);
} }
else else
@@ -1524,6 +1533,7 @@ class VideoDetailView : ConstraintLayout {
_lastVideoSource = videoSource; _lastVideoSource = videoSource;
_lastAudioSource = audioSource; _lastAudioSource = audioSource;
_lastSubtitleSource = subtitleSource;
} }
catch(ex: UnsupportedCastException) { catch(ex: UnsupportedCastException) {
Logger.e(TAG, "Failed to load cast media", ex); Logger.e(TAG, "Failed to load cast media", ex);
@@ -143,7 +143,7 @@ class MediaPlaybackService : Service() {
override fun onDestroy() { override fun onDestroy() {
Logger.v(TAG, "onDestroy"); Logger.v(TAG, "onDestroy");
_instance = null; _instance = null;
MediaControlReceiver.onCloseReceived.emit(); MediaControlReceiver.onPauseReceived.emit();
super.onDestroy(); super.onDestroy();
} }
@@ -153,7 +153,7 @@ class MediaPlaybackService : Service() {
fun closeMediaSession() { fun closeMediaSession() {
Logger.v(TAG, "closeMediaSession"); Logger.v(TAG, "closeMediaSession");
stopForeground(STOP_FOREGROUND_DETACH); stopForeground(STOP_FOREGROUND_REMOVE);
val focusRequest = _focusRequest; val focusRequest = _focusRequest;
if (focusRequest != null) { if (focusRequest != null) {
@@ -162,7 +162,9 @@ class MediaPlaybackService : Service() {
} }
_hasFocus = false; _hasFocus = false;
_notificationManager?.cancel(MEDIA_NOTIF_ID); val notifManager = _notificationManager;
Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
notifManager?.cancel(MEDIA_NOTIF_ID);
_notif_last_video = null; _notif_last_video = null;
_notif_last_bitmap = null; _notif_last_bitmap = null;
_mediaSession = null; _mediaSession = null;
@@ -5,6 +5,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.Color
import android.media.AudioManager import android.media.AudioManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
@@ -38,6 +39,7 @@ import com.futo.platformplayer.receivers.AudioNoisyReceiver
import com.futo.platformplayer.services.DownloadService import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -380,13 +382,15 @@ class StateApp {
Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]"); Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]");
StatePolycentric.instance.load(context); StatePolycentric.instance.load(context);
Logger.i(TAG, "MainApp Starting: Initializing [Saved]");
StateSaved.instance.load();
Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]"); Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]");
displayMetrics = context.resources.displayMetrics; displayMetrics = context.resources.displayMetrics;
ensureConnectivityManager(context); ensureConnectivityManager(context);
Logger.i(TAG, "MainApp Starting: Cleaning up unused downloads");
StateDownloads.instance.cleanupDownloads();
Logger.i(TAG, "MainApp Starting: Initializing [Telemetry]"); Logger.i(TAG, "MainApp Starting: Initializing [Telemetry]");
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
StateTelemetry.instance.initialize(); StateTelemetry.instance.initialize();
@@ -460,7 +464,9 @@ class StateApp {
//Foreground download //Foreground download
autoUpdateEnabled -> { autoUpdateEnabled -> {
StateUpdate.instance.checkForUpdates(context, false); scopeOrNull?.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(context, false)
}
} }
else -> { else -> {
@@ -558,6 +564,40 @@ class StateApp {
if(StateHistory.instance.shouldMigrateLegacyHistory()) if(StateHistory.instance.shouldMigrateLegacyHistory())
StateHistory.instance.migrateLegacyHistory(); StateHistory.instance.migrateLegacyHistory();
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
scopeOrNull?.launch(Dispatchers.IO) {
val updateAvailable = StatePlatform.instance.checkForUpdates()
withContext(Dispatchers.Main) {
if (updateAvailable.isNotEmpty()) {
UIDialogs.appToast(
ToastView.Toast(updateAvailable
.map { " - " + it.name }
.joinToString("\n"),
true,
null,
"Plugin updates available"
));
StateAnnouncement.instance.registerAnnouncement(
"plugin-update",
"Plugin updates available",
"There are ${updateAvailable.size} plugin updates available.",
AnnouncementType.SESSION_RECURRING
)
}
}
}
/*
UIDialogs.appToast("This is a test", false);
UIDialogs.appToast("This is a test 2", false);
UIDialogs.appToastError("This is a test 3 (Error)", false);
UIDialogs.appToast(ToastView.Toast("This is a test 4, with title", false, Color.WHITE, "Test title"));
UIDialogs.appToast("This is a test 5 Long text\nWith enters\nasdh asfh fds h rwe h fxh sdfh sdf h dsfh sdf hasdfhsdhg ads as", true);
*/
} }
fun mainAppStartedWithExternalFiles(context: Context) { fun mainAppStartedWithExternalFiles(context: Context) {
@@ -352,7 +352,10 @@ class StateDownloads {
fun cleanupDownloads(): Pair<Int, Long> { fun cleanupDownloads(): Pair<Int, Long> {
val expected = getDownloadedVideos(); val expected = getDownloadedVideos();
val validFiles = HashSet(expected.flatMap { e -> e.videoSource.map { it.filePath } + e.audioSource.map { it.filePath } }); val validFiles = HashSet(expected.flatMap { e ->
e.videoSource.map { it.filePath } +
e.audioSource.map { it.filePath } +
e.subtitlesSources.map { it.filePath }});
var totalDeleted: Long = 0; var totalDeleted: Long = 0;
var totalDeletedCount = 0; var totalDeletedCount = 0;
@@ -5,6 +5,7 @@ import androidx.collection.LruCache
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.PlatformMultiClientPool import com.futo.platformplayer.api.media.PlatformMultiClientPool
@@ -78,6 +79,7 @@ class StatePlatform {
private val _clientsLock = Object(); private val _clientsLock = Object();
private val _availableClients : ArrayList<IPlatformClient> = ArrayList(); private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
private val _enabledClients : ArrayList<IPlatformClient> = ArrayList(); private val _enabledClients : ArrayList<IPlatformClient> = ArrayList();
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
//ClientPools are used to isolate plugin usage of certain components from others //ClientPools are used to isolate plugin usage of certain components from others
//This prevents for example a background task like subscriptions from blocking a user from opening a video //This prevents for example a background task like subscriptions from blocking a user from opening a video
@@ -932,6 +934,67 @@ class StatePlatform {
} }
} }
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
val updatesAvailableMap = _updatesAvailableMap
synchronized(updatesAvailableMap) {
return updatesAvailableMap.contains(c.id)
}
}
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
var configs = mutableListOf<SourcePluginConfig>()
val updatesAvailableFor = hashSetOf<String>()
for (availableClient in getAvailableClients()) {
if (availableClient !is JSClient) {
continue
}
if (checkForUpdates(availableClient.config)) {
configs.add(availableClient.config);
updatesAvailableFor.add(availableClient.config.id)
}
}
_updatesAvailableMap = updatesAvailableFor
return@withContext configs;
}
fun clearUpdateAvailable(c: SourcePluginConfig) {
val updatesAvailableMap = _updatesAvailableMap
synchronized(updatesAvailableMap) {
updatesAvailableMap.remove(c.id)
}
}
private suspend fun checkForUpdates(c: SourcePluginConfig): Boolean = withContext(Dispatchers.IO) {
val sourceUrl = c.sourceUrl ?: return@withContext false;
Logger.i(TAG, "Check for source updates '${c.name}'.");
try {
val client = ManagedHttpClient();
val response = client.get(sourceUrl);
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
if (!response.isOk || response.body == null) {
return@withContext false;
}
val configJson = response.body.string();
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
val config = SourcePluginConfig.fromJson(configJson);
if (config.version <= c.version) {
return@withContext false;
}
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
return@withContext true;
} catch (e: Throwable) {
Logger.e(TAG, "Failed to check for updates.", e);
return@withContext false;
}
}
companion object { companion object {
private var _instance : StatePlatform? = null; private var _instance : StatePlatform? = null;
val instance : StatePlatform val instance : StatePlatform
@@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.developer.DeveloperEndpoints
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
@@ -467,7 +466,6 @@ class StatePlugins {
_plugins.save(descriptor); _plugins.save(descriptor);
} }
@Serializable @Serializable
private data class PluginConfig( private data class PluginConfig(
val SOURCES_EMBEDDED: Map<String, String>, val SOURCES_EMBEDDED: Map<String, String>,
@@ -1,52 +0,0 @@
package com.futo.platformplayer.states
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
@kotlinx.serialization.Serializable
data class VideoToOpen(val url: String, val timeSeconds: Long);
class StateSaved {
var videoToOpen: VideoToOpen? = null;
private val _videoToOpen = FragmentedStorage.get<StringStorage>("videoToOpen")
fun load() {
val videoToOpenString = _videoToOpen.value;
if (videoToOpenString.isNotEmpty()) {
try {
val v = Serializer.json.decodeFromString<VideoToOpen>(videoToOpenString);
videoToOpen = v;
} catch (e: Throwable) {
Logger.w(TAG, "Failed to load video to open", e)
}
}
Logger.i(TAG, "loaded videoToOpen=$videoToOpen");
}
fun setVideoToOpenNonBlocking(v: VideoToOpen? = null) {
Logger.i(TAG, "set videoToOpen=$v");
videoToOpen = v;
_videoToOpen.setAndSave(if (v != null) Serializer.json.encodeToString(v) else "");
}
fun setVideoToOpenBlocking(v: VideoToOpen? = null) {
Logger.i(TAG, "set videoToOpen=$v");
videoToOpen = v;
_videoToOpen.setAndSaveBlocking(if (v != null) Serializer.json.encodeToString(v) else "");
}
companion object {
const val TAG = "StateSaved"
val instance: StateSaved = StateSaved()
}
}
@@ -2,15 +2,15 @@ package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Environment import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.* import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.copyToOutputStream
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@@ -155,47 +155,45 @@ class StateUpdate {
} }
} }
fun checkForUpdates(context: Context, showUpToDateToast: Boolean) { suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean) = withContext(Dispatchers.IO) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { try {
try { val client = ManagedHttpClient();
val client = ManagedHttpClient(); val latestVersion = downloadVersionCode(client);
val latestVersion = downloadVersionCode(client);
if (latestVersion != null) { if (latestVersion != null) {
val currentVersion = BuildConfig.VERSION_CODE; val currentVersion = BuildConfig.VERSION_CODE;
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}."); Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
if (latestVersion > currentVersion) { if (latestVersion > currentVersion) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
try { try {
UIDialogs.showUpdateAvailableDialog(context, latestVersion); UIDialogs.showUpdateAvailableDialog(context, latestVersion);
} catch (e: Throwable) { } catch (e: Throwable) {
UIDialogs.toast(context, "Failed to show update dialog"); UIDialogs.toast(context, "Failed to show update dialog");
Logger.w(TAG, "Error occurred in update dialog."); Logger.w(TAG, "Error occurred in update dialog.");
}
}
} else {
if (showUpToDateToast) {
withContext(Dispatchers.Main) {
UIDialogs.toast(context, "Already on latest version");
}
} }
} }
} else { } else {
Logger.w(TAG, "Failed to retrieve version from version URL."); if (showUpToDateToast) {
withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { UIDialogs.toast(context, "Already on latest version");
UIDialogs.toast(context, "Failed to retrieve version"); }
} }
} }
} catch (e: Throwable) { } else {
Logger.w(TAG, "Failed to check for updates.", e); Logger.w(TAG, "Failed to retrieve version from version URL.");
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
UIDialogs.toast(context, "Failed to check for updates"); UIDialogs.toast(context, "Failed to retrieve version");
} }
} }
}; } catch (e: Throwable) {
Logger.w(TAG, "Failed to check for updates.", e);
withContext(Dispatchers.Main) {
UIDialogs.toast(context, "Failed to check for updates");
}
}
} }
private fun downloadApkToFile(client: ManagedHttpClient, destinationFile: File, isCancelled: (() -> Boolean)? = null) { private fun downloadApkToFile(client: ManagedHttpClient, destinationFile: File, isCancelled: (() -> Boolean)? = null) {
@@ -55,7 +55,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val clientCacheCount = clientTasks.value.size - clientTaskCount; val clientCacheCount = clientTasks.value.size - clientTaskCount;
val limit = clientTasks.key.getSubscriptionRateLimit(); val limit = clientTasks.key.getSubscriptionRateLimit();
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) { if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)"); UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
} }
} }
@@ -0,0 +1,91 @@
package com.futo.platformplayer.views
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import com.futo.platformplayer.R
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
class ToastView : LinearLayout {
private val root: LinearLayout;
private val title: TextView;
private val text: TextView;
init {
inflate(context, R.layout.toast, this);
root = findViewById(R.id.root);
title = findViewById(R.id.title);
text = findViewById(R.id.text);
}
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
setToast(ToastView.Toast("", false))
root.visibility = GONE;
}
fun hide(animate: Boolean, onFinished: (()->Unit)? = null) {
Logger.i("MainActivity", "Hiding toast");
if(!animate) {
root.visibility = GONE;
alpha = 0f;
onFinished?.invoke();
}
else {
animate()
.alpha(0f)
.setDuration(700)
.translationY(20.dp(context.resources).toFloat())
.withEndAction { root.visibility = GONE; onFinished?.invoke(); }
.start();
}
}
fun show(animate: Boolean) {
Logger.i("MainActivity", "Showing toast");
if(!animate) {
root.visibility = VISIBLE;
alpha = 1f;
}
else {
alpha = 0f;
root.visibility = VISIBLE;
translationY = 20.dp(context.resources).toFloat();
animate()
.alpha(1f)
.setDuration(700)
.translationY(0f)
.start();
}
}
fun setToast(toast: Toast) {
if(toast.title.isNullOrEmpty())
title.isVisible = false;
else {
title.text = toast.title;
title.isVisible = true;
}
text.text = toast.msg;
if(toast.color != null)
text.setTextColor(toast.color);
else
text.setTextColor(Color.WHITE);
}
fun setToastAnimated(toast: Toast) {
hide(true) {
setToast(toast);
show(true);
};
}
class Toast(
val msg: String,
val long: Boolean,
val color: Int? = null,
val title: String? = null
);
}
@@ -1,42 +0,0 @@
package com.futo.platformplayer.views.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.constructs.Event1
class DisabledSourceAdapter : RecyclerView.Adapter<DisabledSourceViewHolder> {
private val _sources: MutableList<IPlatformClient>;
var onClick = Event1<IPlatformClient>();
var onAdd = Event1<IPlatformClient>();
constructor(sources: MutableList<IPlatformClient>) : super() {
_sources = sources;
}
override fun getItemCount() = _sources.size
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): DisabledSourceViewHolder {
val holder = DisabledSourceViewHolder(viewGroup);
holder.onAdd.subscribe {
val source = holder.source;
if (source != null) {
onAdd.emit(source);
}
}
holder.onClick.subscribe {
val source = holder.source;
if (source != null) {
onClick.emit(source);
}
};
return holder;
}
override fun onBindViewHolder(viewHolder: DisabledSourceViewHolder, position: Int) {
viewHolder.bind(_sources[position])
}
}
@@ -1,17 +1,15 @@
package com.futo.platformplayer.views.adapters package com.futo.platformplayer.views.adapters
import android.content.Context import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.states.StatePlatform
class DisabledSourceView : LinearLayout { class DisabledSourceView : LinearLayout {
private val _root: LinearLayout; private val _root: LinearLayout;
@@ -38,7 +36,16 @@ class DisabledSourceView : LinearLayout {
client.icon?.setImageView(_imageSource); client.icon?.setImageView(_imageSource);
_textSource.text = client.name; _textSource.text = client.name;
_textSourceSubtitle.text = context.getString(R.string.tap_to_open);
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
_textSourceSubtitle.text = context.getString(R.string.update_available_exclamation)
_textSourceSubtitle.setTextColor(context.getColor(R.color.light_blue_400))
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_regular)
} else {
_textSourceSubtitle.text = context.getString(R.string.tap_to_open)
_textSourceSubtitle.setTextColor(context.getColor(R.color.gray_ac))
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_extra_light)
}
_buttonAdd.setOnClickListener { onAdd.emit(source) } _buttonAdd.setOnClickListener { onAdd.emit(source) }
_root.setOnClickListener { onClick.emit(); }; _root.setOnClickListener { onClick.emit(); };
@@ -1,44 +0,0 @@
package com.futo.platformplayer.views.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.constructs.Event0
class DisabledSourceViewHolder : ViewHolder {
private val _imageSource: ImageView;
private val _textSource: TextView;
private val _textSourceSubtitle: TextView;
private val _buttonAdd: LinearLayout;
var onClick = Event0();
var onAdd = Event0();
var source: IPlatformClient? = null
private set
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_source_disabled, viewGroup, false)) {
_imageSource = itemView.findViewById(R.id.image_source);
_textSource = itemView.findViewById(R.id.text_source);
_textSourceSubtitle = itemView.findViewById(R.id.text_source_subtitle);
_buttonAdd = itemView.findViewById(R.id.button_add);
val root = itemView.findViewById<LinearLayout>(R.id.root);
_buttonAdd.setOnClickListener { onAdd.emit() }
root.setOnClickListener { onClick.emit(); };
}
fun bind(client: IPlatformClient) {
client.icon?.setImageView(_imageSource);
_textSource.text = client.name;
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
source = client;
}
}
@@ -10,7 +10,9 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.states.StatePlatform
class EnabledSourceViewHolder : ViewHolder { class EnabledSourceViewHolder : ViewHolder {
private val _imageSource: ImageView; private val _imageSource: ImageView;
@@ -57,8 +59,18 @@ class EnabledSourceViewHolder : ViewHolder {
fun bind(client: IPlatformClient) { fun bind(client: IPlatformClient) {
client.icon?.setImageView(_imageSource); client.icon?.setImageView(_imageSource);
_textSource.text = client.name; _textSource.text = client.name
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
_textSourceSubtitle.text = itemView.context.getString(R.string.update_available_exclamation)
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.light_blue_400))
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
} else {
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open)
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.gray_ac))
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_extra_light)
}
source = client source = client
} }
} }
@@ -11,6 +11,7 @@ import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toFile import androidx.core.net.toFile
@@ -48,7 +49,7 @@ class ImageVariableOverlay: ConstraintLayout {
private val _buttonGallery: BigButton; private val _buttonGallery: BigButton;
private val _imageGallerySelected: ImageView; private val _imageGallerySelected: ImageView;
private val _imageGallerySelectedContainer: LinearLayout; private val _imageGallerySelectedContainer: LinearLayout;
private val _buttonSelect: Button; private val _buttonSelect: TextView;
private val _topbar: OverlayTopbar; private val _topbar: OverlayTopbar;
private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>; private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>;
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>; private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
@@ -25,6 +25,7 @@ class SourceHeaderView : LinearLayout {
private val _sourcePlatformUrl: TextView; private val _sourcePlatformUrl: TextView;
private val _sourceRepositoryUrl: TextView; private val _sourceRepositoryUrl: TextView;
private val _sourceScriptUrl: TextView; private val _sourceScriptUrl: TextView;
private val _sourceScriptConfig: TextView;
private val _sourceSignature: TextView; private val _sourceSignature: TextView;
private val _sourcePlatformUrlContainer: LinearLayout; private val _sourcePlatformUrlContainer: LinearLayout;
@@ -45,6 +46,7 @@ class SourceHeaderView : LinearLayout {
_sourcePlatformUrl = findViewById(R.id.source_platform); _sourcePlatformUrl = findViewById(R.id.source_platform);
_sourcePlatformUrlContainer = findViewById(R.id.source_platform_container); _sourcePlatformUrlContainer = findViewById(R.id.source_platform_container);
_sourceScriptUrl = findViewById(R.id.source_script); _sourceScriptUrl = findViewById(R.id.source_script);
_sourceScriptConfig = findViewById(R.id.source_config);
_sourceSignature = findViewById(R.id.source_signature); _sourceSignature = findViewById(R.id.source_signature);
_sourceBy.setOnClickListener { _sourceBy.setOnClickListener {
@@ -59,6 +61,10 @@ class SourceHeaderView : LinearLayout {
if(!_config?.absoluteScriptUrl.isNullOrEmpty()) if(!_config?.absoluteScriptUrl.isNullOrEmpty())
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.absoluteScriptUrl))); context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.absoluteScriptUrl)));
}; };
_sourceScriptConfig.setOnClickListener {
if(!_config?.sourceUrl.isNullOrEmpty())
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.sourceUrl)));
}
_sourcePlatformUrl.setOnClickListener { _sourcePlatformUrl.setOnClickListener {
if(!_config?.platformUrl.isNullOrEmpty()) if(!_config?.platformUrl.isNullOrEmpty())
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.platformUrl))); context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.platformUrl)));
@@ -82,6 +88,7 @@ class SourceHeaderView : LinearLayout {
_sourceVersion.text = config.version.toString(); _sourceVersion.text = config.version.toString();
_sourceScriptUrl.text = config.absoluteScriptUrl; _sourceScriptUrl.text = config.absoluteScriptUrl;
_sourceRepositoryUrl.text = config.repositoryUrl; _sourceRepositoryUrl.text = config.repositoryUrl;
_sourceScriptConfig.text = config.sourceUrl
_sourceAuthorID.text = ""; _sourceAuthorID.text = "";
_sourcePlatformUrl.text = config.platformUrl ?: ""; _sourcePlatformUrl.text = config.platformUrl ?: "";
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#EE202020" />
<corners android:radius="10dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>
+1 -2
View File
@@ -2,8 +2,7 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="960" android:viewportWidth="960"
android:viewportHeight="960" android:viewportHeight="960">
android:tint="?attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M120,640L120,560L440,560L440,640L120,640ZM120,480L120,400L600,400L600,480L120,480ZM120,320L120,240L600,240L600,320L120,320ZM640,840L640,520L880,680L640,840Z"/> android:pathData="M120,640L120,560L440,560L440,640L120,640ZM120,480L120,400L600,400L600,480L120,480ZM120,320L120,240L600,240L600,320L120,320ZM640,840L640,520L880,680L640,840Z"/>
@@ -70,4 +70,13 @@
android:visibility="gone" android:visibility="gone"
android:elevation="15dp"> android:elevation="15dp">
</FrameLayout> </FrameLayout>
<com.futo.platformplayer.views.ToastView
android:id="@+id/toast_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="50dp"
android:elevation="30dp"
app:layout_constraintLeft_toLeftOf="@id/fragment_main"
app:layout_constraintRight_toRightOf="@id/fragment_main"
app:layout_constraintBottom_toBottomOf="@id/fragment_main" />
</androidx.constraintlayout.motion.widget.MotionLayout> </androidx.constraintlayout.motion.widget.MotionLayout>
@@ -38,12 +38,12 @@
android:id="@+id/dialog_text_details" android:id="@+id/dialog_text_details"
android:layout_width="match_parent" android:layout_width="match_parent"
android:textColor="#AAAAAA" android:textColor="#AAAAAA"
android:fontFamily="@font/inter_regular" android:fontFamily="@font/inter_light"
android:text="" android:text=""
android:textAlignment="center" android:textAlignment="center"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:layout_marginEnd="30dp" android:layout_marginEnd="30dp"
android:textSize="11dp" android:textSize="13dp"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<TextView <TextView
android:id="@+id/dialog_text_code" android:id="@+id/dialog_text_code"
@@ -190,7 +190,7 @@
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="5dp" android:layout_marginBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toBottomOf="parent">
<TextView <TextView
@@ -38,7 +38,7 @@
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="5dp" android:layout_marginBottom="10dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" android:layout_height="50dp"
android:background="@drawable/background_button_primary" android:background="@drawable/background_button_primary"
@@ -116,20 +116,29 @@
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
<LinearLayout
<FrameLayout
android:id="@+id/container_select" android:id="@+id/container_select"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="50dp"
android:background="@drawable/background_button_primary"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"> app:layout_constraintRight_toRightOf="parent">
<Button
<TextView
android:id="@+id/button_select" android:id="@+id/button_select"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_margin="10dp" android:fontFamily="@font/inter_regular"
android:background="@drawable/background_button_primary" android:text="@string/select"
android:text="Select" /> android:textSize="16dp"
</LinearLayout> android:gravity="center"
android:layout_gravity="center" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:toolNs="http://schemas.android.com/tools"
android:orientation="vertical"
android:id="@+id/root"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/background_toast"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:textColor="@color/white"
toolNs:text="Some Title"
android:fontFamily="@font/inter_bold"
android:textSize="15dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/text"
android:textColor="@color/white"
android:textSize="14dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
toolNs:text="This is a test" />
</LinearLayout>
</LinearLayout>
@@ -170,6 +170,28 @@
tools:text="https://some.repository.url/whatever/someScript.js" /> tools:text="https://some.repository.url/whatever/someScript.js" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14dp"
android:textColor="@color/white"
android:layout_marginTop="10dp"
android:fontFamily="@font/inter_light"
android:text="@string/config_url" />
<TextView
android:id="@+id/source_config"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14dp"
android:textColor="@color/colorPrimary"
android:fontFamily="@font/inter_extra_light"
tools:text="https://some.repository.url/whatever/someScript.js" />
</LinearLayout>
<!--Script Url--> <!--Script Url-->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
+3 -1
View File
@@ -113,6 +113,7 @@
<string name="platform_url">Platform URL</string> <string name="platform_url">Platform URL</string>
<string name="repository_url">Repository URL</string> <string name="repository_url">Repository URL</string>
<string name="script_url">Script URL</string> <string name="script_url">Script URL</string>
<string name="config_url">Config URL</string>
<string name="source_permissions_explanation">These are the permissions the plugin requires to function</string> <string name="source_permissions_explanation">These are the permissions the plugin requires to function</string>
<string name="source_explain_eval_access">The plugin will have access to eval capacity</string> <string name="source_explain_eval_access">The plugin will have access to eval capacity</string>
<string name="source_explain_script_url">The plugin will have access to the following domains</string> <string name="source_explain_script_url">The plugin will have access to the following domains</string>
@@ -651,6 +652,7 @@
<string name="please_use_at_least_3_characters">Please use at least 3 characters</string> <string name="please_use_at_least_3_characters">Please use at least 3 characters</string>
<string name="are_you_sure_you_want_to_delete_this_video">Are you sure you want to delete this video?</string> <string name="are_you_sure_you_want_to_delete_this_video">Are you sure you want to delete this video?</string>
<string name="tap_to_open">Tap to open</string> <string name="tap_to_open">Tap to open</string>
<string name="update_available_exclamation">Update available!</string>
<string name="watching">watching</string> <string name="watching">watching</string>
<string name="available_in">available in</string> <string name="available_in">available in</string>
<string name="seconds">seconds</string> <string name="seconds">seconds</string>
@@ -724,7 +726,7 @@
<string name="position">Position</string> <string name="position">Position</string>
<string name="tutorials">Tutorials</string> <string name="tutorials">Tutorials</string>
<string name="do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button">Do you want to see the tutorials? You can find them at any time through the more button.</string> <string name="do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button">Do you want to see the tutorials? You can find them at any time through the more button.</string>
<string name="add_creator">Add More</string> <string name="add_creator">Add Creators</string>
<string name="select">Select</string> <string name="select">Select</string>
<string-array name="home_screen_array"> <string-array name="home_screen_array">
<item>Recommendations</item> <item>Recommendations</item>