mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 781d0797e7 | |||
| ec12a06b88 | |||
| bf3e8867c3 | |||
| 176814a715 | |||
| 898637a616 | |||
| f1860126a7 | |||
| f8402676d7 | |||
| cf86ce1ab3 | |||
| f4cb1719e0 | |||
| 4898cb53ae | |||
| 0f60d4737e | |||
| 0dc33e1f2b | |||
| d86a997a88 | |||
| 34d4d92289 | |||
| 4cb1bf268f | |||
| 8488706ff9 | |||
| a348bb2662 | |||
| 60a17b3c67 | |||
| 386c58d4ad | |||
| 356ba01dc1 | |||
| ed2aa848da | |||
| c5dd90048f | |||
| ab04f334dc | |||
| 0d44f8a416 | |||
| d01a1545e2 | |||
| e599729ba1 | |||
| 3ac043740e | |||
| 89603d0ff3 | |||
| 05b6cd7c97 | |||
| ea5aad0631 | |||
| 96e034b9bf | |||
| 6141c36855 | |||
| 4084ab3ed0 | |||
| 34e733823a | |||
| f1d01642cd | |||
| d5551d7118 | |||
| d079a1e8e4 | |||
| c06c00ee9b | |||
| 1d8eababc2 | |||
| 75cf1ffbdd | |||
| 5499706a9b | |||
| ba57e32920 | |||
| df96c5b51c | |||
| 75f81d20db | |||
| 3fc92e4065 | |||
| 8ffd5f411f | |||
| 918161a299 | |||
| 9f50f72eaa | |||
| 2f66f124aa | |||
| 9a11717cf4 |
+1
-1
@@ -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)
|
||||
|
||||
//JS
|
||||
implementation("com.caoccao.javet:javet-android:2.2.1")
|
||||
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||
|
||||
//Exoplayer
|
||||
implementation 'androidx.media3:media3-exoplayer:1.2.0'
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.FutoVideo"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="31"
|
||||
android:largeHeap="true">
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="@string/authority"
|
||||
|
||||
+120
-46
@@ -1,13 +1,37 @@
|
||||
|
||||
declare class ScriptException extends Error {
|
||||
//If only one parameter is provided, acts as msg
|
||||
constructor(type: string, msg: string);
|
||||
}
|
||||
declare class TimeoutException extends ScriptException {
|
||||
|
||||
declare class LoginRequiredException extends ScriptException {
|
||||
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 {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class AgeException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class TimeoutException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class ScriptImplementationException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
@@ -38,16 +62,23 @@ declare class FilterCapability {
|
||||
|
||||
|
||||
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 {
|
||||
id: PlatformID,
|
||||
name: string,
|
||||
thumbnails: Thumbnails,
|
||||
author: PlatformAuthorLink,
|
||||
datetime: integer,
|
||||
url: string
|
||||
}
|
||||
declare interface PlatformContent {}
|
||||
|
||||
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
|
||||
contentUrl: string,
|
||||
contentName: string?,
|
||||
@@ -59,16 +90,26 @@ declare class PlatformNestedMediaContent {
|
||||
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 {
|
||||
thumbnails: Thumbnails,
|
||||
author: PlatformAuthorLink,
|
||||
|
||||
duration: int,
|
||||
viewCount: long,
|
||||
isLive: boolean
|
||||
isLive: boolean,
|
||||
shareUrl: string?
|
||||
}
|
||||
declare interface PlatformContent {}
|
||||
|
||||
declare class PlatformVideo implements PlatformContent {
|
||||
constructor(obj: PlatformVideoDef);
|
||||
}
|
||||
@@ -77,14 +118,15 @@ declare class PlatformVideo implements PlatformContent {
|
||||
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
|
||||
description: string,
|
||||
video: VideoSourceDescriptor,
|
||||
live: SubtitleSource[],
|
||||
rating: IRating
|
||||
live: IVideoSource,
|
||||
rating: IRating,
|
||||
subtitles: SubtitleSource[]
|
||||
}
|
||||
declare class PlatformVideoDetails extends PlatformVideo {
|
||||
constructor(obj: PlatformVideoDetailsDef);
|
||||
}
|
||||
|
||||
declare class PlatformPostDef extends PlatformContentDef {
|
||||
declare interface PlatformPostDef extends PlatformContentDef {
|
||||
thumbnails: string[],
|
||||
images: string[],
|
||||
description: string
|
||||
@@ -93,7 +135,7 @@ declare class PlatformPost extends PlatformContent {
|
||||
constructor(obj: PlatformPostDef)
|
||||
}
|
||||
|
||||
declare class PlatformPostDetailsDef extends PlatformPostDef {
|
||||
declare interface PlatformPostDetailsDef extends PlatformPostDef {
|
||||
rating: IRating,
|
||||
textType: int,
|
||||
content: String
|
||||
@@ -110,8 +152,8 @@ declare interface MuxVideoSourceDescriptorDef {
|
||||
isUnMuxed: boolean,
|
||||
videoSources: VideoSource[]
|
||||
}
|
||||
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor {
|
||||
constructor(obj: VideoSourceDescriptorDef);
|
||||
declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
|
||||
constructor(videoSourcesOrObj: VideoSource[]);
|
||||
}
|
||||
|
||||
declare interface UnMuxVideoSourceDescriptorDef {
|
||||
@@ -129,7 +171,7 @@ declare interface IVideoSource {
|
||||
declare interface IAudioSource {
|
||||
|
||||
}
|
||||
interface VideoUrlSourceDef implements IVideoSource {
|
||||
declare interface VideoUrlSourceDef implements IVideoSource {
|
||||
width: integer,
|
||||
height: integer,
|
||||
container: string,
|
||||
@@ -139,22 +181,22 @@ interface VideoUrlSourceDef implements IVideoSource {
|
||||
duration: integer,
|
||||
url: string
|
||||
}
|
||||
class VideoUrlSource {
|
||||
declare class VideoUrlSource {
|
||||
constructor(obj: VideoUrlSourceDef);
|
||||
|
||||
getRequestModifier(): RequestModifier?;
|
||||
}
|
||||
interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
||||
declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
||||
itagId: integer,
|
||||
initStart: integer,
|
||||
initEnd: integer,
|
||||
indexStart: integer,
|
||||
indexEnd: integer,
|
||||
}
|
||||
class VideoUrlRangeSource extends VideoUrlSource {
|
||||
declare class VideoUrlRangeSource extends VideoUrlSource {
|
||||
constructor(obj: YTVideoSourceDef);
|
||||
}
|
||||
interface AudioUrlSourceDef {
|
||||
declare interface AudioUrlSourceDef {
|
||||
name: string,
|
||||
bitrate: integer,
|
||||
container: string,
|
||||
@@ -163,24 +205,12 @@ interface AudioUrlSourceDef {
|
||||
url: string,
|
||||
language: string
|
||||
}
|
||||
class AudioUrlSource implements IAudioSource {
|
||||
declare class AudioUrlSource implements IAudioSource {
|
||||
constructor(obj: AudioUrlSourceDef);
|
||||
|
||||
getRequestModifier(): RequestModifier?;
|
||||
}
|
||||
interface IRequest {
|
||||
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 {
|
||||
declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||
itagId: integer,
|
||||
initStart: integer,
|
||||
initEnd: integer,
|
||||
@@ -188,28 +218,44 @@ interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||
indexEnd: integer,
|
||||
audioChannels: integer
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
declare class AudioUrlRangeSource extends AudioUrlSource {
|
||||
constructor(obj: AudioUrlRangeSourceDef);
|
||||
}
|
||||
interface HLSSourceDef {
|
||||
declare interface HLSSourceDef {
|
||||
name: string,
|
||||
duration: integer,
|
||||
url: string
|
||||
url: string,
|
||||
priority: boolean?,
|
||||
language: string?
|
||||
}
|
||||
class HLSSource implements IVideoSource {
|
||||
declare class HLSSource implements IVideoSource {
|
||||
constructor(obj: HLSSourceDef);
|
||||
}
|
||||
interface DashSourceDef {
|
||||
declare interface DashSourceDef {
|
||||
name: string,
|
||||
duration: integer,
|
||||
url: string
|
||||
url: string,
|
||||
language: string?
|
||||
}
|
||||
class DashSource implements IVideoSource {
|
||||
declare class DashSource implements IVideoSource {
|
||||
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
|
||||
interface PlatformChannelDef {
|
||||
declare interface PlatformChannelDef {
|
||||
id: PlatformID,
|
||||
name: string,
|
||||
thumbnail: string,
|
||||
@@ -217,12 +263,29 @@ interface PlatformChannelDef {
|
||||
subscribers: integer,
|
||||
description: string,
|
||||
url: string,
|
||||
urlAlternatives: string[],
|
||||
links: Map<string>?
|
||||
}
|
||||
class PlatformChannel {
|
||||
declare class PlatformChannel {
|
||||
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
|
||||
interface IRating {
|
||||
type: integer
|
||||
@@ -250,7 +313,11 @@ declare class PlatformComment {
|
||||
constructor(obj: CommentDef);
|
||||
}
|
||||
|
||||
declare class PlaybackTracker {
|
||||
constructor(interval: integer);
|
||||
|
||||
setProgress(seconds: integer);
|
||||
}
|
||||
|
||||
declare class LiveEventPager {
|
||||
nextRequest = 4000;
|
||||
@@ -261,8 +328,8 @@ declare class LiveEventPager {
|
||||
nextPage(): LiveEventPager; //Could be self
|
||||
}
|
||||
|
||||
class LiveEvent {
|
||||
type: String
|
||||
declare class LiveEvent {
|
||||
constructor(type: integer);
|
||||
}
|
||||
declare class LiveEventComment extends LiveEvent {
|
||||
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
|
||||
@@ -287,25 +354,31 @@ declare class ContentPager {
|
||||
constructor(results: PlatformContent[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean
|
||||
nextPage(): VideoPager; //Could be self
|
||||
nextPage(): ContentPager?; //Could be self
|
||||
}
|
||||
declare class VideoPager {
|
||||
constructor(results: PlatformVideo[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean
|
||||
nextPage(): VideoPager; //Could be self
|
||||
nextPage(): VideoPager?; //Could be self
|
||||
}
|
||||
declare class ChannelPager {
|
||||
constructor(results: PlatformChannel[], hasMore: 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 {
|
||||
constructor(results: PlatformComment[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean
|
||||
nextPage(): CommentPager; //Could be self
|
||||
nextPage(): CommentPager?; //Could be self
|
||||
}
|
||||
|
||||
interface Map<T> {
|
||||
@@ -341,8 +414,9 @@ interface Source {
|
||||
getChannelCapabilities(): ResultCapabilities;
|
||||
|
||||
isContentDetailsUrl(url: string): boolean;
|
||||
getContentDetails(url: string): PlatformVideoDetails;
|
||||
getContentDetails(url: string): PlatformContentDetails;
|
||||
|
||||
//Optional
|
||||
getLiveEvents(url: string): LiveEventPager;
|
||||
|
||||
//Optional
|
||||
|
||||
@@ -78,6 +78,11 @@ class ScriptLoginRequiredException extends ScriptException {
|
||||
super("ScriptLoginRequiredException", msg);
|
||||
}
|
||||
}
|
||||
class LoginRequiredException extends ScriptException {
|
||||
constructor(msg) {
|
||||
super("ScriptLoginRequiredException", msg);
|
||||
}
|
||||
}
|
||||
class CaptchaRequiredException extends Error {
|
||||
constructor(url, body) {
|
||||
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
||||
@@ -249,8 +254,8 @@ class PlatformVideoDetails extends PlatformVideo {
|
||||
|
||||
this.description = obj.description ?? "";//String
|
||||
this.video = obj.video ?? {}; //VideoSourceDescriptor
|
||||
this.dash = obj.dash ?? null; //DashSource
|
||||
this.hls = obj.hls ?? null; //HLSSource
|
||||
this.dash = obj.dash ?? null; //DashSource, deprecated
|
||||
this.hls = obj.hls ?? null; //HLSSource, deprecated
|
||||
this.live = obj.live ?? null; //VideoSource
|
||||
|
||||
this.rating = obj.rating ?? null; //IRating
|
||||
@@ -321,6 +326,8 @@ class VideoUrlSource {
|
||||
this.bitrate = obj.bitrate ?? 0;
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class VideoUrlRangeSource extends VideoUrlSource {
|
||||
@@ -346,6 +353,8 @@ class AudioUrlSource {
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
this.language = obj.language ?? Language.UNKNOWN;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
@@ -371,6 +380,8 @@ class HLSSource {
|
||||
this.priority = obj.priority ?? false;
|
||||
if(obj.language)
|
||||
this.language = obj.language;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class DashSource {
|
||||
@@ -382,13 +393,15 @@ class DashSource {
|
||||
this.url = obj.url;
|
||||
if(obj.language)
|
||||
this.language = obj.language;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
|
||||
class RequestModifier {
|
||||
constructor(obj) {
|
||||
obj = obj ?? {};
|
||||
this.allowByteSkip = obj.allowByteSkip;
|
||||
this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.util.Log
|
||||
import com.google.common.base.CharMatcher
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
@@ -9,7 +10,6 @@ import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
private const val IPV4_PART_COUNT = 4;
|
||||
@@ -216,15 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
||||
}
|
||||
|
||||
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
val timeout = 2000
|
||||
|
||||
if (addresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (addresses.size == 1) {
|
||||
val socket = Socket()
|
||||
|
||||
try {
|
||||
return Socket(addresses[0], port);
|
||||
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
||||
} catch (e: Throwable) {
|
||||
//Ignored.
|
||||
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||
socket.close()
|
||||
}
|
||||
|
||||
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) {
|
||||
if (connectedSocket == null) {
|
||||
@@ -263,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
//Ignore
|
||||
Log.i("getConnectedSocket", "Failed to connect to: $address", e)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -685,7 +685,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||
fun manualCheck() {
|
||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
StateUpdate.instance.checkForUpdates(it, true);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
StateUpdate.instance.checkForUpdates(it, true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -398,13 +399,28 @@ class UIDialogs {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
StateApp.withContext {
|
||||
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
|
||||
toast(it, text, long);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
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) {
|
||||
//TODO: Is not actually clickable...
|
||||
|
||||
@@ -343,7 +343,7 @@ class UISlideOverlays {
|
||||
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||
) as IVideoUrlSource;
|
||||
) as IVideoUrlSource?;
|
||||
}
|
||||
|
||||
if (audioSources != null) {
|
||||
|
||||
@@ -216,8 +216,10 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
|
||||
fun install(config: SourcePluginConfig, script: String) {
|
||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||
if(it)
|
||||
if(it) {
|
||||
StatePlatform.instance.clearUpdateAvailable(config)
|
||||
backToSources();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.Logging
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.SourcePluginAuthConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
@@ -39,6 +40,7 @@ class LoginActivity : AppCompatActivity() {
|
||||
_textUrl = findViewById(R.id.text_url);
|
||||
_buttonClose = findViewById(R.id.button_close);
|
||||
_buttonClose.setOnClickListener {
|
||||
UIDialogs.toast("Login cancelled", false);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -45,6 +45,7 @@ import com.futo.platformplayer.states.*
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.*
|
||||
@@ -54,6 +55,7 @@ import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
@@ -65,6 +67,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
lateinit var rootView : MotionLayout;
|
||||
|
||||
private lateinit var _overlayContainer: FrameLayout;
|
||||
private lateinit var _toastView: ToastView;
|
||||
|
||||
//Segment Containers
|
||||
private lateinit var _fragContainerTopBar: FragmentContainerView;
|
||||
@@ -207,7 +210,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
|
||||
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
|
||||
_overlayContainer = findViewById(R.id.overlay_container);
|
||||
//_overlayContainer.visibility = View.GONE;
|
||||
_toastView = findViewById(R.id.toast_view);
|
||||
|
||||
//Initialize fragments
|
||||
|
||||
@@ -478,21 +481,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
|
||||
_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() {
|
||||
@@ -864,7 +852,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_orientationManager.disable();
|
||||
|
||||
StateApp.instance.mainAppDestroyed(this);
|
||||
StateSaved.instance.setVideoToOpenBlocking(null);
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
+1
@@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.modifier
|
||||
interface IModifierOptions {
|
||||
val applyAuthClient: String?;
|
||||
val applyCookieClient: String?;
|
||||
val applyOtherHeaders: Boolean;
|
||||
}
|
||||
+1
@@ -11,4 +11,5 @@ class SourcePluginAuthConfig(
|
||||
val userAgent: String? = null,
|
||||
val loginButton: String? = null,
|
||||
val domainHeadersToFind: Map<String, List<String>>? = null,
|
||||
val loginWarning: String? = null
|
||||
) { }
|
||||
+23
-5
@@ -23,21 +23,31 @@ class JSRequest : IRequest {
|
||||
_v8Options = options;
|
||||
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 config = plugin.config;
|
||||
_v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
|
||||
_v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
|
||||
_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);
|
||||
}
|
||||
|
||||
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
|
||||
val config = plugin.config;
|
||||
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.applyCookieClient != null && url != null) {
|
||||
@@ -68,10 +78,18 @@ class JSRequest : IRequest {
|
||||
class Options: IModifierOptions {
|
||||
override val applyAuthClient: 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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ class JSRequestModifier: IRequestModifier {
|
||||
} as V8ValueObject;
|
||||
|
||||
val req = JSRequest(_plugin, result, url, headers);
|
||||
result.close();
|
||||
return req;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ abstract class JSSource {
|
||||
this.type = type;
|
||||
|
||||
_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");
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ class AirPlayCastingDevice : CastingDevice {
|
||||
try {
|
||||
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
||||
if (connectedSocket == null) {
|
||||
delay(3000);
|
||||
delay(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import org.json.JSONObject
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocket
|
||||
@@ -303,17 +305,18 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
_thread = Thread {
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
|
||||
var connectedSocket: Socket? = null
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
||||
if (connectedSocket == null) {
|
||||
Thread.sleep(3000);
|
||||
val resultSocket = getConnectedSocket(adrs.toList(), port);
|
||||
if (resultSocket == null) {
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
connectedSocket = resultSocket
|
||||
usedRemoteAddress = connectedSocket.inetAddress;
|
||||
localAddress = connectedSocket.localAddress;
|
||||
connectedSocket.close();
|
||||
break;
|
||||
} catch (e: Throwable) {
|
||||
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 address = InetSocketAddress(usedRemoteAddress, port)
|
||||
|
||||
//Connection loop
|
||||
while (_scopeIO?.isActive == true) {
|
||||
Logger.i(TAG, "Connecting to Chromecast.");
|
||||
@@ -332,7 +337,16 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
try {
|
||||
_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();
|
||||
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);
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -363,7 +377,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
_socket?.close();
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -415,7 +429,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
Logger.i(TAG, "Socket disconnected.");
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped connection loop.");
|
||||
@@ -432,10 +446,11 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
|
||||
Thread.sleep(5000);
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send ping.");
|
||||
}
|
||||
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped ping loop.");
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
|
||||
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
|
||||
import com.futo.platformplayer.casting.models.FCastVersionMessage
|
||||
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
|
||||
import com.futo.platformplayer.ensureNotMainThread
|
||||
import com.futo.platformplayer.getConnectedSocket
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
@@ -27,11 +28,12 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.math.BigInteger
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPair
|
||||
@@ -81,12 +83,15 @@ class FCastCastingDevice : CastingDevice {
|
||||
var port: Int = 0;
|
||||
|
||||
private var _socket: Socket? = null;
|
||||
private var _outputStream: DataOutputStream? = null;
|
||||
private var _inputStream: DataInputStream? = null;
|
||||
private var _outputStream: OutputStream? = null;
|
||||
private var _inputStream: InputStream? = null;
|
||||
private var _scopeIO: CoroutineScope? = null;
|
||||
private var _started: Boolean = false;
|
||||
private var _version: Long = 1;
|
||||
private var _thread: Thread? = null
|
||||
private var _pingThread: Thread? = null
|
||||
private var _lastPongTime = -1L
|
||||
private var _outputStreamLock = Object()
|
||||
|
||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||
this.name = name;
|
||||
@@ -206,7 +211,13 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
|
||||
if(Looper.getMainLooper().thread == Thread.currentThread()) {
|
||||
_scopeIO?.launch { action(); }
|
||||
_scopeIO?.launch {
|
||||
try {
|
||||
action();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to invoke in IO scope.", e)
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -240,77 +251,113 @@ class FCastCastingDevice : CastingDevice {
|
||||
val adrs = addresses ?: return;
|
||||
|
||||
val thread = _thread
|
||||
if (thread == null || !thread.isAlive) {
|
||||
Log.i(TAG, "Restarting thread because the thread has died")
|
||||
val pingThread = _pingThread
|
||||
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);
|
||||
|
||||
_thread = Thread {
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Log.i(TAG, "Connection thread started.")
|
||||
|
||||
var connectedSocket: Socket? = null
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
||||
if (connectedSocket == null) {
|
||||
Thread.sleep(3000);
|
||||
Log.i(TAG, "getConnectedSocket (adrs = [ ${adrs.joinToString(", ")} ], port = ${port}).")
|
||||
|
||||
val resultSocket = getConnectedSocket(adrs.toList(), port);
|
||||
|
||||
if (resultSocket == null) {
|
||||
Log.i(TAG, "Connection failed, waiting 1 seconds.")
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
usedRemoteAddress = connectedSocket.inetAddress;
|
||||
localAddress = connectedSocket.localAddress;
|
||||
connectedSocket.close();
|
||||
Log.i(TAG, "Connection succeeded.")
|
||||
|
||||
connectedSocket = resultSocket
|
||||
usedRemoteAddress = connectedSocket.inetAddress
|
||||
localAddress = connectedSocket.localAddress
|
||||
break;
|
||||
} 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
|
||||
while (_scopeIO?.isActive == true) {
|
||||
Logger.i(TAG, "Connecting to FastCast.");
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
|
||||
try {
|
||||
_socket = Socket(usedRemoteAddress, port);
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.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");
|
||||
|
||||
_outputStream = DataOutputStream(_socket?.outputStream);
|
||||
_inputStream = DataInputStream(_socket?.inputStream);
|
||||
_outputStream = _socket?.outputStream;
|
||||
_inputStream = _socket?.inputStream;
|
||||
} catch (e: IOException) {
|
||||
_socket?.close();
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
Logger.i(TAG, "Failed to connect to FastCast.", e);
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
localAddress = _socket?.localAddress;
|
||||
connectionState = CastConnectionState.CONNECTED;
|
||||
_lastPongTime = -1L
|
||||
|
||||
val buffer = ByteArray(4096);
|
||||
|
||||
Logger.i(TAG, "Started receiving.");
|
||||
var exceptionOccurred = false;
|
||||
while (_scopeIO?.isActive == true && !exceptionOccurred) {
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
val inputStream = _inputStream ?: break;
|
||||
Log.d(TAG, "Receiving next packet...");
|
||||
val b1 = inputStream.readUnsignedByte();
|
||||
val b2 = inputStream.readUnsignedByte();
|
||||
val b3 = inputStream.readUnsignedByte();
|
||||
val b4 = inputStream.readUnsignedByte();
|
||||
val size = ((b4.toLong() shl 24) or (b3.toLong() shl 16) or (b2.toLong() shl 8) or b1.toLong()).toInt();
|
||||
|
||||
var headerBytesRead = 0
|
||||
while (headerBytesRead < 4) {
|
||||
val read = inputStream.read(buffer, headerBytesRead, 4 - headerBytesRead)
|
||||
if (read == -1)
|
||||
throw Exception("Stream closed")
|
||||
headerBytesRead += read
|
||||
}
|
||||
|
||||
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) {
|
||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||
inputStream.skip(size.toLong());
|
||||
continue;
|
||||
Logger.w(TAG, "Packets larger than $size bytes are not supported.")
|
||||
break
|
||||
}
|
||||
|
||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||
inputStream.read(buffer, 0, size);
|
||||
var bytesRead = 0
|
||||
while (bytesRead < size) {
|
||||
val read = inputStream.read(buffer, bytesRead, size - bytesRead)
|
||||
if (read == -1)
|
||||
throw Exception("Stream closed")
|
||||
bytesRead += read
|
||||
}
|
||||
|
||||
val messageBytes = buffer.sliceArray(IntRange(0, size));
|
||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||
@@ -324,31 +371,68 @@ class FCastCastingDevice : CastingDevice {
|
||||
try {
|
||||
handleMessage(Opcode.find(opcode), json);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to handle message.", e);
|
||||
Logger.w(TAG, "Failed to handle message.", e)
|
||||
break
|
||||
}
|
||||
} catch (e: java.net.SocketException) {
|
||||
Logger.e(TAG, "Socket exception while receiving.", e);
|
||||
exceptionOccurred = true;
|
||||
break
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Exception while receiving.", e);
|
||||
exceptionOccurred = true;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_socket?.close();
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
Logger.i(TAG, "Socket disconnected.");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to close socket.", e)
|
||||
}
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped connection loop.");
|
||||
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.")
|
||||
|
||||
try {
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to close socket.", e)
|
||||
}
|
||||
}
|
||||
|
||||
/*if (_lastPongTime != -1L && System.currentTimeMillis() - _lastPongTime > 6000) {
|
||||
Logger.w(TAG, "Closing socket due to last pong time being larger than 6 seconds.")
|
||||
|
||||
try {
|
||||
_socket?.close()
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to close socket.", e)
|
||||
}
|
||||
}*/
|
||||
|
||||
Thread.sleep(2000)
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped ping loop.");
|
||||
}.apply { start() }
|
||||
} else {
|
||||
Log.i(TAG, "Thread was still alive, not restarted")
|
||||
}
|
||||
@@ -401,39 +485,44 @@ class FCastCastingDevice : CastingDevice {
|
||||
Logger.i(TAG, "Remote version received: $version")
|
||||
}
|
||||
Opcode.Ping -> send(Opcode.Pong)
|
||||
Opcode.Pong -> _lastPongTime = System.currentTimeMillis()
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun send(opcode: Opcode, message: String? = null) {
|
||||
try {
|
||||
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
||||
val size = 1 + data.size
|
||||
val outputStream = _outputStream
|
||||
if (outputStream == null) {
|
||||
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
|
||||
return
|
||||
ensureNotMainThread()
|
||||
|
||||
synchronized (_outputStreamLock) {
|
||||
try {
|
||||
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
||||
val size = 1 + data.size
|
||||
val outputStream = _outputStream
|
||||
if (outputStream == null) {
|
||||
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
|
||||
return
|
||||
}
|
||||
|
||||
val serializedSizeLE = ByteArray(4)
|
||||
serializedSizeLE[0] = (size and 0xff).toByte()
|
||||
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
|
||||
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
|
||||
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
|
||||
outputStream.write(serializedSizeLE)
|
||||
|
||||
val opcodeBytes = ByteArray(1)
|
||||
opcodeBytes[0] = opcode.value
|
||||
outputStream.write(opcodeBytes)
|
||||
|
||||
if (data.isNotEmpty()) {
|
||||
outputStream.write(data)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to send message.", e)
|
||||
throw e
|
||||
}
|
||||
|
||||
val serializedSizeLE = ByteArray(4)
|
||||
serializedSizeLE[0] = (size and 0xff).toByte()
|
||||
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
|
||||
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
|
||||
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
|
||||
outputStream.write(serializedSizeLE)
|
||||
|
||||
val opcodeBytes = ByteArray(1)
|
||||
opcodeBytes[0] = opcode.value
|
||||
outputStream.write(opcodeBytes)
|
||||
|
||||
if (data.isNotEmpty()) {
|
||||
outputStream.write(data)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to send message.", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,6 +542,7 @@ class FCastCastingDevice : CastingDevice {
|
||||
_started = false;
|
||||
//TODO: Kill and/or join thread?
|
||||
_thread = null;
|
||||
_pingThread = null;
|
||||
|
||||
val socket = _socket;
|
||||
val scopeIO = _scopeIO;
|
||||
@@ -462,6 +552,8 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
scopeIO.launch {
|
||||
socket.close();
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
connectionState = CastConnectionState.DISCONNECTED;
|
||||
scopeIO.cancel();
|
||||
Logger.i(TAG, "Cancelled scopeIO with open socket.")
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -26,8 +27,8 @@ import kotlinx.coroutines.launch
|
||||
class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
private lateinit var _imageLoader: ImageView;
|
||||
private lateinit var _buttonClose: Button;
|
||||
private lateinit var _buttonAdd: Button;
|
||||
private lateinit var _buttonScanQR: Button;
|
||||
private lateinit var _buttonAdd: ImageButton;
|
||||
private lateinit var _buttonScanQR: ImageButton;
|
||||
private lateinit var _textNoDevicesFound: TextView;
|
||||
private lateinit var _textNoDevicesRemembered: TextView;
|
||||
private lateinit var _recyclerDevices: RecyclerView;
|
||||
|
||||
@@ -143,7 +143,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
||||
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;
|
||||
@@ -185,8 +187,10 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
_sliderPosition.valueFrom = 0.0f;
|
||||
_sliderVolume.valueFrom = 0.0f;
|
||||
_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) {
|
||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||
|
||||
@@ -337,8 +337,10 @@ class VideoDownload {
|
||||
});
|
||||
}
|
||||
|
||||
var wasSuccesful = false;
|
||||
try {
|
||||
awaitAll(*sourcesToDownload.toTypedArray());
|
||||
wasSuccesful = true;
|
||||
}
|
||||
catch(runtimeEx: RuntimeException) {
|
||||
if(runtimeEx.cause != null)
|
||||
@@ -349,6 +351,29 @@ class VideoDownload {
|
||||
catch(ex: Throwable) {
|
||||
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 {
|
||||
|
||||
+4
-4
@@ -297,9 +297,9 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
}
|
||||
|
||||
if (!StatePayment.instance.hasPaid) {
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||
}
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.faq, canToggle = false, { false }, {
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
|
||||
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||
}))
|
||||
|
||||
@@ -356,8 +356,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
|
||||
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
|
||||
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }),
|
||||
ButtonDefinition(10, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
|
||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
|
||||
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
|
||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
||||
val c = it.context ?: return@ButtonDefinition;
|
||||
Logger.i(TAG, "settings preventPictureInPicture()");
|
||||
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
||||
|
||||
+1
@@ -418,6 +418,7 @@ class ChannelFragment : MainFragment() {
|
||||
|
||||
_buttonSubscribe.setSubscribeChannel(channel);
|
||||
_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 "";
|
||||
|
||||
//TODO: Find a better way to access the adapter fragments..
|
||||
|
||||
+17
-5
@@ -354,11 +354,22 @@ class SourceDetailFragment : MainFragment() {
|
||||
if(config.authentication == null)
|
||||
return;
|
||||
|
||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||
|
||||
reloadSource(config.id);
|
||||
};
|
||||
if(config.authentication.loginWarning != null) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Login Warning",
|
||||
config.authentication.loginWarning, null, 0,
|
||||
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) {
|
||||
val config = _config ?: return;
|
||||
@@ -454,6 +465,7 @@ class SourceDetailFragment : MainFragment() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fun checkForUpdatesSource() {
|
||||
val c = _config ?: return;
|
||||
val sourceUrl = c.sourceUrl ?: return;
|
||||
|
||||
+30
-7
@@ -4,19 +4,15 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -25,14 +21,13 @@ import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.SearchView
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
||||
import com.futo.platformplayer.views.overlays.CreatorSelectOverlay
|
||||
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
|
||||
@@ -64,6 +59,11 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
return view;
|
||||
}
|
||||
|
||||
override fun onHide() {
|
||||
super.onHide();
|
||||
_view?.onHide();
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SourcesFragment";
|
||||
fun newInstance() = SubscriptionGroupFragment().apply {}
|
||||
@@ -86,7 +86,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
private val _buttonSettings: ImageButton;
|
||||
private val _buttonDelete: ImageButton;
|
||||
|
||||
private val _buttonAddCreator: Button;
|
||||
private val _buttonAddCreator: FrameLayout;
|
||||
|
||||
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
|
||||
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
|
||||
@@ -97,6 +97,8 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
|
||||
private var _group: SubscriptionGroup? = null;
|
||||
|
||||
private var _didDelete: Boolean = false;
|
||||
|
||||
constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
|
||||
inflate(context, R.layout.fragment_subscriptions_group, this);
|
||||
_fragment = fragment;
|
||||
@@ -137,6 +139,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
UIDialogs.Action("Delete", {
|
||||
_group?.let {
|
||||
it.urls.remove(channel.url);
|
||||
save();
|
||||
reloadCreators(it);
|
||||
}
|
||||
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||
@@ -178,6 +181,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
UIDialogs.Action("Cancel", {}),
|
||||
UIDialogs.Action("Delete", {
|
||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
|
||||
_didDelete = true;
|
||||
fragment.close(true);
|
||||
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||
};
|
||||
@@ -246,6 +250,18 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
_overlay.animate().alpha(1f).setDuration(300).start();
|
||||
overlay.onSelected.subscribe {
|
||||
_group?.let { g ->
|
||||
if(g.urls.isEmpty() && g.image == null) {
|
||||
//Obtain image
|
||||
for(sub in it) {
|
||||
val sub = StateSubscriptions.instance.getSubscription(sub);
|
||||
if(sub != null && sub.channel.thumbnail != null) {
|
||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||
g.image?.setImageView(_imageGroup);
|
||||
g.image?.setImageView(_imageGroupBackground);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(url in it) {
|
||||
if(!g.urls.contains(url))
|
||||
g.urls.add(url);
|
||||
@@ -262,6 +278,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
|
||||
|
||||
fun setGroup(group: SubscriptionGroup?) {
|
||||
_didDelete = false;
|
||||
_group = group;
|
||||
_textGroupTitle.text = group?.name;
|
||||
|
||||
@@ -278,6 +295,12 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
reloadCreators(group);
|
||||
}
|
||||
|
||||
fun onHide() {
|
||||
if(!_didDelete && _group != null && StateSubscriptionGroups.instance.getSubscriptionGroup(_group!!.id) === null) {
|
||||
UIDialogs.toast(context, "Group creation cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun reloadCreators(group: SubscriptionGroup?) {
|
||||
_enabledCreators.clear();
|
||||
|
||||
+38
-23
@@ -10,6 +10,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.futo.platformplayer.*
|
||||
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.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
@@ -17,7 +18,6 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.exceptions.RateLimitException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -53,6 +53,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
override val hasBottomBar: Boolean get() = true;
|
||||
|
||||
private var _view: SubscriptionsFeedView? = null;
|
||||
private var _group: SubscriptionGroup? = null;
|
||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
@@ -73,6 +74,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = SubscriptionsFeedView(this, inflater, _cachedRecyclerData);
|
||||
_view = view;
|
||||
if(_group != null)
|
||||
view.selectSubgroup(_group);
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -81,6 +84,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
val view = _view;
|
||||
if (view != null) {
|
||||
_cachedRecyclerData = view.recyclerData;
|
||||
_group = view.subGroup;
|
||||
view.cleanup();
|
||||
_view = null;
|
||||
}
|
||||
@@ -101,20 +105,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
|
||||
|
||||
private var _subGroup: SubscriptionGroup? = null;
|
||||
var subGroup: SubscriptionGroup? = null;
|
||||
|
||||
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()");
|
||||
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 ->
|
||||
};
|
||||
|
||||
@@ -142,7 +136,6 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
recyclerData.lastLoad = OffsetDateTime.now();
|
||||
|
||||
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen) {
|
||||
loadCache();
|
||||
loadResults(false);
|
||||
}
|
||||
else if(recyclerData.results.size == 0) {
|
||||
@@ -171,12 +164,24 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
|
||||
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() {
|
||||
super.cleanup()
|
||||
StateSubscriptions.instance.global.onUpdateProgress.remove(this);
|
||||
StateSubscriptions.instance.onSubscriptionsChanged.remove(this);
|
||||
StateSubscriptions.instance.onFeedProgress.remove(this);
|
||||
}
|
||||
|
||||
override val feedStyle: FeedStyle get() = Settings.instance.subscriptions.getSubscriptionsFeedStyle();
|
||||
@@ -200,7 +205,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
private var _bypassRateLimit = false;
|
||||
private val _lastExceptions: List<Throwable>? = null;
|
||||
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
||||
val group = _subGroup;
|
||||
val group = subGroup;
|
||||
if(!_bypassRateLimit) {
|
||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
|
||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
||||
@@ -260,6 +265,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
}
|
||||
};
|
||||
|
||||
fun selectSubgroup(g: SubscriptionGroup?) {
|
||||
if(g != null)
|
||||
_subscriptionBar?.selectGroup(g);
|
||||
}
|
||||
|
||||
private fun initializeToolbarContent() {
|
||||
_subscriptionBar = SubscriptionBar(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
@@ -269,11 +279,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
if(g is SubscriptionGroup.Add)
|
||||
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
|
||||
else {
|
||||
_subGroup = g;
|
||||
subGroup = g;
|
||||
setProgress(0, 0);
|
||||
if(Settings.instance.subscriptions.fetchOnTabOpen)
|
||||
if(Settings.instance.subscriptions.fetchOnTabOpen) {
|
||||
loadCache();
|
||||
loadResults(false);
|
||||
else loadCache();
|
||||
}
|
||||
else if(g != null && StateSubscriptions.instance.getFeed(g.id) != null) {
|
||||
loadResults(false);
|
||||
}
|
||||
else
|
||||
loadCache();
|
||||
}
|
||||
};
|
||||
_subscriptionBar?.onHoldGroup?.subscribe { g ->
|
||||
@@ -314,7 +330,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
|
||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||
val nowSoon = OffsetDateTime.now().plusMinutes(5);
|
||||
val filterGroup = _subGroup;
|
||||
val filterGroup = subGroup;
|
||||
return results.filter {
|
||||
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
||||
|
||||
@@ -414,7 +430,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
context?.let {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
if (exs.size <= 8) {
|
||||
if (exs.size <= 3) {
|
||||
for (ex in exs) {
|
||||
var toShow = ex;
|
||||
var channel: String? = null;
|
||||
@@ -424,12 +440,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
}
|
||||
Logger.e(TAG, "Channel [${channel}] failed", ex);
|
||||
if (toShow is PluginException)
|
||||
UIDialogs.toast(
|
||||
it,
|
||||
UIDialogs.appToast(
|
||||
context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
|
||||
);
|
||||
else
|
||||
UIDialogs.toast(it, ex.message ?: "");
|
||||
UIDialogs.appToast(ex.message ?: "");
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -440,7 +455,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
.map { it!! }
|
||||
.toList();
|
||||
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) {
|
||||
Logger.e(TAG, "Failed to handle exceptions", e)
|
||||
|
||||
-7
@@ -25,8 +25,6 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||
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
|
||||
|
||||
class VideoDetailFragment : MainFragment {
|
||||
@@ -372,11 +370,6 @@ class VideoDetailFragment : MainFragment {
|
||||
|
||||
Logger.v(TAG, "shouldStop: $shouldStop");
|
||||
if(shouldStop) {
|
||||
_viewDetail?.let {
|
||||
val v = it.video ?: return@let;
|
||||
StateSaved.instance.setVideoToOpenBlocking(VideoToOpen(v.url, (it.lastPositionMilliseconds / 1000.0f).toLong()));
|
||||
}
|
||||
|
||||
_viewDetail?.onStop();
|
||||
StateCasting.instance.onStop();
|
||||
Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
|
||||
|
||||
+25
-10
@@ -149,6 +149,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import userpackage.Protocol
|
||||
import java.time.OffsetDateTime
|
||||
@@ -447,6 +449,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
updateMoreButtons();
|
||||
|
||||
_channelButton.setOnClickListener {
|
||||
if (video is TutorialFragment.TutorialVideo) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
(video?.author ?: _searchVideo?.author)?.let {
|
||||
fragment.navigate<ChannelFragment>(it);
|
||||
fragment.lifecycleScope.launch {
|
||||
@@ -849,14 +855,19 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val _historyIndexLock = Mutex(false);
|
||||
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
|
||||
val current = _historyIndex;
|
||||
if(current == null || current.url != video.url) {
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
||||
_historyIndex = index;
|
||||
return@withContext index;
|
||||
_historyIndexLock.withLock {
|
||||
val current = _historyIndex;
|
||||
if(current == null || current.url != video.url) {
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
||||
_historyIndex = index;
|
||||
return@withContext index;
|
||||
}
|
||||
return@withContext current;
|
||||
}
|
||||
return@withContext current;
|
||||
}
|
||||
|
||||
|
||||
@@ -1117,7 +1128,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
switchContentView(_container_content_main);
|
||||
}
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
//@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||
|
||||
@@ -1140,6 +1151,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(videoDetail is VideoLocal) {
|
||||
videoLocal = videoDetail;
|
||||
video = videoDetail;
|
||||
this.video = video;
|
||||
val videoTask = StatePlatform.instance.getContentDetails(videoDetail.url);
|
||||
videoTask.invokeOnCompletion { ex ->
|
||||
if(ex != null) {
|
||||
@@ -1212,7 +1224,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_addCommentView.setContext(video.url, ref)
|
||||
_player.setMetadata(video.name, video.author.name);
|
||||
|
||||
if (video !is TutorialFragment.TutorialVideo) {
|
||||
if (video is TutorialFragment.TutorialVideo) {
|
||||
_toggleCommentType.setValue(false, false);
|
||||
} else {
|
||||
_toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false);
|
||||
@@ -1479,12 +1491,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
private fun loadCurrentVideo(resumePositionMs: Long = 0) {
|
||||
_didStop = false;
|
||||
|
||||
val video = video ?: return;
|
||||
val video = (videoLocal ?: video) ?: return;
|
||||
|
||||
try {
|
||||
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
|
||||
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
val subtitleSource = _lastSubtitleSource;
|
||||
val subtitleSource = _lastSubtitleSource ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
|
||||
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
|
||||
|
||||
if(videoSource == null && audioSource == null) {
|
||||
@@ -1512,6 +1524,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
_player.setArtwork(null);
|
||||
|
||||
_player.setSource(videoSource, audioSource, _playWhenReady, false);
|
||||
if(subtitleSource != null)
|
||||
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
|
||||
_player.seekTo(resumePositionMs);
|
||||
}
|
||||
else
|
||||
@@ -1519,6 +1533,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_lastVideoSource = videoSource;
|
||||
_lastAudioSource = audioSource;
|
||||
_lastSubtitleSource = subtitleSource;
|
||||
}
|
||||
catch(ex: UnsupportedCastException) {
|
||||
Logger.e(TAG, "Failed to load cast media", ex);
|
||||
|
||||
@@ -143,7 +143,7 @@ class MediaPlaybackService : Service() {
|
||||
override fun onDestroy() {
|
||||
Logger.v(TAG, "onDestroy");
|
||||
_instance = null;
|
||||
MediaControlReceiver.onCloseReceived.emit();
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ class MediaPlaybackService : Service() {
|
||||
|
||||
fun closeMediaSession() {
|
||||
Logger.v(TAG, "closeMediaSession");
|
||||
stopForeground(STOP_FOREGROUND_DETACH);
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
|
||||
val focusRequest = _focusRequest;
|
||||
if (focusRequest != null) {
|
||||
@@ -162,7 +162,9 @@ class MediaPlaybackService : Service() {
|
||||
}
|
||||
_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_bitmap = null;
|
||||
_mediaSession = null;
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Color
|
||||
import android.media.AudioManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
@@ -38,6 +39,7 @@ import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
||||
import com.futo.platformplayer.services.DownloadService
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
@@ -380,13 +382,15 @@ class StateApp {
|
||||
|
||||
Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]");
|
||||
StatePolycentric.instance.load(context);
|
||||
Logger.i(TAG, "MainApp Starting: Initializing [Saved]");
|
||||
StateSaved.instance.load();
|
||||
|
||||
Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]");
|
||||
displayMetrics = context.resources.displayMetrics;
|
||||
ensureConnectivityManager(context);
|
||||
|
||||
Logger.i(TAG, "MainApp Starting: Cleaning up unused downloads");
|
||||
StateDownloads.instance.cleanupDownloads();
|
||||
|
||||
|
||||
Logger.i(TAG, "MainApp Starting: Initializing [Telemetry]");
|
||||
if (!BuildConfig.DEBUG) {
|
||||
StateTelemetry.instance.initialize();
|
||||
@@ -460,7 +464,9 @@ class StateApp {
|
||||
|
||||
//Foreground download
|
||||
autoUpdateEnabled -> {
|
||||
StateUpdate.instance.checkForUpdates(context, false);
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
StateUpdate.instance.checkForUpdates(context, false)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -558,6 +564,40 @@ class StateApp {
|
||||
|
||||
if(StateHistory.instance.shouldMigrateLegacyHistory())
|
||||
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) {
|
||||
|
||||
@@ -352,7 +352,10 @@ class StateDownloads {
|
||||
|
||||
fun cleanupDownloads(): Pair<Int, Long> {
|
||||
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 totalDeletedCount = 0;
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.collection.LruCache
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
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.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
||||
@@ -78,6 +79,7 @@ class StatePlatform {
|
||||
private val _clientsLock = Object();
|
||||
private val _availableClients : 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
|
||||
//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 {
|
||||
private var _instance : StatePlatform? = null;
|
||||
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.SourcePluginConfig
|
||||
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.models.ImageVariable
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
@@ -467,7 +466,6 @@ class StatePlugins {
|
||||
_plugins.save(descriptor);
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
private data class PluginConfig(
|
||||
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.os.Build
|
||||
import android.os.Environment
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.copyToOutputStream
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
@@ -155,47 +155,45 @@ class StateUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
fun checkForUpdates(context: Context, showUpToDateToast: Boolean) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val latestVersion = downloadVersionCode(client);
|
||||
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val latestVersion = downloadVersionCode(client);
|
||||
|
||||
if (latestVersion != null) {
|
||||
val currentVersion = BuildConfig.VERSION_CODE;
|
||||
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
|
||||
if (latestVersion != null) {
|
||||
val currentVersion = BuildConfig.VERSION_CODE;
|
||||
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
|
||||
|
||||
if (latestVersion > currentVersion) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.toast(context, "Failed to show update dialog");
|
||||
Logger.w(TAG, "Error occurred in update dialog.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (showUpToDateToast) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Already on latest version");
|
||||
}
|
||||
if (latestVersion > currentVersion) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.toast(context, "Failed to show update dialog");
|
||||
Logger.w(TAG, "Error occurred in update dialog.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.w(TAG, "Failed to retrieve version from version URL.");
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to retrieve version");
|
||||
if (showUpToDateToast) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Already on latest version");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to check for updates.", e);
|
||||
} else {
|
||||
Logger.w(TAG, "Failed to retrieve version from version URL.");
|
||||
|
||||
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) {
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
||||
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) {
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
|
||||
class DisabledSourceView : LinearLayout {
|
||||
private val _root: LinearLayout;
|
||||
@@ -38,7 +36,16 @@ class DisabledSourceView : LinearLayout {
|
||||
client.icon?.setImageView(_imageSource);
|
||||
|
||||
_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) }
|
||||
_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;
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -10,7 +10,9 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
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.Event1
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
|
||||
class EnabledSourceViewHolder : ViewHolder {
|
||||
private val _imageSource: ImageView;
|
||||
@@ -57,8 +59,18 @@ class EnabledSourceViewHolder : ViewHolder {
|
||||
fun bind(client: IPlatformClient) {
|
||||
client.icon?.setImageView(_imageSource);
|
||||
|
||||
_textSource.text = client.name;
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
|
||||
_textSource.text = client.name
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -15,6 +15,7 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.debug.Stopwatch
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
@@ -46,7 +47,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
val contentDetails = StatePlatform.instance.getContentDetails(video.url).await();
|
||||
stopwatch.logAndNext(TAG, "Retrieving video detail (IO thread)")
|
||||
return@TaskHandler Pair(viewHolder, contentDetails)
|
||||
}).success { previewContentDetails(it.first, it.second) }
|
||||
}).exception<Throwable> { Logger.e(TAG, "Failed to retrieve preview content.", it) }.success { previewContentDetails(it.first, it.second) }
|
||||
|
||||
constructor(context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
|
||||
initialPlay: Boolean = false, viewsToPrepend: ArrayList<View> = arrayListOf(),
|
||||
|
||||
+11
-24
@@ -3,45 +3,31 @@ package com.futo.platformplayer.views.adapters.viewholders
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
||||
class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
|
||||
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_group_bar, _viewGroup, false)) {
|
||||
private var _group: SubscriptionGroup? = null;
|
||||
|
||||
private val _root: FrameLayout;
|
||||
private val _image: ShapeableImageView;
|
||||
private val _textSubGroup: TextView;
|
||||
|
||||
|
||||
val onClick = Event1<SubscriptionGroup>();
|
||||
val onClickLong = Event1<SubscriptionGroup>();
|
||||
|
||||
init {
|
||||
_root = _view.findViewById(R.id.root);
|
||||
_image = _view.findViewById(R.id.image);
|
||||
_textSubGroup = _view.findViewById(R.id.text_sub_group);
|
||||
|
||||
val dp6 = 6.dp(_view.resources);
|
||||
_image.shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
|
||||
.build()
|
||||
|
||||
_view.setOnClickListener {
|
||||
_group?.let {
|
||||
onClick.emit(it);
|
||||
@@ -58,9 +44,9 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
override fun bind(value: SubscriptionGroup) {
|
||||
_group = value;
|
||||
val img = value.image;
|
||||
if(img != null)
|
||||
if(img != null) {
|
||||
img.setImageView(_image)
|
||||
else {
|
||||
} else {
|
||||
_image.setImageResource(0);
|
||||
|
||||
if(value is SubscriptionGroup.Add)
|
||||
@@ -68,10 +54,11 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
}
|
||||
_textSubGroup.text = value.name;
|
||||
|
||||
if(value is SubscriptionGroup.Selectable && value.selected)
|
||||
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
|
||||
else
|
||||
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
|
||||
if (value is SubscriptionGroup.Selectable && value.selected) {
|
||||
_root.setBackgroundResource(R.drawable.background_primary_round_6dp)
|
||||
} else {
|
||||
_root.background = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,52 +1,25 @@
|
||||
package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.shapes.Shape
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import android.widget.FrameLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.PresetImages
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.SearchView
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SelectableCreatorBarViewHolder
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import java.io.File
|
||||
|
||||
class CreatorSelectOverlay: ConstraintLayout {
|
||||
private val _buttonSelect: Button;
|
||||
private val _buttonSelect: FrameLayout;
|
||||
private val _topbar: OverlayTopbar;
|
||||
|
||||
private val _searchBar: SearchView;
|
||||
@@ -97,7 +70,7 @@ class CreatorSelectOverlay: ConstraintLayout {
|
||||
this.orientation = LinearLayoutManager.VERTICAL;
|
||||
};
|
||||
_buttonSelect.setOnClickListener {
|
||||
_selected?.let {
|
||||
if (_selected.isNotEmpty()) {
|
||||
select();
|
||||
}
|
||||
};
|
||||
@@ -134,7 +107,7 @@ class CreatorSelectOverlay: ConstraintLayout {
|
||||
|
||||
private fun filterCreators(withUpdate: Boolean = true) {
|
||||
val query = _searchBar.textSearch.text.toString().lowercase();
|
||||
val filteredEnabled = _creators.filter { query.isNullOrEmpty() || it.channel.name.lowercase().contains(query) };
|
||||
val filteredEnabled = _creators.filter { query.isEmpty() || it.channel.name.lowercase().contains(query) };
|
||||
|
||||
//Optimize
|
||||
_creatorsFiltered.clear();
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.net.toFile
|
||||
@@ -48,7 +49,7 @@ class ImageVariableOverlay: ConstraintLayout {
|
||||
private val _buttonGallery: BigButton;
|
||||
private val _imageGallerySelected: ImageView;
|
||||
private val _imageGallerySelectedContainer: LinearLayout;
|
||||
private val _buttonSelect: Button;
|
||||
private val _buttonSelect: TextView;
|
||||
private val _topbar: OverlayTopbar;
|
||||
private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>;
|
||||
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
|
||||
|
||||
@@ -25,6 +25,7 @@ class SourceHeaderView : LinearLayout {
|
||||
private val _sourcePlatformUrl: TextView;
|
||||
private val _sourceRepositoryUrl: TextView;
|
||||
private val _sourceScriptUrl: TextView;
|
||||
private val _sourceScriptConfig: TextView;
|
||||
private val _sourceSignature: TextView;
|
||||
|
||||
private val _sourcePlatformUrlContainer: LinearLayout;
|
||||
@@ -45,6 +46,7 @@ class SourceHeaderView : LinearLayout {
|
||||
_sourcePlatformUrl = findViewById(R.id.source_platform);
|
||||
_sourcePlatformUrlContainer = findViewById(R.id.source_platform_container);
|
||||
_sourceScriptUrl = findViewById(R.id.source_script);
|
||||
_sourceScriptConfig = findViewById(R.id.source_config);
|
||||
_sourceSignature = findViewById(R.id.source_signature);
|
||||
|
||||
_sourceBy.setOnClickListener {
|
||||
@@ -59,6 +61,10 @@ class SourceHeaderView : LinearLayout {
|
||||
if(!_config?.absoluteScriptUrl.isNullOrEmpty())
|
||||
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 {
|
||||
if(!_config?.platformUrl.isNullOrEmpty())
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.platformUrl)));
|
||||
@@ -82,6 +88,7 @@ class SourceHeaderView : LinearLayout {
|
||||
_sourceVersion.text = config.version.toString();
|
||||
_sourceScriptUrl.text = config.absoluteScriptUrl;
|
||||
_sourceRepositoryUrl.text = config.repositoryUrl;
|
||||
_sourceScriptConfig.text = config.sourceUrl
|
||||
_sourceAuthorID.text = "";
|
||||
|
||||
_sourcePlatformUrl.text = config.platformUrl ?: "";
|
||||
|
||||
@@ -88,6 +88,13 @@ class SubscriptionBar : LinearLayout {
|
||||
updateExplore();
|
||||
}
|
||||
|
||||
fun selectGroup(group: SubscriptionGroup) {
|
||||
val relevantGroup = _groups.find { it.id == group.id };
|
||||
if(relevantGroup != null && _group != relevantGroup) {
|
||||
groupClicked(relevantGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private fun groupClicked(g: SubscriptionGroup) {
|
||||
if(g is SubscriptionGroup.Add) {
|
||||
onToggleGroup.emit(g);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -11,6 +14,9 @@ import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.PlayerControlView
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
@@ -39,6 +45,14 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
|
||||
|
||||
//Events
|
||||
private val _evMuteChanged = mutableListOf<(FutoThumbnailPlayer, Boolean)->Unit>();
|
||||
private val _loadArtwork = object: CustomTarget<Bitmap>() {
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
setArtwork(BitmapDrawable(resources, resource));
|
||||
}
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
setArtwork(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@@ -113,11 +127,38 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
|
||||
fun setPreview(video: IPlatformVideoDetails) {
|
||||
val videoSource = VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
|
||||
val audioSource = VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
setSource(videoSource, audioSource,true, false);
|
||||
if (video.live != null) {
|
||||
setSource(video.live, null,true, false);
|
||||
} else {
|
||||
val videoSource = VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
|
||||
val audioSource = VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
if (videoSource == null && audioSource != null) {
|
||||
val thumbnail = video.thumbnails.getHQThumbnail();
|
||||
if (!thumbnail.isNullOrBlank()) {
|
||||
Glide.with(videoView).asBitmap().load(thumbnail).into(_loadArtwork);
|
||||
} else {
|
||||
Glide.with(videoView).clear(_loadArtwork);
|
||||
setArtwork(null);
|
||||
}
|
||||
} else {
|
||||
Glide.with(videoView).clear(_loadArtwork);
|
||||
}
|
||||
|
||||
setSource(videoSource, audioSource,true, false);
|
||||
}
|
||||
}
|
||||
override fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun setArtwork(drawable: Drawable?) {
|
||||
if (drawable != null) {
|
||||
videoView.defaultArtwork = drawable;
|
||||
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL;
|
||||
} else {
|
||||
videoView.defaultArtwork = null;
|
||||
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#99000000" />
|
||||
<corners android:radius="6dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#2D63ED" />
|
||||
<corners android:radius="6dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M481.54,685.39Q494.85,685.39 503.96,676.27Q513.08,667.15 513.08,653.85Q513.08,640.54 503.96,631.42Q494.85,622.31 481.54,622.31Q468.23,622.31 459.11,631.42Q450,640.54 450,653.85Q450,667.15 459.11,676.27Q468.23,685.39 481.54,685.39ZM460.92,552.92L499.54,552.92Q501.08,526.15 509.46,510.31Q517.85,494.46 541.54,470.77Q570.39,441.92 583.35,420.73Q596.31,399.54 596.31,372.61Q596.31,325.77 564.15,297.11Q532,268.46 485.61,268.46Q442.92,268.46 412.11,291.23Q381.31,314 367.08,345.85L403.85,361.08Q413.92,337.15 432.61,321.42Q451.31,305.69 483.15,305.69Q520.92,305.69 539.31,326.35Q557.69,347 557.69,373.31Q557.69,393.39 546.69,410.08Q535.69,426.77 515.85,445.15Q483.61,474.92 472.27,498.73Q460.92,522.54 460.92,552.92ZM224.62,800Q197,800 178.5,781.5Q160,763 160,735.39L160,224.61Q160,197 178.5,178.5Q197,160 224.62,160L735.39,160Q763,160 781.5,178.5Q800,197 800,224.61L800,735.39Q800,763 781.5,781.5Q763,800 735.39,800L224.62,800ZM224.62,760L735.39,760Q744.61,760 752.31,752.31Q760,744.61 760,735.39L760,224.61Q760,215.39 752.31,207.69Q744.61,200 735.39,200L224.62,200Q215.38,200 207.69,207.69Q200,215.39 200,224.61L200,735.39Q200,744.61 207.69,752.31Q215.38,760 224.62,760ZM200,200L200,200Q200,200 200,206.92Q200,213.85 200,224.61L200,735.39Q200,746.15 200,753.08Q200,760 200,760L200,760Q200,760 200,753.08Q200,746.15 200,735.39L200,224.61Q200,213.85 200,206.92Q200,200 200,200Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M481.54,685.39Q494.85,685.39 503.96,676.27Q513.08,667.15 513.08,653.85Q513.08,640.54 503.96,631.42Q494.85,622.31 481.54,622.31Q468.23,622.31 459.11,631.42Q450,640.54 450,653.85Q450,667.15 459.11,676.27Q468.23,685.39 481.54,685.39ZM460.92,552.92L499.54,552.92Q501.08,526.15 509.46,510.31Q517.85,494.46 541.54,470.77Q570.39,441.92 583.35,420.73Q596.31,399.54 596.31,372.61Q596.31,325.77 564.15,297.11Q532,268.46 485.61,268.46Q442.92,268.46 412.11,291.23Q381.31,314 367.08,345.85L403.85,361.08Q413.92,337.15 432.61,321.42Q451.31,305.69 483.15,305.69Q520.92,305.69 539.31,326.35Q557.69,347 557.69,373.31Q557.69,393.39 546.69,410.08Q535.69,426.77 515.85,445.15Q483.61,474.92 472.27,498.73Q460.92,522.54 460.92,552.92ZM224.62,800Q197,800 178.5,781.5Q160,763 160,735.39L160,224.61Q160,197 178.5,178.5Q197,160 224.62,160L735.39,160Q763,160 781.5,178.5Q800,197 800,224.61L800,735.39Q800,763 781.5,781.5Q763,800 735.39,800L224.62,800Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M459.39,744.61L498.61,744.61L498.61,697.69Q541.69,694.08 581.15,666.77Q620.61,639.46 620.61,582Q620.61,540 595.08,510.39Q569.54,480.77 497.54,454.77Q431.39,431.69 413,415.15Q394.61,398.61 394.61,368Q394.61,337.39 418.5,317Q442.39,296.61 482,296.61Q512.46,296.61 532.77,310.58Q553.08,324.54 565.69,346L600.46,332.31Q586.39,303.46 559.19,284Q532,264.54 500.61,262.31L500.61,215.39L461.39,215.39L461.39,262.31Q409.08,271 382.23,301.31Q355.39,331.61 355.39,368Q355.39,411.15 382.5,437.08Q409.61,463 474,486.31Q538.54,510.08 560.73,529.23Q582.92,548.39 582.92,582Q582.92,624.23 552.11,642.81Q521.31,661.39 486,661.39Q451.46,661.39 423.65,641.27Q395.85,621.15 379.23,584L344,599.23Q361.08,640.31 389.81,663.27Q418.54,686.23 459.39,695.69L459.39,744.61ZM480,840Q405.46,840 339.77,811.58Q274.08,783.15 225.46,734.54Q176.85,685.92 148.42,620.23Q120,554.54 120,480Q120,405.46 148.42,339.77Q176.85,274.08 225.46,225.46Q274.08,176.85 339.77,148.42Q405.46,120 480,120Q554.54,120 620.23,148.42Q685.92,176.85 734.54,225.46Q783.15,274.08 811.58,339.77Q840,405.46 840,480Q840,554.54 811.58,620.23Q783.15,685.92 734.54,734.54Q685.92,783.15 620.23,811.58Q554.54,840 480,840Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
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"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M540,581.54Q552.39,581.54 561.81,572.11Q571.23,562.69 571.23,550.31Q571.23,537.92 561.81,528.5Q552.39,519.08 540,519.08Q527.61,519.08 518.19,528.5Q508.77,537.92 508.77,550.31Q508.77,562.69 518.19,572.11Q527.61,581.54 540,581.54ZM522.31,468.92L557.69,468.92Q559.23,443.77 565.62,431.04Q572,418.31 596.31,395.54Q621.69,372.46 631.69,354.35Q641.69,336.23 641.69,312.77Q641.69,272.39 612.89,245.42Q584.08,218.46 540,218.46Q506.69,218.46 480.81,236.46Q454.92,254.46 441.39,285.54L473.85,299.85Q485.15,276.39 501.42,264.65Q517.69,252.92 540,252.92Q568.61,252.92 587.46,269.89Q606.31,286.85 606.31,313.69Q606.31,330 597.15,344.04Q588,358.08 565.69,377.85Q540.39,399.92 531.35,418.35Q522.31,436.77 522.31,468.92ZM324.61,680Q297,680 278.5,661.5Q260,643 260,615.39L260,184.61Q260,157 278.5,138.5Q297,120 324.61,120L755.39,120Q783,120 801.5,138.5Q820,157 820,184.61L820,615.39Q820,643 801.5,661.5Q783,680 755.39,680L324.61,680ZM204.62,800Q177,800 158.5,781.5Q140,763 140,735.39L140,264.61L180,264.61L180,735.39Q180,744.62 187.69,752.31Q195.38,760 204.62,760L675.39,760L675.39,800L204.62,800Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M405.38,840L390.92,724.31Q371.77,718.54 349.5,706.15Q327.23,693.77 311.61,679.62L204.92,725L130.31,595L222.54,525.46Q220.77,514.61 219.62,503.11Q218.46,491.61 218.46,480.77Q218.46,470.69 219.62,459.58Q220.77,448.46 222.54,434.54L130.31,365L204.92,236.54L310.85,281.15Q328.77,266.23 349.61,254.23Q370.46,242.23 390.15,235.69L405.38,120L554.62,120L569.08,236.46Q592.08,244.54 609.73,255Q627.39,265.46 646.08,281.15L755.08,236.54L829.69,365L734.39,436.85Q737.69,449.23 738.08,459.58Q738.46,469.92 738.46,480Q738.46,489.31 737.69,499.65Q736.92,510 734.15,524.69L827.92,595L753.31,725L646.08,678.85Q627.39,694.54 608.46,705.77Q589.54,717 569.08,723.54L554.62,840L405.38,840ZM478.92,580Q520.77,580 549.85,550.92Q578.92,521.85 578.92,480Q578.92,438.15 549.85,409.08Q520.77,380 478.92,380Q436.85,380 407.88,409.08Q378.92,438.15 378.92,480Q378.92,521.85 407.88,550.92Q436.85,580 478.92,580Z"/>
|
||||
</vector>
|
||||
@@ -70,4 +70,13 @@
|
||||
android:visibility="gone"
|
||||
android:elevation="15dp">
|
||||
</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>
|
||||
@@ -81,43 +81,38 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_remembered_devices"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="3"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/remembered_devices"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:textSize="14dp"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/white"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular" />
|
||||
|
||||
<Button
|
||||
<ImageButton
|
||||
android:id="@+id/button_scan_qr"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1.7"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scan_qr"
|
||||
android:textSize="14dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:srcCompat="@drawable/ic_qr"
|
||||
app:tint="@color/primary" />
|
||||
|
||||
<Button
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add"
|
||||
android:textSize="14dp"
|
||||
android:textAlignment="textEnd"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:srcCompat="@drawable/ic_add"
|
||||
app:tint="@color/primary"
|
||||
android:layout_marginEnd="20dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -38,12 +38,12 @@
|
||||
android:id="@+id/dialog_text_details"
|
||||
android:layout_width="match_parent"
|
||||
android:textColor="#AAAAAA"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text=""
|
||||
android:textAlignment="center"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:textSize="11dp"
|
||||
android:textSize="13dp"
|
||||
android:layout_height="wrap_content" />
|
||||
<TextView
|
||||
android:id="@+id/dialog_text_code"
|
||||
|
||||
@@ -180,20 +180,28 @@
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/container_top"
|
||||
app:layout_constraintBottom_toTopOf="@id/button_creator_add"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<Button
|
||||
<FrameLayout
|
||||
android:id="@+id/button_creator_add"
|
||||
android:layout_width="match_parent"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:layout_height="50dp"
|
||||
android:layout_margin="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:text="Add Creator" />
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/add_creator"
|
||||
android:textSize="16dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay"
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:gravity="top"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="5dp">
|
||||
android:paddingBottom="0dp">
|
||||
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/creator_thumbnail"
|
||||
@@ -179,6 +179,7 @@
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray_e0"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:textSize="12dp"
|
||||
tools:text="57K views • 1 day ago" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -140,37 +140,22 @@
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:paddingBottom="3dp"
|
||||
app:srcCompat="@drawable/ic_queue_16dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_to"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
|
||||
android:padding="5dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp"
|
||||
android:background="@color/transparent"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/options" />
|
||||
|
||||
@@ -185,6 +170,23 @@
|
||||
android:layout_marginEnd="4dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:src="@drawable/ic_queue"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="7dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_video_name"
|
||||
android:layout_width="fill_parent"
|
||||
@@ -195,8 +197,9 @@
|
||||
android:textSize="13dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
tools:text="Legendary grant recipient: Marvin Wißfeld of MicroG Very loong title"
|
||||
tools:text="Legendary grant recipient: Marvin Wißfeld of MicroG Very loong title fff"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
@@ -174,37 +174,23 @@
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:paddingBottom="3dp"
|
||||
app:srcCompat="@drawable/ic_queue_16dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_to"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
|
||||
android:padding="5dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp"
|
||||
android:background="@color/transparent"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/options" />
|
||||
|
||||
@@ -219,6 +205,23 @@
|
||||
android:layout_marginEnd="4dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:src="@drawable/ic_queue"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="7dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_video_name"
|
||||
android:layout_width="fill_parent"
|
||||
|
||||
@@ -28,25 +28,32 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/search_bar"
|
||||
app:layout_constraintBottom_toTopOf="@id/container_select"
|
||||
app:layout_constraintBottom_toTopOf="@id/button_select"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_select"
|
||||
<FrameLayout
|
||||
android:id="@+id/button_select"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
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_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
<Button
|
||||
android:id="@+id/button_select"
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="10dp"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:text="Select" />
|
||||
</LinearLayout>
|
||||
android:layout_height="match_parent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/select"
|
||||
android:textSize="16dp"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -116,20 +116,29 @@
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
<FrameLayout
|
||||
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_height="wrap_content"
|
||||
android:layout_height="50dp"
|
||||
android:background="@drawable/background_button_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
<Button
|
||||
|
||||
<TextView
|
||||
android:id="@+id/button_select"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="10dp"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:text="Select" />
|
||||
</LinearLayout>
|
||||
android:layout_height="match_parent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/select"
|
||||
android:textSize="16dp"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -107,7 +107,7 @@
|
||||
android:id="@id/exo_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginBottom="-1dp"
|
||||
android:layout_marginBottom="-2dp"
|
||||
android:paddingStart="0dp"
|
||||
app:scrubber_drawable="@drawable/player_thumb"
|
||||
app:bar_height="2dp"
|
||||
|
||||
@@ -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>
|
||||
@@ -4,7 +4,10 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/root">
|
||||
|
||||
|
||||
@@ -170,6 +170,28 @@
|
||||
tools:text="https://some.repository.url/whatever/someScript.js" />
|
||||
</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-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="78dp"
|
||||
android:layout_height="54dp"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="2dp"
|
||||
android:layout_margin="2dp"
|
||||
android:clickable="true"
|
||||
android:id="@+id/root">
|
||||
android:id="@+id/root"
|
||||
android:background="@drawable/background_primary_round_6dp">
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/xp_book" />
|
||||
<LinearLayout
|
||||
android:src="@drawable/xp_book"
|
||||
app:shapeAppearanceOverlay="@style/roundedCorners_6dp"
|
||||
android:layout_margin="2dp" />
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#99000000"
|
||||
android:gravity="center">
|
||||
<TextView
|
||||
android:id="@+id/text_sub_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:textSize="12dp"
|
||||
android:textAlignment="center"
|
||||
android:text="News" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:background="@drawable/background_dark_round_6dp"
|
||||
android:gravity="center"
|
||||
android:layout_margin="2dp" />
|
||||
<TextView
|
||||
android:id="@+id/text_sub_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:textSize="12dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_gravity="center"
|
||||
tools:text="News" />
|
||||
</FrameLayout>
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
<string name="platform_url">Platform URL</string>
|
||||
<string name="repository_url">Repository 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_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>
|
||||
@@ -651,6 +652,7 @@
|
||||
<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="tap_to_open">Tap to open</string>
|
||||
<string name="update_available_exclamation">Update available!</string>
|
||||
<string name="watching">watching</string>
|
||||
<string name="available_in">available in</string>
|
||||
<string name="seconds">seconds</string>
|
||||
@@ -724,6 +726,8 @@
|
||||
<string name="position">Position</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="add_creator">Add Creators</string>
|
||||
<string name="select">Select</string>
|
||||
<string-array name="home_screen_array">
|
||||
<item>Recommendations</item>
|
||||
<item>Subscriptions</item>
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">4dp</item>
|
||||
</style>
|
||||
<style name="roundedCorners_6dp" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">6dp</item>
|
||||
</style>
|
||||
<style name="roundedCorners_10dp" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">10dp</item>
|
||||
|
||||
Submodule app/src/stable/assets/sources/odysee updated: a21ad56829...537ec49663
Submodule app/src/stable/assets/sources/patreon updated: 55aef15f4b...139444608d
Submodule app/src/stable/assets/sources/rumble updated: 7d4303c82a...263ed8c7df
Submodule app/src/stable/assets/sources/youtube updated: 13551ab67f...b7864b9101
Submodule app/src/unstable/assets/sources/odysee updated: a21ad56829...537ec49663
Submodule app/src/unstable/assets/sources/patreon updated: 55aef15f4b...139444608d
Submodule app/src/unstable/assets/sources/rumble updated: 7d4303c82a...263ed8c7df
Submodule app/src/unstable/assets/sources/youtube updated: 13551ab67f...b7864b9101
Reference in New Issue
Block a user