Compare commits

..

1 Commits

Author SHA1 Message Date
Gabe Rogan b3c3917d6c http promises (wip) 2023-12-19 09:57:02 -05:00
177 changed files with 1655 additions and 4033 deletions
+3
View File
@@ -1,6 +1,9 @@
[submodule "dep/polycentricandroid"]
path = dep/polycentricandroid
url = ../polycentricandroid.git
[submodule "app/src/playstore/assets/sources/peertube"]
path = app/src/playstore/assets/sources/peertube
url = ../plugins/peertube.git
[submodule "app/src/stable/assets/sources/kick"]
path = app/src/stable/assets/sources/kick
url = ../plugins/kick.git
+11 -11
View File
@@ -151,7 +151,7 @@ dependencies {
//Core
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
//Images
@@ -169,18 +169,18 @@ 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:3.0.2")
implementation("com.caoccao.javet:javet-android:2.2.1")
//Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.1'
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
implementation 'androidx.media3:media3-ui:1.2.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
implementation 'androidx.media3:media3-transformer:1.2.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.media3:media3-exoplayer:1.2.0'
implementation 'androidx.media3:media3-exoplayer-dash:1.2.0'
implementation 'androidx.media3:media3-ui:1.2.0'
implementation 'androidx.media3:media3-exoplayer-hls:1.2.0'
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.0'
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.0'
implementation 'androidx.media3:media3-transformer:1.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
implementation 'androidx.media:media:1.7.0'
//Other
+1 -3
View File
@@ -13,7 +13,6 @@
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
<application
android:allowBackup="true"
@@ -25,8 +24,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.FutoVideo"
android:usesCleartextTraffic="true"
tools:targetApi="31"
android:largeHeap="true">
tools:targetApi="31">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/authority"
@@ -233,9 +233,6 @@ function pluginRemoteProp(objID, propName) {
function pluginRemoteCall(objID, methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
}
function pluginRemoteTest(methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
}
function pluginIsLoggedIn(cb, err) {
fetch("/plugin/isLoggedIn", {
+2 -53
View File
@@ -385,8 +385,8 @@
</v-card-text>
</v-card>
<div style="width: 50%" v-if="Plugin.currentPlugin">
<v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
<v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
<!--Get Home-->
<v-card class="requestCard" v-for="req in Testing.requests">
<v-card-text>
<div class="title">
<span v-if="req.isOptional">(Optional)</span>
@@ -416,9 +416,6 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="testSourceRemotely(req)">
Test Android
</v-btn>
<v-btn @click="testSource(req)">
Test
</v-btn>
@@ -548,7 +545,6 @@
new Vue({
el: '#app',
data: {
searchTestMethods: "",
page: "Plugin",
pastPluginUrls: [],
settings: {},
@@ -864,53 +860,6 @@
"Error: " + ex;
}
},
testSourceRemotely(req) {
const name = req.title;
const parameterVals = req.parameters.map(x=>{
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
return JSON.parse(x.value.substring(5));
return x.value
});
if(name == "enable") {
if(parameterVals.length > 0)
parameterVals[0] = this.Plugin.currentPlugin;
else
parameterVals.push(this.Plugin.currentPlugin);
if(parameterVals.length > 1)
parameterVals[1] = __DEV_SETTINGS;
else
parameterVals.push(__DEV_SETTINGS);
}
const func = source[name];
if(!func)
alert("Test func not found");
try {
const remoteResult = pluginRemoteTest(name, parameterVals);
console.log("Result for " + req.title, remoteResult);
this.Testing.lastResult = "//Results [" + name + "]\n" +
JSON.stringify(remoteResult, null, 3);
this.Testing.lastResultError = "";
}
catch(ex) {
if(ex.plugin_type == "CaptchaRequiredException") {
let shouldCaptcha = confirm("Do you want to request captcha?");
if(shouldCaptcha) {
pluginCaptchaTestPlugin(ex.url, ex.body);
}
}
console.error("Failed to run test for " + req.title, ex);
this.Testing.lastResult = ""
if(ex.message)
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex.message + "\n\n" + ex.stack;
else
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex;
}
},
showTestResults(results) {
},
+48 -124
View File
@@ -1,37 +1,13 @@
declare class ScriptException extends Error {
//If only one parameter is provided, acts as msg
constructor(type: string, msg: string);
}
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 UnavailableException extends ScriptException {
constructor(msg: string);
}
declare class ScriptImplementationException extends ScriptException {
constructor(msg: string);
}
@@ -62,23 +38,16 @@ declare class FilterCapability {
declare class PlatformAuthorLink {
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?);
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?);
}
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?,
@@ -90,26 +59,16 @@ 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,
shareUrl: string?
isLive: boolean
}
declare interface PlatformContent {}
declare class PlatformVideo implements PlatformContent {
constructor(obj: PlatformVideoDef);
}
@@ -118,15 +77,14 @@ declare class PlatformVideo implements PlatformContent {
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
description: string,
video: VideoSourceDescriptor,
live: IVideoSource,
rating: IRating,
subtitles: SubtitleSource[]
live: SubtitleSource[],
rating: IRating
}
declare class PlatformVideoDetails extends PlatformVideo {
constructor(obj: PlatformVideoDetailsDef);
}
declare interface PlatformPostDef extends PlatformContentDef {
declare class PlatformPostDef extends PlatformContentDef {
thumbnails: string[],
images: string[],
description: string
@@ -135,7 +93,7 @@ declare class PlatformPost extends PlatformContent {
constructor(obj: PlatformPostDef)
}
declare interface PlatformPostDetailsDef extends PlatformPostDef {
declare class PlatformPostDetailsDef extends PlatformPostDef {
rating: IRating,
textType: int,
content: String
@@ -152,8 +110,8 @@ declare interface MuxVideoSourceDescriptorDef {
isUnMuxed: boolean,
videoSources: VideoSource[]
}
declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
constructor(videoSourcesOrObj: VideoSource[]);
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor {
constructor(obj: VideoSourceDescriptorDef);
}
declare interface UnMuxVideoSourceDescriptorDef {
@@ -171,7 +129,7 @@ declare interface IVideoSource {
declare interface IAudioSource {
}
declare interface VideoUrlSourceDef implements IVideoSource {
interface VideoUrlSourceDef implements IVideoSource {
width: integer,
height: integer,
container: string,
@@ -181,22 +139,22 @@ declare interface VideoUrlSourceDef implements IVideoSource {
duration: integer,
url: string
}
declare class VideoUrlSource {
class VideoUrlSource {
constructor(obj: VideoUrlSourceDef);
getRequestModifier(): RequestModifier?;
}
declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
interface VideoUrlRangeSourceDef extends VideoUrlSource {
itagId: integer,
initStart: integer,
initEnd: integer,
indexStart: integer,
indexEnd: integer,
}
declare class VideoUrlRangeSource extends VideoUrlSource {
class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj: YTVideoSourceDef);
}
declare interface AudioUrlSourceDef {
interface AudioUrlSourceDef {
name: string,
bitrate: integer,
container: string,
@@ -205,12 +163,24 @@ declare interface AudioUrlSourceDef {
url: string,
language: string
}
declare class AudioUrlSource implements IAudioSource {
class AudioUrlSource implements IAudioSource {
constructor(obj: AudioUrlSourceDef);
getRequestModifier(): RequestModifier?;
}
declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
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 {
itagId: integer,
initStart: integer,
initEnd: integer,
@@ -218,46 +188,28 @@ declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
indexEnd: integer,
audioChannels: integer
}
declare class AudioUrlRangeSource extends AudioUrlSource {
class AudioUrlRangeSource extends AudioUrlSource {
constructor(obj: AudioUrlRangeSourceDef);
}
declare interface HLSSourceDef {
interface HLSSourceDef {
name: string,
duration: integer,
url: string,
priority: boolean?,
language: string?
url: string
}
declare class HLSSource implements IVideoSource {
class HLSSource implements IVideoSource {
constructor(obj: HLSSourceDef);
}
declare interface DashSourceDef {
interface DashSourceDef {
name: string,
duration: integer,
url: string,
language: string?
url: string
}
declare class DashSource implements IVideoSource {
class DashSource implements IVideoSource {
constructor(obj: DashSourceDef)
}
declare interface IRequest {
url: string,
headers: Map<string, string>?,
method: string?,
body: string?
}
declare interface IRequestModifierDef {
allowByteSkip: boolean
}
declare class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
}
//Channel
declare interface PlatformChannelDef {
interface PlatformChannelDef {
id: PlatformID,
name: string,
thumbnail: string,
@@ -265,29 +217,12 @@ declare interface PlatformChannelDef {
subscribers: integer,
description: string,
url: string,
urlAlternatives: string[],
links: Map<string>?
}
declare class PlatformChannel {
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
@@ -315,11 +250,7 @@ declare class PlatformComment {
constructor(obj: CommentDef);
}
declare class PlaybackTracker {
constructor(interval: integer);
setProgress(seconds: integer);
}
declare class LiveEventPager {
nextRequest = 4000;
@@ -330,8 +261,8 @@ declare class LiveEventPager {
nextPage(): LiveEventPager; //Could be self
}
declare class LiveEvent {
constructor(type: integer);
class LiveEvent {
type: String
}
declare class LiveEventComment extends LiveEvent {
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
@@ -356,31 +287,25 @@ declare class ContentPager {
constructor(results: PlatformContent[], hasMore: boolean);
hasMorePagers(): boolean
nextPage(): ContentPager?; //Could be self
nextPage(): VideoPager; //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
}
declare class PlaylistPager {
constructor(results: PlatformPlaylist[], hasMore: boolean);
hasMorePagers(): boolean;
nextPage(): PlaylistPager?;
nextPage(): ChannelPager; //Could be self
}
declare class CommentPager {
constructor(results: PlatformComment[], hasMore: boolean);
hasMorePagers(): boolean
nextPage(): CommentPager?; //Could be self
nextPage(): CommentPager; //Could be self
}
interface Map<T> {
@@ -416,9 +341,8 @@ interface Source {
getChannelCapabilities(): ResultCapabilities;
isContentDetailsUrl(url: string): boolean;
getContentDetails(url: string): PlatformContentDetails;
getContentDetails(url: string): PlatformVideoDetails;
//Optional
getLiveEvents(url: string): LiveEventPager;
//Optional
+4 -18
View File
@@ -37,8 +37,7 @@ let Type = {
NORMAL: 0,
SKIPPABLE: 5,
SKIP: 6,
SKIPONCE: 7
SKIP: 6
}
};
@@ -78,11 +77,6 @@ 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 }));
@@ -254,8 +248,8 @@ class PlatformVideoDetails extends PlatformVideo {
this.description = obj.description ?? "";//String
this.video = obj.video ?? {}; //VideoSourceDescriptor
this.dash = obj.dash ?? null; //DashSource, deprecated
this.hls = obj.hls ?? null; //HLSSource, deprecated
this.dash = obj.dash ?? null; //DashSource
this.hls = obj.hls ?? null; //HLSSource
this.live = obj.live ?? null; //VideoSource
this.rating = obj.rating ?? null; //IRating
@@ -326,8 +320,6 @@ 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 {
@@ -353,8 +345,6 @@ 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 {
@@ -380,8 +370,6 @@ class HLSSource {
this.priority = obj.priority ?? false;
if(obj.language)
this.language = obj.language;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class DashSource {
@@ -393,15 +381,13 @@ 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; //Kinda deprecated.. wip
this.allowByteSkip = obj.allowByteSkip;
}
}
@@ -1,6 +1,5 @@
package com.futo.platformplayer
import android.util.Log
import com.google.common.base.CharMatcher
import java.io.ByteArrayOutputStream
import java.io.IOException
@@ -10,6 +9,7 @@ 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,20 +216,15 @@ 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.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
return Socket(addresses[0], port);
} catch (e: Throwable) {
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
socket.close()
//Ignored.
}
return null;
@@ -254,7 +249,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
}
}
socket.connect(InetSocketAddress(address, port), timeout);
socket.connect(InetSocketAddress(address, port));
synchronized(syncObject) {
if (connectedSocket == null) {
@@ -268,7 +263,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
}
}
} catch (e: Throwable) {
Log.i("getConnectedSocket", "Failed to connect to: $address", e)
//Ignore
}
};
@@ -1,13 +1,11 @@
package com.futo.platformplayer
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.adapters.CommentViewHolder
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import userpackage.Protocol
import kotlin.math.abs
import kotlin.math.min
@@ -49,15 +47,6 @@ fun Protocol.Claim.resolveChannelUrls(): List<String> {
}
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) {
removeServer(PolycentricCache.STAGING_SERVER)
}
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
removeServer(PolycentricCache.SERVER)
}
val exceptions = fullyBackfillServers()
for (pair in exceptions) {
val server = pair.key
@@ -277,7 +277,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true;
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10, "background_update")
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10)
@DropdownFieldOptionsId(R.array.background_interval)
var subscriptionsBackgroundUpdateInterval: Int = 0;
@@ -685,9 +685,7 @@ class Settings : FragmentedStorageFileJson() {
fun manualCheck() {
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
SettingsActivity.getActivity()?.let {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(it, true)
}
StateUpdate.instance.checkForUpdates(it, true);
}
} else {
SettingsActivity.getActivity()?.let {
@@ -809,36 +807,7 @@ class Settings : FragmentedStorageFileJson() {
var polycentricEnabled: Boolean = true;
}
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
var gestureControls = GestureControls();
@Serializable
class GestureControls {
@FormField(R.string.volume_slider, FieldForm.TOGGLE, R.string.volume_slider_descr, 1)
var volumeSlider: Boolean = true;
@FormField(R.string.brightness_slider, FieldForm.TOGGLE, R.string.brightness_slider_descr, 2)
var brightnessSlider: Boolean = true;
@FormField(R.string.toggle_full_screen, FieldForm.TOGGLE, R.string.toggle_full_screen_descr, 3)
var toggleFullscreen: Boolean = true;
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
var useSystemBrightness: Boolean = false;
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
var useSystemVolume: Boolean = true;
@FormField(R.string.restore_system_brightness, FieldForm.TOGGLE, R.string.restore_system_brightness_descr, 6)
var restoreSystemBrightness: Boolean = true;
@FormField(R.string.zoom_option, FieldForm.TOGGLE, R.string.zoom_option_descr, 7)
var zoom: Boolean = true;
@FormField(R.string.pan_option, FieldForm.TOGGLE, R.string.pan_option_descr, 8)
var pan: Boolean = true;
}
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
var info = Info();
@Serializable
class Info {
@@ -37,7 +37,6 @@ 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
@@ -304,16 +303,12 @@ class UIDialogs {
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
}
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
fun showUpdateAvailableDialog(context: Context, lastVersion: Int) {
val dialog = AutoUpdateDialog(context);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
dialog.setMaxVersion(lastVersion);
if (hideExceptionButtons) {
dialog.hideExceptionButtons()
}
}
fun showChangelogDialog(context: Context, lastVersion: Int) {
@@ -403,28 +398,13 @@ class UIDialogs {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try {
StateApp.withContext {
toast(it, text, long);
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
}
} 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...
@@ -1,13 +1,9 @@
package com.futo.platformplayer
import android.app.NotificationManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.view.View
import android.view.ViewGroup
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@@ -67,7 +63,7 @@ class UISlideOverlays {
return menu;
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
val items = arrayListOf<View>();
val originalNotif = subscription.doNotifications;
@@ -76,13 +72,15 @@ class UISlideOverlays {
val originalVideo = subscription.doFetchVideos;
val originalPosts = subscription.doFetchPosts;
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val capabilities = plugin.getChannelCapabilities();
withContext(Dispatchers.Main) {
var menu: SlideUpMenuOverlay? = null;
items.addAll(listOf(
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
@@ -116,7 +114,7 @@ class UISlideOverlays {
}, false)*/
).filterNotNull());
menu.setItems(items);
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
if(subscription.doNotifications)
menu.selectOption(null, "notifications", true, true);
@@ -133,29 +131,8 @@ class UISlideOverlays {
subscription.save();
menu.hide(true);
if(subscription.doNotifications && !originalNotif) {
val mainContext = StateApp.instance.contextOrNull;
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
if(mainContext is MainActivity) {
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
"You need to set a Background Updating interval for notifications", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Configure", {
val intent = Intent(mainContext, SettingsActivity::class.java);
intent.putExtra("query", mainContext.getString(R.string.background_update));
mainContext.startActivity(intent);
}, UIDialogs.ActionStyle.PRIMARY));
}
return@subscribe;
}
else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
UIDialogs.toast(container.context, "Android notifications are disabled");
if(mainContext is MainActivity) {
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
}
}
if(subscription.doNotifications && !originalNotif && Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
}
};
menu.onCancel.subscribe {
@@ -171,8 +148,6 @@ class UISlideOverlays {
menu.show();
}
}
return menu;
}
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
@@ -342,7 +317,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) {
@@ -686,7 +661,7 @@ class UISlideOverlays {
}
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup, slideUpMenuOverlayUpdated: (SlideUpMenuOverlay) -> Unit): SlideUpMenuOverlay {
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>();
@@ -717,13 +692,6 @@ class UISlideOverlays {
);
val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
});
}, false))
for (playlist in allPlaylists) {
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
{
@@ -745,7 +713,7 @@ class UISlideOverlays {
}
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), invokeParents: Boolean = true, onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
@@ -753,7 +721,7 @@ class UISlideOverlays {
hidden
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
btn.handler?.invoke(btn);
}, invokeParents) as View }.toTypedArray(),
}, true) as View }.toTypedArray(),
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
val selected = it
@@ -12,7 +12,6 @@ import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
@@ -38,10 +37,8 @@ class AddSourceActivity : AppCompatActivity() {
private lateinit var _sourceHeader: SourceHeaderView;
private lateinit var _sourcePermissions: LinearLayout;
private lateinit var _sourceWarnings: LinearLayout;
private lateinit var _sourceWarningsContainer: LinearLayout;
private lateinit var _container: ScrollView;
private lateinit var _loader: ImageView;
@@ -82,7 +79,6 @@ class AddSourceActivity : AppCompatActivity() {
_sourcePermissions = findViewById(R.id.source_permissions);
_sourceWarnings = findViewById(R.id.source_warnings);
_sourceWarningsContainer = findViewById(R.id.container_source_warnings);
_container = findViewById(R.id.configContainer);
_loader = findViewById(R.id.loader);
@@ -207,30 +203,21 @@ class AddSourceActivity : AppCompatActivity() {
val pastelRed = ContextCompat.getColor(this, R.color.pastel_red);
val warnings = config.getWarnings(script);
for(warning in warnings)
for(warning in config.getWarnings(script))
_sourceWarnings.addView(
SourceInfoView(this,
R.drawable.ic_security_pred,
warning.first,
warning.second)
.withDescriptionColor(pastelRed));
_sourceWarningsContainer.isVisible = warnings.isNotEmpty();
setLoading(false);
}
fun install(config: SourcePluginConfig, script: String) {
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
if(it) {
StatePlatform.instance.clearUpdateAvailable(config)
if(isNew)
lifecycleScope.launch {
StatePlatform.instance.enableClient(listOf(config.id));
}
if(it)
backToSources();
}
}
}
@@ -16,7 +16,6 @@ class AddSourceOptionsActivity : AppCompatActivity() {
lateinit var _buttonBack: ImageButton;
lateinit var _buttonQR: BigButton;
lateinit var _buttonBrowse: BigButton;
lateinit var _buttonURL: BigButton;
lateinit var _buttonPlugins: BigButton;
@@ -57,7 +56,6 @@ class AddSourceOptionsActivity : AppCompatActivity() {
_buttonBack = findViewById(R.id.button_back);
_buttonQR = findViewById(R.id.option_qr);
_buttonBrowse = findViewById(R.id.option_browse);
_buttonURL = findViewById(R.id.option_url);
_buttonPlugins = findViewById(R.id.option_plugins);
@@ -76,9 +74,6 @@ class AddSourceOptionsActivity : AppCompatActivity() {
integrator.setCaptureActivity(QRCaptureActivity::class.java);
_qrCodeResultLauncher.launch(integrator.createScanIntent())
}
_buttonBrowse.onClick.subscribe {
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
}
_buttonURL.onClick.subscribe {
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
@@ -4,19 +4,15 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.*
import com.futo.platformplayer.logging.LogLevel
import com.futo.platformplayer.logging.Logging
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateUpdate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -30,7 +26,6 @@ class ExceptionActivity : AppCompatActivity() {
private lateinit var _buttonSubmit: LinearLayout;
private lateinit var _buttonRestart: LinearLayout;
private lateinit var _buttonClose: LinearLayout;
private lateinit var _buttonCheckForUpdates: LinearLayout;
private var _file: File? = null;
private var _submitted = false;
@@ -48,7 +43,6 @@ class ExceptionActivity : AppCompatActivity() {
_buttonSubmit = findViewById(R.id.button_submit);
_buttonRestart = findViewById(R.id.button_restart);
_buttonClose = findViewById(R.id.button_close);
_buttonCheckForUpdates = findViewById(R.id.button_check_for_updates);
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
@@ -87,17 +81,6 @@ class ExceptionActivity : AppCompatActivity() {
_buttonClose.setOnClickListener {
finish();
};
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
_buttonCheckForUpdates.visibility = View.VISIBLE
_buttonCheckForUpdates.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(this@ExceptionActivity, true, true)
}
}
} else {
_buttonCheckForUpdates.visibility = View.GONE
}
}
private fun submitFile() {
@@ -10,7 +10,6 @@ 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
@@ -40,7 +39,6 @@ class LoginActivity : AppCompatActivity() {
_textUrl = findViewById(R.id.text_url);
_buttonClose = findViewById(R.id.button_close);
_buttonClose.setOnClickListener {
UIDialogs.toast("Login cancelled", false);
finish();
}
@@ -5,7 +5,6 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
@@ -18,18 +17,14 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.app.ActivityCompat
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
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
@@ -46,7 +41,6 @@ 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.*
@@ -56,7 +50,6 @@ 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 {
@@ -68,7 +61,6 @@ 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;
@@ -142,9 +134,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
try {
runBlocking {
handleUrlAll(content)
}
handleUrlAll(content)
} catch (e: Throwable) {
Logger.i(TAG, "Failed to handle URL.", e)
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
@@ -213,7 +203,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
_overlayContainer = findViewById(R.id.overlay_container);
_toastView = findViewById(R.id.toast_view);
//_overlayContainer.visibility = View.GONE;
//Initialize fragments
@@ -484,6 +474,21 @@ 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() {
@@ -538,28 +543,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
navigate(_fragMainSources);
}
};
"BROWSE_PLUGINS" -> {
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let {
if(it is MainActivity) {
runBlocking {
it.handleUrlAll(req.url.toString());
}
}
};
}
)));
}
}
}
}
try {
if (targetData != null) {
runBlocking {
handleUrlAll(targetData)
}
handleUrlAll(targetData)
}
}
catch(ex: Throwable) {
@@ -567,7 +557,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
suspend fun handleUrlAll(url: String) {
fun handleUrlAll(url: String) {
val uri = Uri.parse(url)
when (uri.scheme) {
"grayjay" -> {
@@ -651,38 +641,23 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
suspend fun handleUrl(url: String): Boolean {
fun handleUrl(url: String): Boolean {
Logger.i(TAG, "handleUrl(url=$url)")
return withContext(Dispatchers.IO) {
Logger.i(TAG, "handleUrl(url=$url) on IO");
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found video client");
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragVideoDetail, url);
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
navigate(_fragVideoDetail, url);
_fragVideoDetail.maximizeVideoDetail(true);
return true;
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) {
navigate(_fragMainChannel, url);
_fragVideoDetail.maximizeVideoDetail(true);
}
return@withContext true;
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found channel client");
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainChannel, url);
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
return@withContext true;
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainPlaylist, url);
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
return@withContext true;
}
return@withContext false;
lifecycleScope.launch {
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
return true;
}
return false;
}
fun handleContent(file: String, mime: String? = null): Boolean {
Logger.i(TAG, "handleContent(url=$file)");
@@ -835,9 +810,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(_fragBotBarMenu.onBackPressed())
return;
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED &&
_fragVideoDetail.onBackPressed())
return;
if(!fragCurrent.onBackPressed())
closeSegment();
}
@@ -883,6 +860,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_orientationManager.disable();
StateApp.instance.mainAppDestroyed(this);
StateSaved.instance.setVideoToOpenBlocking(null);
}
inline fun <reified T> isFragmentActive(): Boolean {
@@ -1044,70 +1022,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted)
UIDialogs.toast(this, "Notification permission granted");
else
UIDialogs.toast(this, "Notification permission denied");
}
fun requestNotificationPermissions(reason: String) {
when {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
}
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
reason, null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY));
}
else -> {
requestPermissionLauncher.launch(notifPermission);
}
}
}
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.
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
@@ -17,7 +17,9 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.Synchronization
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -69,7 +71,6 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
try {
processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret);
processHandle.addServer("https://srv1-stg.polycentric.io");
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
@@ -8,15 +8,12 @@ import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret
@@ -24,9 +21,6 @@ import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.base64UrlToByteArray
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol
import userpackage.Protocol.ExportBundle
@@ -35,7 +29,6 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
private lateinit var _buttonScanProfile: LinearLayout;
private lateinit var _buttonImportProfile: LinearLayout;
private lateinit var _editProfile: EditText;
private lateinit var _loaderOverlay: LoaderOverlay;
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
@@ -59,7 +52,6 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
_buttonHelp = findViewById(R.id.button_help);
_buttonScanProfile = findViewById(R.id.button_scan_profile);
_buttonImportProfile = findViewById(R.id.button_import_profile);
_loaderOverlay = findViewById(R.id.loader_overlay);
_editProfile = findViewById(R.id.edit_profile);
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
@@ -102,57 +94,42 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
return;
}
_loaderOverlay.show()
try {
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
lifecycleScope.launch(Dispatchers.IO) {
try {
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
if (existingProcessSecret != null) {
UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported));
return;
}
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
if (existingProcessSecret != null) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
}
return@launch;
}
val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret);
val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret);
val processHandle = processSecret.toProcessHandle();
val processHandle = processSecret.toProcessHandle();
for (e in exportBundle.events.eventsList) {
try {
val se = SignedEvent.fromProto(e);
Store.instance.putSignedEvent(se);
} catch (e: Throwable) {
Logger.w(TAG, "Ignored invalid event", e);
}
}
StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
}
} catch (e: Throwable) {
Logger.w(TAG, "Failed to import profile", e);
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
}
} finally {
withContext(Dispatchers.Main) {
_loaderOverlay.hide();
for (e in exportBundle.events.eventsList) {
try {
val se = SignedEvent.fromProto(e);
Store.instance.putSignedEvent(se);
} catch (e: Throwable) {
Logger.w(TAG, "Ignored invalid event", e);
}
}
StatePolycentric.instance.setProcessHandle(processHandle);
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
} catch (e: Throwable) {
Logger.w(TAG, "Failed to import profile", e);
UIDialogs.toast(this, getString(R.string.failed_to_import_profile) + " '${e.message}'");
}
}
@@ -1,8 +1,6 @@
package com.futo.platformplayer.activities
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
@@ -14,7 +12,6 @@ import android.webkit.MimeTypeMap
import android.widget.EditText
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
@@ -24,16 +21,14 @@ import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.Synchronization
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.github.dhaval2404.imagepicker.ImagePicker
import kotlinx.coroutines.Dispatchers
@@ -51,8 +46,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String;
private lateinit var _imagePolycentric: ImageView;
private lateinit var _loaderOverlay: LoaderOverlay;
private lateinit var _textSystem: TextView;
private var _avatarUri: Uri? = null;
override fun attachBaseContext(newBase: Context?) {
@@ -70,13 +63,28 @@ class PolycentricProfileActivity : AppCompatActivity() {
_buttonExport = findViewById(R.id.button_export);
_buttonLogout = findViewById(R.id.button_logout);
_buttonDelete = findViewById(R.id.button_delete);
_loaderOverlay = findViewById(R.id.loader_overlay);
_textSystem = findViewById(R.id.text_system)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
saveIfRequired();
finish();
};
lifecycleScope.launch(Dispatchers.IO) {
try {
val processHandle = StatePolycentric.instance.processHandle!!;
Synchronization.fullyBackFillClient(processHandle, processHandle.system, "https://srv1-stg.polycentric.io");
withContext(Dispatchers.Main) {
updateUI();
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
}
}
}
updateUI();
_imagePolycentric.setOnClickListener {
ImagePicker.with(this)
.cropSquare()
@@ -112,37 +120,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
finish();
});
}
_textSystem.setOnLongClickListener {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("system", _textSystem.text)
clipboard.setPrimaryClip(clip)
return@setOnLongClickListener true
}
updateUI()
StatePolycentric.instance.processHandle?.let { processHandle ->
_loaderOverlay.show()
lifecycleScope.launch(Dispatchers.IO) {
try {
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
withContext(Dispatchers.Main) {
updateUI();
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
}
} finally {
withContext(Dispatchers.Main) {
_loaderOverlay.hide()
}
}
}
}
}
private fun saveIfRequired() {
@@ -151,17 +128,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
var hasChanges = false;
val username = _editName.text.toString();
if (username.length < 3) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
}
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
return@launch;
}
val processHandle = StatePolycentric.instance.processHandle;
if (processHandle == null) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
}
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
return@launch;
}
@@ -246,7 +219,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
private fun updateUI() {
val processHandle = StatePolycentric.instance.processHandle!!;
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
_textSystem.text = processHandle.system.key.toBase64Url()
_username = systemState.username;
_editName.text.clear();
_editName.text.append(_username);
@@ -1,10 +1,8 @@
package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
@@ -14,8 +12,6 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.logging.Logger
@@ -37,14 +33,6 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
lateinit var overlay: FrameLayout;
val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted)
UIDialogs.toast(this, "Notification permission granted");
else
UIDialogs.toast(this, "Notification permission denied");
}
override fun attachBaseContext(newBase: Context?) {
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@@ -70,33 +58,6 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
}
if(field.descriptor?.id == "background_update") {
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
if(!notifManager.areNotificationsEnabled()) {
UIDialogs.toast(this, "Notifications aren't enabled");
when {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
}
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
"Notifications need to be enabled for background updating to function", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY));
}
else -> {
requestPermissionLauncher.launch(notifPermission);
}
}
}
}
}
};
_buttonBack.setOnClickListener {
finish();
@@ -111,10 +72,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
reloadSettings();
}
var isFirstLoad = true;
fun reloadSettings() {
val firstLoad = isFirstLoad;
isFirstLoad = false;
_form.setSearchVisible(false);
_loaderView.start();
_form.fromObject(lifecycleScope, Settings.instance) {
@@ -132,13 +90,6 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
}
};
if(firstLoad) {
val query = intent.getStringExtra("query");
if(!query.isNullOrEmpty()) {
_form.setSearchQuery(query);
}
}
};
}
@@ -184,7 +135,6 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
resultLauncher.launch(intent);
}
companion object {
//TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak")
@@ -14,8 +14,7 @@ enum class ChapterType(val value: Int) {
NORMAL(0),
SKIPPABLE(5),
SKIP(6),
SKIPONCE(7);
SKIP(6);
@@ -19,9 +19,8 @@ class PolycentricPlatformComment : IPlatformComment {
val eventPointer: Pointer;
val reference: Reference;
val parentReference: Reference?;
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) {
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) {
this.contextUrl = contextUrl;
this.author = author;
this.message = msg;
@@ -30,7 +29,6 @@ class PolycentricPlatformComment : IPlatformComment {
this.replyCount = replyCount;
this.eventPointer = eventPointer;
this.reference = eventPointer.toReference();
this.parentReference = parentReference;
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
@@ -38,11 +36,10 @@ class PolycentricPlatformComment : IPlatformComment {
}
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount);
}
companion object {
private const val TAG = "PolycentricPlatformComment"
val MAX_COMMENT_SIZE = 2000
}
}
@@ -3,5 +3,4 @@ package com.futo.platformplayer.api.media.models.modifier
interface IModifierOptions {
val applyAuthClient: String?;
val applyCookieClient: String?;
val applyOtherHeaders: Boolean;
}
@@ -2,7 +2,5 @@ package com.futo.platformplayer.api.media.models.modifier
interface IRequest {
val url: String?;
val headers: Map<String, String>?;
val method: String?;
val body: String?;
val headers: Map<String, String>;
}
@@ -11,5 +11,4 @@ class SourcePluginAuthConfig(
val userAgent: String? = null,
val loginButton: String? = null,
val domainHeadersToFind: Map<String, List<String>>? = null,
val loginWarning: String? = null
) { }
@@ -2,9 +2,6 @@ package com.futo.platformplayer.api.media.platforms.js
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.views.fields.DropdownFieldOptions
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
@@ -58,16 +55,7 @@ class SourcePluginDescriptor {
onCaptchaChanged.emit();
}
fun getCaptchaData(): SourceCaptchaData? {
try {
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
}
catch(ex: Throwable) {
Logger.e("SourcePluginDescriptor", "Captcha decode failed, disabling auth.", ex);
StateAnnouncement.instance.registerAnnouncement("CAP_BROKEN_" + config.id,
"Captcha corrupted for plugin [${config.name}]",
"Something went wrong in the stored captcha, you'll have to login again", AnnouncementType.SESSION);
return null;
}
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
}
fun updateAuth(str: SourceAuth?) {
@@ -75,24 +63,12 @@ class SourcePluginDescriptor {
onAuthChanged.emit();
}
fun getAuth(): SourceAuth? {
try {
return SourceAuth.fromEncrypted(authEncrypted);
}
catch(ex: Throwable) {
Logger.e("SourcePluginDescriptor", "Authentication decode failed, disabling auth.", ex);
StateAnnouncement.instance.registerAnnouncement("AUTH_BROKEN_" + config.id,
"Authentication corrupted for plugin [${config.name}]",
"Something went wrong in the stored authentication, you'll have to login again", AnnouncementType.SESSION);
return null;
}
return SourceAuth.fromEncrypted(authEncrypted);
}
@Serializable
class AppPluginSettings {
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 1)
var checkForUpdates: Boolean = true;
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
var tabEnabled = TabEnabled();
@Serializable
@@ -13,52 +13,31 @@ class JSRequest : IRequest {
private val _v8Url: String?;
private val _v8Headers: Map<String, String>?;
private val _v8Options: Options?;
private val _v8Method: String?;
private val _v8Body: String?;
override var url: String? = null;
override lateinit var headers: Map<String, String>;
override var method: String? = null;
override var body: String? = null;
constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, method: String?, body: String? = null, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) {
constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) {
_v8Url = url;
_v8Headers = headers;
_v8Options = options;
_v8Method = method;
_v8Body = body;
initialize(plugin, originalUrl, originalHeaders);
}
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, applyOtherHeadersByDefault: Boolean = false) {
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?) {
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, applyOtherHeadersByDefault);
} ?: Options(null, null, applyOtherHeadersByDefault);
_v8Method = obj.getOrDefault<String>(config, "method", contextName, null);
_v8Body = obj.getOrDefault<String>(config, "body", contextName, null);
Options(config, it);
}
initialize(plugin, originalUrl, originalHeaders);
}
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
val config = plugin.config;
url = _v8Url ?: originalUrl;
method = _v8Method;
body = _v8Body;
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();
headers = _v8Headers ?: originalHeaders ?: mapOf();
if(_v8Options != null) {
if(_v8Options.applyCookieClient != null && url != null) {
@@ -81,7 +60,7 @@ class JSRequest : IRequest {
}
fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?): JSRequest {
return JSRequest(plugin, _v8Url, _v8Headers, _v8Method, _v8Body, _v8Options, originalUrl, originalHeaders);
return JSRequest(plugin, _v8Url, _v8Headers, _v8Options, originalUrl, originalHeaders);
}
@@ -89,18 +68,10 @@ class JSRequest : IRequest {
class Options: IModifierOptions {
override val applyAuthClient: String?;
override val applyCookieClient: String?;
override val applyOtherHeaders: Boolean;
constructor(config: IV8PluginConfig, obj: V8ValueObject, applyOtherHeadersByDefault: Boolean = false) {
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
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;
}
}
@@ -40,13 +40,9 @@ class JSRequestModifier: IRequestModifier {
} as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers);
result.close();
return req;
}
data class Request(override val url: String,
override val headers: Map<String, String>,
override val method: String? = null,
override val body: String? = null) : IRequest;
data class Request(override val url: String, override val headers: Map<String, String>) : IRequest;
}
@@ -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, true);
JSRequest(plugin, it, null, null);
}
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
}
@@ -138,7 +138,7 @@ class AirPlayCastingDevice : CastingDevice {
try {
val connectedSocket = getConnectedSocket(adrs.toList(), port);
if (connectedSocket == null) {
delay(1000);
delay(3000);
continue;
}
@@ -17,8 +17,6 @@ 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
@@ -305,18 +303,17 @@ class ChromecastCastingDevice : CastingDevice {
_thread = Thread {
connectionState = CastConnectionState.CONNECTING;
var connectedSocket: Socket? = null
while (_scopeIO?.isActive == true) {
try {
val resultSocket = getConnectedSocket(adrs.toList(), port);
if (resultSocket == null) {
Thread.sleep(1000);
val connectedSocket = getConnectedSocket(adrs.toList(), port);
if (connectedSocket == null) {
Thread.sleep(3000);
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)
@@ -328,8 +325,6 @@ class ChromecastCastingDevice : CastingDevice {
val factory = sslContext.socketFactory;
val address = InetSocketAddress(usedRemoteAddress, port)
//Connection loop
while (_scopeIO?.isActive == true) {
Logger.i(TAG, "Connecting to Chromecast.");
@@ -337,16 +332,7 @@ class ChromecastCastingDevice : CastingDevice {
try {
_socket?.close()
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 = factory.createSocket(usedRemoteAddress, port) as SSLSocket;
_socket?.startHandshake();
Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port");
@@ -361,7 +347,7 @@ class ChromecastCastingDevice : CastingDevice {
Logger.i(TAG, "Failed to connect to Chromecast.", e);
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(1000);
Thread.sleep(3000);
continue;
}
@@ -377,7 +363,7 @@ class ChromecastCastingDevice : CastingDevice {
_socket?.close();
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(1000);
Thread.sleep(3000);
continue;
}
@@ -429,7 +415,7 @@ class ChromecastCastingDevice : CastingDevice {
Logger.i(TAG, "Socket disconnected.");
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(1000);
Thread.sleep(3000);
}
Logger.i(TAG, "Stopped connection loop.");
@@ -446,11 +432,10 @@ 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,7 +15,6 @@ 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
@@ -28,12 +27,11 @@ 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
@@ -83,15 +81,12 @@ class FCastCastingDevice : CastingDevice {
var port: Int = 0;
private var _socket: Socket? = null;
private var _outputStream: OutputStream? = null;
private var _inputStream: InputStream? = null;
private var _outputStream: DataOutputStream? = null;
private var _inputStream: DataInputStream? = 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;
@@ -211,13 +206,7 @@ class FCastCastingDevice : CastingDevice {
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
if(Looper.getMainLooper().thread == Thread.currentThread()) {
_scopeIO?.launch {
try {
action();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to invoke in IO scope.", e)
}
}
_scopeIO?.launch { action(); }
return true;
}
@@ -251,113 +240,77 @@ class FCastCastingDevice : CastingDevice {
val adrs = addresses ?: return;
val thread = _thread
val pingThread = _pingThread
if (_started && (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.")
}
if (thread == null || !thread.isAlive) {
Log.i(TAG, "Restarting thread because the thread has died")
_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 {
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);
val connectedSocket = getConnectedSocket(adrs.toList(), port);
if (connectedSocket == null) {
Thread.sleep(3000);
continue;
}
Log.i(TAG, "Connection succeeded.")
connectedSocket = resultSocket
usedRemoteAddress = connectedSocket.inetAddress
localAddress = connectedSocket.localAddress
usedRemoteAddress = connectedSocket.inetAddress;
localAddress = connectedSocket.localAddress;
connectedSocket.close();
break;
} catch (e: Throwable) {
Logger.w(TAG, "Failed to get setup initial connection to FastCast device.", e)
Logger.w(ChromecastCastingDevice.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?.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) };
}
_socket = Socket(usedRemoteAddress, port);
Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port");
_outputStream = _socket?.outputStream;
_inputStream = _socket?.inputStream;
_outputStream = DataOutputStream(_socket?.outputStream);
_inputStream = DataInputStream(_socket?.inputStream);
} catch (e: IOException) {
_socket?.close()
_inputStream?.close()
_outputStream?.close()
_socket?.close();
Logger.i(TAG, "Failed to connect to FastCast.", e);
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(1000);
Thread.sleep(3000);
continue;
}
localAddress = _socket?.localAddress;
connectionState = CastConnectionState.CONNECTED;
_lastPongTime = -1L
val buffer = ByteArray(4096);
Logger.i(TAG, "Started receiving.");
while (_scopeIO?.isActive == true) {
var exceptionOccurred = false;
while (_scopeIO?.isActive == true && !exceptionOccurred) {
try {
val inputStream = _inputStream ?: break;
Log.d(TAG, "Receiving next packet...");
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();
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();
if (size > buffer.size) {
Logger.w(TAG, "Packets larger than $size bytes are not supported.")
break
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
inputStream.skip(size.toLong());
continue;
}
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
var bytesRead = 0
while (bytesRead < size) {
val read = inputStream.read(buffer, bytesRead, size - bytesRead)
if (read == -1)
throw Exception("Stream closed")
bytesRead += read
}
inputStream.read(buffer, 0, size);
val messageBytes = buffer.sliceArray(IntRange(0, size));
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
@@ -371,68 +324,31 @@ class FCastCastingDevice : CastingDevice {
try {
handleMessage(Opcode.find(opcode), json);
} catch (e: Throwable) {
Logger.w(TAG, "Failed to handle message.", e)
break
Logger.w(TAG, "Failed to handle message.", e);
}
} catch (e: java.net.SocketException) {
Logger.e(TAG, "Socket exception while receiving.", e);
break
exceptionOccurred = true;
} catch (e: Throwable) {
Logger.e(TAG, "Exception while receiving.", e);
break
exceptionOccurred = true;
}
}
try {
_socket?.close()
_inputStream?.close()
_outputStream?.close()
_socket?.close();
Logger.i(TAG, "Socket disconnected.");
} catch (e: Throwable) {
Logger.e(TAG, "Failed to close socket.", e)
}
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(1000);
Thread.sleep(3000);
}
Logger.i(TAG, "Stopped connection loop.");
connectionState = CastConnectionState.DISCONNECTED;
}.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() }
}.apply { start() };
} else {
Log.i(TAG, "Thread was still alive, not restarted")
}
@@ -485,44 +401,39 @@ 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) {
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
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
}
}
@@ -542,7 +453,6 @@ class FCastCastingDevice : CastingDevice {
_started = false;
//TODO: Kill and/or join thread?
_thread = null;
_pingThread = null;
val socket = _socket;
val scopeIO = _scopeIO;
@@ -552,8 +462,6 @@ class FCastCastingDevice : CastingDevice {
scopeIO.launch {
socket.close();
_inputStream?.close()
_outputStream?.close()
connectionState = CastConnectionState.DISCONNECTED;
scopeIO.cancel();
Logger.i(TAG, "Cancelled scopeIO with open socket.")
@@ -9,10 +9,7 @@ import com.futo.platformplayer.api.http.server.HttpGET
import com.futo.platformplayer.api.http.server.HttpPOST
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.dev.V8RemoteObject
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
@@ -23,29 +20,18 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateAssets
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StatePlatform
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Modifier
import java.util.UUID
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.jvm.javaType
import kotlin.reflect.jvm.jvmErasure
class DeveloperEndpoints(private val context: Context) {
private val TAG = "DeveloperEndpoints";
private val _client = ManagedHttpClient();
private var _testPlugin: V8Plugin? = null;
private var _testPluginFull: JSClient? = null;
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
@@ -204,17 +190,6 @@ class DeveloperEndpoints(private val context: Context) {
val client = JSHttpClient(null, null, null, config);
val clientAuth = JSHttpClient(null, null, null, config);
_testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth);
try {
val script = _client.get(config.absoluteScriptUrl);
_testPluginFull = JSClient(StateApp.instance.context, SourcePluginDescriptor(
config, null, null, null
), null, script.body?.string() ?: "");
_testPluginFull!!.initialize();
}
catch (ex: Throwable) {
Logger.e(TAG, "Loading full client failed", ex);
_testPluginFull = null;
}
context.respondJson(200, testPluginOrThrow.getPackageVariables());
}
@@ -465,68 +440,6 @@ class DeveloperEndpoints(private val context: Context) {
}
}
private val _fieldAttributesField = FieldAttributes::class.java.getDeclaredField("field");
init {
_fieldAttributesField.isAccessible = true;
}
private val _remoteTestGson = GsonBuilder()
.setExclusionStrategies(object : ExclusionStrategy {
override fun shouldSkipClass(clazz: Class<*>?): Boolean {
return clazz?.simpleName == "JSClient" ||
clazz?.simpleName == "KSerializer[]" ||
clazz?.simpleName == "V8ValueObject";
}
override fun shouldSkipField(f: FieldAttributes?): Boolean {
val isPublic = f?.hasModifier(Modifier.PUBLIC) ?: true;
if(!isPublic) {
val underlyingField = _fieldAttributesField.get(f) as Field;
return !(underlyingField.declaringClass as Class).methods.any { it.name == "get" + underlyingField.name.replaceFirstChar { it.uppercaseChar() } && Modifier.isPublic(it.modifiers) };
}
else
return !isPublic;
}
}).create();
@HttpPOST("/plugin/remoteTest")
fun pluginRemoteTest(context: HttpContext) {
val method = context.query.getOrDefault("method", "");
try {
val parameters = context.readContentString();
val paras = JsonParser.parseString(parameters);
if(!paras.isJsonArray)
throw IllegalArgumentException("Expected json array as body");
val plugin = _testPluginFull ?: throw IllegalStateException("Plugin not loaded");
val function = plugin::class.memberFunctions.filter { it.findAnnotation<JSDocs>() != null }
.find { it.name == method };
if(function == null)
throw java.lang.IllegalArgumentException("Plugin method [${function}] not found");
val callResult = function.call(*(listOf(plugin) + paras.asJsonArray.take(function.parameters.size - 1).mapIndexed { index, jsonElement ->
//For now, manual conversion.
val parameter = function.parameters[index + 1];
val value = _remoteTestGson.fromJson<Any>(jsonElement, parameter.type.javaType);
return@mapIndexed value;
}).toTypedArray());
val json = if(callResult is IPager<*>)
_remoteTestGson.toJson(callResult.getResults())
else
_remoteTestGson.toJson(callResult);
//val json = wrapRemoteResult(callResult, false);
context.respondCode(200, json);
}
catch(ex: InvocationTargetException) {
Logger.e(TAG, "Remote test for [${method}] is failed", ex.targetException);
context.respondCode(500, ex.targetException.message ?: "", "text/plain")
}
catch(ex: Exception) {
Logger.e(TAG, "Remote test for [${method}] is failed", ex);
context.respondCode(500, ex.message ?: "", "text/plain")
}
}
//Internal calls
@HttpPOST("/get")
fun get(context: HttpContext) {
@@ -96,11 +96,6 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
}
fun hideExceptionButtons() {
_buttonNever.visibility = View.GONE
_buttonShowChangelog.visibility = View.GONE
}
private fun update() {
_buttonShowChangelog.visibility = Button.GONE;
_buttonNever.visibility = Button.GONE;
@@ -6,12 +6,11 @@ import android.graphics.Color
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.*
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.PlatformID
@@ -26,11 +25,7 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.polycentric.core.ClaimType
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.futo.polycentric.core.*
import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -98,7 +93,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
val comment = _editComment.text.toString();
val processHandle = StatePolycentric.instance.processHandle!!
val eventPointer = processHandle.post(comment, ref)
val eventPointer = processHandle.post(comment, null, ref)
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
@@ -123,8 +118,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
msg = comment,
rating = RatingLikeDislikes(0, 0),
date = OffsetDateTime.now(),
eventPointer = eventPointer,
parentReference = ref
eventPointer = eventPointer
));
dismiss();
@@ -7,7 +7,6 @@ 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
@@ -27,8 +26,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: ImageButton;
private lateinit var _buttonScanQR: ImageButton;
private lateinit var _buttonAdd: Button;
private lateinit var _buttonScanQR: Button;
private lateinit var _textNoDevicesFound: TextView;
private lateinit var _textNoDevicesRemembered: TextView;
private lateinit var _recyclerDevices: RecyclerView;
@@ -143,9 +143,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
val dur = it.toFloat().coerceAtLeast(1.0f)
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur);
_sliderPosition.valueTo = dur
_sliderPosition.valueTo = it.toFloat().coerceAtLeast(1.0f);
};
_device = StateCasting.instance.activeDevice;
@@ -187,10 +185,8 @@ 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);
val dur = d.duration.toFloat().coerceAtLeast(1.0f)
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
_sliderPosition.valueTo = dur
_sliderPosition.valueTo = d.duration.toFloat().coerceAtLeast(1.0f);
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
if (d.canSetVolume) {
_layoutVolumeAdjustable.visibility = View.VISIBLE;
@@ -337,10 +337,8 @@ class VideoDownload {
});
}
var wasSuccesful = false;
try {
awaitAll(*sourcesToDownload.toTypedArray());
wasSuccesful = true;
}
catch(runtimeEx: RuntimeException) {
if(runtimeEx.cause != null)
@@ -351,29 +349,6 @@ 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 {
@@ -2,7 +2,6 @@ package com.futo.platformplayer.engine
import android.content.Context
import com.caoccao.javet.exceptions.JavetCompilationException
import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interop.V8Host
import com.caoccao.javet.interop.V8Runtime
@@ -11,7 +10,6 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1
@@ -31,6 +29,7 @@ import com.futo.platformplayer.engine.internal.V8Converter
import com.futo.platformplayer.engine.packages.PackageBridge
import com.futo.platformplayer.engine.packages.PackageDOMParser
import com.futo.platformplayer.engine.packages.PackageHttp
import com.futo.platformplayer.engine.packages.PackageHttpPromises
import com.futo.platformplayer.engine.packages.PackageUtilities
import com.futo.platformplayer.engine.packages.V8Package
import com.futo.platformplayer.getOrThrow
@@ -175,16 +174,8 @@ class V8Plugin {
isStopped = true;
_runtime?.let {
_runtime = null;
if(!it.isClosed && !it.isDead) {
try {
it.close();
}
catch(ex: JavetException) {
//In case race conditions are going on, already closed runtimes are fine.
if(ex.message?.contains("Runtime is already closed") != true)
throw ex;
}
}
if(!it.isClosed && !it.isDead)
it.close();
Logger.i(TAG, "Stopped plugin [${config.name}]");
};
}
@@ -249,6 +240,7 @@ class V8Plugin {
return when(packageName) {
"DOMParser" -> PackageDOMParser(this)
"Http" -> PackageHttp(this, config)
"HttpPromises" -> PackageHttpPromises(this, config)
"Utilities" -> PackageUtilities(this, config)
else -> throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
};
@@ -5,11 +5,8 @@ import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.annotations.V8Property
import com.caoccao.javet.enums.V8ConversionMode
import com.caoccao.javet.enums.V8ProxyMode
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.internal.V8BindObject
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
@@ -68,7 +65,7 @@ class PackageDOMParser : V8Package {
return result;
}
@V8Property
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
fun attributes(): Map<String, String> = _element.attributes().dataset();
@V8Property
fun innerHTML(): String = _element.html();
@V8Property
@@ -141,32 +138,10 @@ class PackageDOMParser : V8Package {
super.dispose();
}
@V8Function
fun toNodeTree(): SerializedNode {
return SerializedNode(
childNodes().map { it.toNodeTree() },
_element.tagName(),
_element.text(),
attributes()
);
}
@V8Function
fun toNodeTreeJson(): String {
return Json.encodeToString(SerializedNode.serializer(), toNodeTree());
}
companion object {
fun parse(parser: PackageDOMParser, str: String): DOMNode {
return DOMNode(parser, Jsoup.parse(str));
}
}
@Serializable
class SerializedNode(
val children: List<SerializedNode>,
val name: String,
val value: String,
val attributes: Map<String, String>
);
}
}
@@ -0,0 +1,478 @@
package com.futo.platformplayer.engine.packages
import com.caoccao.javet.annotations.V8Convert
import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.annotations.V8Property
import com.caoccao.javet.enums.V8ConversionMode
import com.caoccao.javet.enums.V8ProxyMode
import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.interop.V8Runtime
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.internal.IV8Convertable
import com.futo.platformplayer.engine.internal.V8BindObject
import com.futo.platformplayer.logging.Logger
import okhttp3.internal.concurrent.Task
import java.net.SocketTimeoutException
import kotlin.streams.asSequence
class PackageHttpPromises: V8Package {
@Transient
private val _config: IV8PluginConfig;
@Transient
private val _client: ManagedHttpClient
@Transient
private val _clientAuth: ManagedHttpClient
@Transient
private val _packageClient: PackageHttpClient;
@Transient
private val _packageClientAuth: PackageHttpClient
override val name: String get() = "HttpPromises";
override val variableName: String get() = "httpx";
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
_config = config;
_client = plugin.httpClient;
_clientAuth = plugin.httpClientAuth;
_packageClient = PackageHttpClient(this, _client);
_packageClientAuth = PackageHttpClient(this, _clientAuth);
}
@V8Function
fun newClient(withAuth: Boolean): PackageHttpClient {
return PackageHttpClient(this, if(withAuth) _clientAuth.clone() else _client.clone());
}
@V8Function
fun getDefaultClient(withAuth: Boolean): PackageHttpClient {
return if(withAuth) _packageClientAuth else _packageClient;
}
@V8Function
fun batch(): BatchBuilder {
return BatchBuilder(this);
}
@V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
return if(useAuth)
_packageClientAuth.request(method, url, headers)
else
_packageClient.request(method, url, headers);
}
@V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
return if(useAuth)
_packageClientAuth.requestWithBody(method, url, body, headers)
else
_packageClient.requestWithBody(method, url, body, headers);
}
@V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
return if(useAuth)
_packageClientAuth.GET(url, headers)
else
_packageClient.GET(url, headers);
}
@V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
return if(useAuth)
_packageClientAuth.POST(url, body, headers)
else
_packageClient.POST(url, body, headers);
}
@V8Function
fun socket(url: String, headers: Map<String, String>? = null, useAuth: Boolean = false): SocketResult {
return if(useAuth)
_packageClientAuth.socket(url, headers)
else
_packageClient.socket(url, headers);
}
private fun <T> logExceptions(handle: ()->T): T {
try {
return handle();
}
catch(ex: Exception) {
Logger.e("Plugin[${_config.name}]", ex.message, ex);
throw ex;
}
}
@kotlinx.serialization.Serializable
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
val isOk = code >= 200 && code < 300;
override fun toV8(runtime: V8Runtime): V8Value? {
val obj = runtime.createV8ValueObject();
obj.set("url", url);
obj.set("code", code);
obj.set("body", body);
obj.set("headers", headers);
obj.set("isOk", isOk);
// return obj;
val v8ValuePromiseResolver = runtime.createV8ValuePromise()
v8ValuePromiseResolver.promise.resolve(obj);
return v8ValuePromiseResolver.promise
}
}
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
class BatchBuilder(private val _package: PackageHttpPromises, existingRequests: MutableList<Pair<PackageHttpClient, RequestDescriptor>> = mutableListOf()): V8BindObject() {
@Transient
private val _reqs = existingRequests;
@V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder {
return clientRequest(_package.getDefaultClient(useAuth), method, url, headers);
}
@V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder {
return clientRequestWithBody(_package.getDefaultClient(useAuth), method, url, body, headers);
}
@V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientGET(_package.getDefaultClient(useAuth), url, headers);
@V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
//Client-specific
@V8Function
fun clientRequest(client: PackageHttpClient, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(client, RequestDescriptor(method, url, headers)));
return BatchBuilder(_package, _reqs);
}
@V8Function
fun clientRequestWithBody(client: PackageHttpClient, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(client, RequestDescriptor(method, url, headers, body)));
return BatchBuilder(_package, _reqs);
}
@V8Function
fun clientGET(client: PackageHttpClient, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequest(client, "GET", url, headers);
@V8Function
fun clientPOST(client: PackageHttpClient, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequestWithBody(client, "POST", url, body, headers);
//Finalizer
@V8Function
fun execute(): List<BridgeHttpResponse> {
return _reqs.parallelStream().map {
if(it.second.body != null)
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers);
else
return@map it.first.request(it.second.method, it.second.url, it.second.headers);
}
.asSequence()
.toList();
}
}
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
class PackageHttpClient : V8BindObject {
@Transient
private val _package: PackageHttpPromises;
@Transient
private val _client: ManagedHttpClient;
@Transient
private val _defaultHeaders = mutableMapOf<String, String>();
constructor(pack: PackageHttpPromises, baseClient: ManagedHttpClient): super() {
_package = pack;
_client = baseClient;
}
@V8Function
fun setDefaultHeaders(defaultHeaders: Map<String, String>): PackageHttpClient {
for(pair in defaultHeaders)
_defaultHeaders[pair.key] = pair.value;
return this;
}
@V8Function
fun setDoApplyCookies(apply: Boolean): PackageHttpClient {
if(_client is JSHttpClient)
_client.doApplyCookies = apply;
return this;
}
@V8Function
fun setDoUpdateCookies(update: Boolean): PackageHttpClient {
if(_client is JSHttpClient)
_client.doUpdateCookies = update;
return this;
}
@V8Function
fun setDoAllowNewCookies(allow: Boolean): PackageHttpClient {
if(_client is JSHttpClient)
_client.doAllowNewCookies = allow;
return this;
}
@V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
return@logExceptions catchHttp {
val client = _client;
//logRequest(method, url, headers, null);
val resp = client.requestMethod(method, url, headers);
val responseBody = resp.body?.string();
//logResponse(method, url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
}
};
}
@V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
catchHttp {
val client = _client;
//logRequest(method, url, headers, body);
val resp = client.requestMethod(method, url, body, headers);
val responseBody = resp.body?.string();
//logResponse(method, url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
}
};
}
@V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
catchHttp {
val client = _client;
//logRequest("GET", url, headers, null);
val resp = client.get(url, headers);
val responseBody = resp.body?.string();
//logResponse("GET", url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
}
};
}
@V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
catchHttp {
val client = _client;
//logRequest("POST", url, headers, body);
val resp = client.post(url, body, headers);
val responseBody = resp.body?.string();
//logResponse("POST", url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
}
};
}
@V8Function
fun socket(url: String, headers: Map<String, String>? = null): SocketResult {
val socketHeaders = headers?.toMutableMap() ?: HashMap();
applyDefaultHeaders(socketHeaders);
return SocketResult(this, _client, url, socketHeaders);
}
private fun applyDefaultHeaders(headerMap: MutableMap<String, String>) {
synchronized(_defaultHeaders) {
for(toApply in _defaultHeaders)
if(!headerMap.containsKey(toApply.key))
headerMap[toApply.key] = toApply.value;
}
}
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?): Map<String, List<String>> {
val result = mutableMapOf<String, List<String>>()
headers?.forEach { (header, values) ->
val lowerCaseHeader = header.lowercase()
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
result[lowerCaseHeader] = values
}
}
return result
}
/*private fun logRequest(method: String, url: String, headers: Map<String, String> = HashMap(), body: String?) {
Logger.v(TAG) {
val stringBuilder = StringBuilder();
stringBuilder.appendLine("HTTP request (useAuth = )");
stringBuilder.appendLine("$method $url");
for (pair in headers) {
stringBuilder.appendLine("${pair.key}: ${pair.value}");
}
if (body != null) {
stringBuilder.appendLine();
stringBuilder.appendLine(body);
}
return@v stringBuilder.toString();
};
}*/
/*private fun logResponse(method: String, url: String, responseCode: Int? = null, responseHeaders: Map<String, List<String>> = HashMap(), responseBody: String? = null) {
Logger.v(TAG) {
val stringBuilder = StringBuilder();
if (responseCode != null) {
stringBuilder.appendLine("HTTP response (${responseCode})");
stringBuilder.appendLine("$method $url");
for (pair in responseHeaders) {
if (pair.key.equals("authorization", ignoreCase = true) || pair.key.equals("set-cookie", ignoreCase = true)) {
stringBuilder.appendLine("${pair.key}: @CENSOREDVALUE@");
} else {
stringBuilder.appendLine("${pair.key}: ${pair.value.joinToString("; ")}");
}
}
if (responseBody != null) {
stringBuilder.appendLine();
stringBuilder.appendLine(responseBody);
}
} else {
stringBuilder.appendLine("No response");
}
return@v stringBuilder.toString();
};
}*/
fun <T> logExceptions(handle: ()->T): T {
try {
return handle();
}
catch(ex: Exception) {
Logger.e("Plugin[${_package._config.name}]", ex.message, ex);
throw ex;
}
}
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
try{
return handle();
}
//Forward timeouts
catch(ex: SocketTimeoutException) {
return BridgeHttpResponse("", 408, null);
}
}
}
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
class SocketResult: V8BindObject {
private var _isOpen = false;
private var _socket: ManagedHttpClient.Socket? = null;
private var _listeners: V8ValueObject? = null;
private val _packageClient: PackageHttpClient;
private val _client: ManagedHttpClient;
private val _url: String;
private val _headers: Map<String, String>;
constructor(pack: PackageHttpClient, client: ManagedHttpClient, url: String, headers: Map<String,String>) {
_packageClient = pack;
_client = client;
_url = url;
_headers = headers;
}
@V8Property
fun isOpen(): Boolean = _isOpen; //TODO
@V8Function
fun connect(socketObj: V8ValueObject) {
val hasOpen = socketObj.has("open");
val hasMessage = socketObj.has("message");
val hasClosing = socketObj.has("closing");
val hasClosed = socketObj.has("closed");
val hasFailure = socketObj.has("failure");
//socketObj.setWeak(); //We have to manage this lifecycle
_listeners = socketObj;
_socket = _packageClient.logExceptions {
val client = _client;
return@logExceptions client.socket(_url, _headers.toMutableMap(), object: ManagedHttpClient.SocketListener {
override fun open() {
Logger.i(TAG, "Websocket opened: " + _url);
_isOpen = true;
if(hasOpen)
_listeners?.invokeVoid("open", arrayOf<Any>());
}
override fun message(msg: String) {
if(hasMessage) {
try {
_listeners?.invokeVoid("message", msg);
}
catch(ex: Throwable) {}
}
}
override fun closing(code: Int, reason: String) {
if(hasClosing)
_listeners?.invokeVoid("closing", code, reason);
}
override fun closed(code: Int, reason: String) {
_isOpen = false;
if(hasClosed)
_listeners?.invokeVoid("closed", code, reason);
}
override fun failure(exception: Throwable) {
_isOpen = false;
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
if(hasFailure)
_listeners?.invokeVoid("failure", exception.message);
}
});
};
}
@V8Function
fun send(msg: String) {
_socket?.send(msg);
}
}
data class RequestDescriptor(
val method: String,
val url: String,
val headers: MutableMap<String, String>,
val body: String? = null,
val contentType: String? = null
)
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
try{
return handle();
}
//Forward timeouts
catch(ex: SocketTimeoutException) {
return BridgeHttpResponse("", 408, null);
}
}
companion object {
private const val TAG = "PackageHttp";
private val WHITELISTED_RESPONSE_HEADERS = listOf("content-type", "date", "content-length", "last-modified", "etag", "cache-control", "content-encoding", "content-disposition", "connection")
}
}
@@ -27,7 +27,6 @@ import com.futo.platformplayer.constructs.Event2
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.fragment.mainactivity.main.FeedView
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger
@@ -337,11 +336,8 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
context?.let {
lifecycleScope.launch(Dispatchers.Main) {
try {
val channel = if(kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null;
if(jsVideoPager != null)
UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n" +
(if(!channel.isNullOrEmpty()) "(${channel}) " else "") +
"${kv.value.message}", false);
UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n${kv.value.message}", false);
else
UIDialogs.toast(it, kv.value.message ?: "", false);
} catch (e: Throwable) {
@@ -247,14 +247,11 @@ class MenuBottomBarFragment : MainActivityFragment() {
val defs = currentButtonDefinitions?.toMutableList() ?: return
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
if (_buttonsVisible >= defs.size) {
if (_buttonsVisible - 1 >= defs.size) {
updateBottomMenuButtons(defs.toMutableList(), false);
} else if (_buttonsVisible > 0) {
updateBottomMenuButtons(defs.take(_buttonsVisible - 1).toMutableList(), true);
updateMoreButtons(defs.drop(_buttonsVisible - 1).toMutableList());
} else {
updateBottomMenuButtons(mutableListOf(), false)
updateMoreButtons(defs.toMutableList())
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
}
}
@@ -292,17 +289,10 @@ class MenuBottomBarFragment : MainActivityFragment() {
buttonDefinitions.find { d -> d.id == it.id }
}.toMutableList()
//Add unconfigured tabs with default values
buttonDefinitions.forEach { buttonDefinition ->
if (!Settings.instance.tabs.any { it.id == buttonDefinition.id }) {
newCurrentButtonDefinitions.add(buttonDefinition)
}
}
if (!StatePayment.instance.hasPaid) {
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(98, R.drawable.ic_paid, R.drawable.ic_paid, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
}
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.faq, canToggle = false, { false }, {
it.navigate<BrowserFragment>(Settings.URL_FAQ);
}))
@@ -359,8 +349,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_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 }, {
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 }, {
val c = it.context ?: return@ButtonDefinition;
Logger.i(TAG, "settings preventPictureInPicture()");
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
@@ -22,17 +22,15 @@ class BrowserFragment : MainFragment() {
override val hasBottomBar: Boolean get() = true;
private var _webview: WebView? = null;
private val _webviewWithoutHandling = object: WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return false;
}
};
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_browser, container, false);
_webview = view.findViewById<WebView?>(R.id.webview).apply {
this.webViewClient = _webviewWithoutHandling;
this.webViewClient = object: WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return false;
}
};
this.settings.javaScriptEnabled = true;
CookieManager.getInstance().setAcceptCookie(true);
this.settings.domStorageEnabled = true;
@@ -43,26 +41,8 @@ class BrowserFragment : MainFragment() {
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack)
if(parameter is String) {
_webview?.webViewClient = _webviewWithoutHandling;
if(parameter is String)
_webview?.loadUrl(parameter);
}
else if(parameter is NavigateOptions) {
if(parameter.urlHandlers != null && parameter.urlHandlers.isNotEmpty())
_webview?.webViewClient = object: WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val schema = request?.url?.scheme;
if(schema != null && parameter.urlHandlers.containsKey(schema)) {
parameter.urlHandlers[schema]?.invoke(request);
return true;
}
return false;
}
};
else
_webview?.webViewClient = _webviewWithoutHandling;
_webview?.loadUrl(parameter.url);
}
else
_webview?.loadUrl("about:blank");
}
@@ -79,9 +59,4 @@ class BrowserFragment : MainFragment() {
companion object {
fun newInstance() = BrowserFragment().apply {}
}
class NavigateOptions(
val url: String,
val urlHandlers: Map<String, (WebResourceRequest)->Unit>? = null
)
}
@@ -418,7 +418,6 @@ 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..
@@ -466,7 +465,7 @@ class ChannelFragment : MainFragment() {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
@@ -117,7 +117,6 @@ class CommentsFragment : MainFragment() {
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
holder.onDelete.subscribe(::onDelete);
holder.onRepliesClick.subscribe(::onRepliesClick);
holder.onClick.subscribe(::onClick);
return@InsertedViewAdapterWithLoader holder;
}
);
@@ -201,17 +200,6 @@ class CommentsFragment : MainFragment() {
return false
}
private fun onClick(c: IPlatformComment) {
if (c !is PolycentricPlatformComment) {
return
}
val parentRef = c.parentReference
if (parentRef != null && _repliesOverlay.handleParentClick(c.contextUrl, parentRef)) {
setRepliesOverlayVisible(true, true)
}
}
private fun onRepliesClick(c: IPlatformComment) {
val replyCount = c.replyCount ?: 0;
var metadata = "";
@@ -154,14 +154,8 @@ class ContentSearchResultsFragment : MainFragment() {
};
onSearch.subscribe(this) {
if(it.isHttpUrl()) {
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
navigate<PlaylistFragment>(it);
else if(StatePlatform.instance.hasEnabledChannelClient(it))
navigate<ChannelFragment>(it);
else
navigate<VideoDetailFragment>(it);
}
if(it.isHttpUrl())
navigate<VideoDetailFragment>(it);
else
setQuery(it, true);
};
@@ -8,7 +8,6 @@ import android.view.ViewGroup
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.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.EmptyPager
@@ -18,23 +17,15 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.NoResultsView
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView
import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.OffsetDateTime
import java.util.UUID
@@ -156,42 +147,6 @@ class HomeFragment : MainFragment() {
finishRefreshLayoutLoader();
}
override fun getEmptyPagerView(): View? {
val dp10 = 10.dp(resources);
val dp30 = 30.dp(resources);
val pluginsExist = StatePlatform.instance.getAvailableClients().isNotEmpty();
if(StatePlatform.instance.getEnabledClients().isEmpty())
//Initial setup
return NoResultsView(context, "No enabled sources", if(pluginsExist)
"Enable or install some sources"
else "This Grayjay version comes without any sources, install sources externally or using the button below.", R.drawable.ic_sources,
listOf(BigButton(context, "Browse Online Sources", "View official sources online", R.drawable.ic_explore) {
fragment.navigate<BrowserFragment>(BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let {
if(it is MainActivity) {
runBlocking {
it.handleUrlAll(req.url.toString());
}
}
};
}
)));
}.withMargin(dp10, dp30),
if(pluginsExist) BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
fragment.navigate<SourcesFragment>();
}.withMargin(dp10, dp30) else null).filterNotNull()
);
else
return NoResultsView(context, "Nothing to see here", "The enabled sources do not have any results.", R.drawable.ic_help,
listOf(BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
fragment.navigate<SourcesFragment>();
}.withMargin(dp10, dp30))
);
return null;
}
override fun reload() {
loadResults();
}
@@ -206,15 +161,13 @@ class HomeFragment : MainFragment() {
}
private fun loadedResult(pager : IPager<IPlatformContent>) {
if (pager is EmptyPager<IPlatformContent>) {
//StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
}
Logger.i(TAG, "Got new home pager ${pager}");
finishRefreshLayoutLoader();
setLoading(false);
setPager(pager);
if(pager.getResults().isEmpty() && !pager.hasMorePages())
setEmptyPager(true);
}
}
@@ -314,8 +314,8 @@ class PostDetailFragment : MainFragment {
private fun updatePolycentricRating() {
_rating.visibility = View.GONE;
val ref = Models.referenceFromBuffer((_post?.url ?: _postOverview?.url)?.toByteArray() ?: return)
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.let { if (it.isNotEmpty()) it.toByteArray() else null }
val value = _post?.id?.value ?: _postOverview?.id?.value ?: return;
val ref = Models.referenceFromBuffer(value.toByteArray());
val version = _version;
_rating.onLikeDislikeUpdated.remove(this);
@@ -333,8 +333,7 @@ class PostDetailFragment : MainFragment {
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.dislike.data)).build()
),
extraByteReferences = listOfNotNull(extraBytesRef)
)
);
if (version != _version) {
@@ -343,8 +342,8 @@ class PostDetailFragment : MainFragment {
val likes = queryReferencesResponse.countsList[0];
val dislikes = queryReferencesResponse.countsList[1];
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
val hasLiked = StatePolycentric.instance.hasLiked(ref);
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
withContext(Dispatchers.Main) {
if (version != _version) {
@@ -469,7 +468,9 @@ class PostDetailFragment : MainFragment {
if (_postOverview == null) {
fetchPolycentricProfile();
updatePolycentricRating();
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
_addCommentView.setContext(value.url, ref);
}
updateCommentType(true);
@@ -488,7 +489,9 @@ class PostDetailFragment : MainFragment {
_textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count?
_textContent.text = value.description.fixHtmlWhitespace();
_platformIndicator.setPlatformFromClientID(value.id.pluginId);
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
_addCommentView.setContext(value.url, ref);
updatePolycentricRating();
fetchPolycentricProfile();
@@ -633,12 +636,12 @@ class PostDetailFragment : MainFragment {
if (cachedPolycentricProfile?.profile == null) {
_layoutMonetization.visibility = View.GONE;
_creatorThumbnail.setHarborAvailable(false, animate, null);
_creatorThumbnail.setHarborAvailable(false, animate);
return;
}
_layoutMonetization.visibility = View.VISIBLE;
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
_creatorThumbnail.setHarborAvailable(true, animate);
}
private fun fetchPost() {
@@ -662,16 +665,14 @@ class PostDetailFragment : MainFragment {
private fun fetchPolycentricComments() {
Logger.i(TAG, "fetchPolycentricComments")
val post = _post;
val ref = (_post?.url ?: _postOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) }
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.let { if (it.isNotEmpty()) it.toByteArray() else null }
if (ref == null) {
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
val idValue = post?.id?.value
if (idValue == null) {
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
_commentsList.clear();
return
}
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post.url, Models.referenceFromBuffer(idValue.toByteArray())); };
}
private fun updateCommentType(reloadComments: Boolean) {
@@ -295,24 +295,17 @@ class SourceDetailFragment : MainFragment() {
}
}
val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
val clientIfExists = if(config.id != StateDeveloper.DEV_ID)
StatePlugins.instance.getPlugin(config.id);
else null;
groups.add(
BigButtonGroup(c, context.getString(R.string.management),
if(!isEmbedded) BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
uninstallSource();
}.withBackground(R.drawable.background_big_button_red).apply {
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
};
} else BigButton(c, context.getString(R.string.uninstall), "Cannot uninstall embedded plugins", R.drawable.ic_block, {}).apply {
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
};
this.alpha = 0.5f
},
if(clientIfExists?.captchaEncrypted != null)
BigButton(c, context.getString(R.string.delete_captcha), context.getString(R.string.deletes_stored_captcha_answer_for_this_plugin), R.drawable.ic_block) {
@@ -332,6 +325,7 @@ class SourceDetailFragment : MainFragment() {
_sourceButtons.addView(group);
}
val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
val advancedButtons = BigButtonGroup(c, "Advanced",
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
@@ -339,15 +333,9 @@ class SourceDetailFragment : MainFragment() {
this.alpha = 0.5f;
},
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
val embeddedConfig = StatePlugins.instance.getEmbeddedPluginConfigFromID(context, config.id);
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Are you sure you want to downgrade (${config.version}=>${embeddedConfig?.version})?",
"This will revert the plugin back to the originally embedded version.\nVersion change: ${config.version}=>${embeddedConfig?.version}", null,
0, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Reinstall", {
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
reloadSource(config.id);
UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh");
}, UIDialogs.ActionStyle.DANGEROUS));
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
reloadSource(config.id);
UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh");
}.apply {
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
@@ -366,22 +354,11 @@ class SourceDetailFragment : MainFragment() {
if(config.authentication == null)
return;
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);
};
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;
@@ -477,7 +454,6 @@ class SourceDetailFragment : MainFragment() {
}
});
}
private fun checkForUpdatesSource() {
val c = _config ?: return;
val sourceUrl = c.sourceUrl ?: return;
@@ -8,7 +8,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -26,7 +25,6 @@ import com.futo.platformplayer.views.adapters.DisabledSourceView
import com.futo.platformplayer.views.adapters.EnabledSourceAdapter
import com.futo.platformplayer.views.adapters.EnabledSourceViewHolder
import com.futo.platformplayer.views.adapters.ItemMoveCallback
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.sources.SourceUnderConstructionView
import kotlinx.coroutines.runBlocking
import java.util.Collections
@@ -41,12 +39,10 @@ class SourcesFragment : MainFragment() {
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack)
if(topBar is AddTopBarFragment) {
(topBar as AddTopBarFragment).onAdd.clear();
if(topBar is AddTopBarFragment)
(topBar as AddTopBarFragment).onAdd.subscribe {
startActivity(Intent(requireContext(), AddSourceOptionsActivity::class.java));
};
}
_view?.reloadSources();
}
@@ -88,14 +84,6 @@ class SourcesFragment : MainFragment() {
_containerDisabledViews = findViewById(R.id.container_disabled_views);
_containerConstruction = findViewById(R.id.container_construction);
if(StatePlatform.instance.getAvailableClients().isEmpty()) {
findViewById<LinearLayout>(R.id.no_sources).isVisible = true;
findViewById<LinearLayout>(R.id.plugin_disclaimer).isVisible = false;
}
findViewById<BigButton>(R.id.button_add_sources).onClick.subscribe {
fragment.startActivity(Intent(context, AddSourceOptionsActivity::class.java));
};
for(inConstructSource in StatePlugins.instance.getSourcesUnderConstruction(context))
_containerConstruction.addView(SourceUnderConstructionView(context, inConstructSource.key, inConstructSource.value));
@@ -121,6 +109,8 @@ class SourcesFragment : MainFragment() {
adapterSourcesEnabled.notifyItemMoved(fromPosition, toPosition);
onEnabledChanged(enabledSources);
if(toPosition == 0)
onPrimaryChanged(enabledSources.first());
StatePlatform.instance.setPlatformOrder(enabledSources.map { it.name });
};
@@ -141,6 +131,8 @@ class SourcesFragment : MainFragment() {
updateContainerVisibility();
onEnabledChanged(enabledSources);
if(index == 0)
onPrimaryChanged(enabledSources.first());
if(enabledSources.size <= 1)
setCanRemove(false);
@@ -227,6 +219,9 @@ class SourcesFragment : MainFragment() {
_adapterSourcesEnabled.canRemove = canRemove;
}
private fun onPrimaryChanged(client: IPlatformClient) {
StatePlatform.instance.selectPrimaryClient(client.id);
}
private fun onEnabledChanged(clients: List<IPlatformClient>) {
runBlocking {
StatePlatform.instance.selectClients(*clients.map { it.id }.toTypedArray());
@@ -4,15 +4,19 @@ 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
@@ -21,13 +25,14 @@ 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.ImageVariable
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
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
@@ -59,11 +64,6 @@ 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: FrameLayout;
private val _buttonAddCreator: Button;
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
@@ -97,8 +97,6 @@ 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;
@@ -139,7 +137,6 @@ class SubscriptionGroupFragment : MainFragment() {
UIDialogs.Action("Delete", {
_group?.let {
it.urls.remove(channel.url);
save();
reloadCreators(it);
}
}, UIDialogs.ActionStyle.DANGEROUS))
@@ -181,7 +178,6 @@ class SubscriptionGroupFragment : MainFragment() {
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Delete", {
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
_didDelete = true;
fragment.close(true);
}, UIDialogs.ActionStyle.DANGEROUS))
};
@@ -192,12 +188,6 @@ class SubscriptionGroupFragment : MainFragment() {
filterCreators();
}
_topbar.setButtons(
Pair(R.drawable.ic_share) {
UIDialogs.toast(context, "Coming soon");
}
);
setGroup(null);
}
@@ -250,18 +240,6 @@ 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);
@@ -278,7 +256,6 @@ class SubscriptionGroupFragment : MainFragment() {
fun setGroup(group: SubscriptionGroup?) {
_didDelete = false;
_group = group;
_textGroupTitle.text = group?.name;
@@ -295,12 +272,6 @@ 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();
@@ -107,14 +107,12 @@ class SubscriptionGroupListFragment : MainFragment() {
updateGroups();
}
if(topBar is AddTopBarFragment) {
(topBar as AddTopBarFragment).onAdd.clear();
if(topBar is AddTopBarFragment)
(topBar as AddTopBarFragment).onAdd.subscribe {
_overlay?.let {
UISlideOverlays.showCreateSubscriptionGroup(it)
}
};
}
}
private fun updateGroups() {
@@ -10,7 +10,6 @@ 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
@@ -31,7 +30,6 @@ import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.NoResultsView
import com.futo.platformplayer.views.ToastView
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
@@ -45,7 +43,6 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.nio.channels.Channel
import java.time.OffsetDateTime
import kotlin.system.measureTimeMillis
@@ -55,7 +52,6 @@ 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) {
@@ -76,8 +72,6 @@ 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;
}
@@ -86,7 +80,6 @@ class SubscriptionsFeedFragment : MainFragment() {
val view = _view;
if (view != null) {
_cachedRecyclerData = view.recyclerData;
_group = view.subGroup;
view.cleanup();
_view = null;
}
@@ -107,11 +100,18 @@ class SubscriptionsFeedFragment : MainFragment() {
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
var subGroup: SubscriptionGroup? = null;
private 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.global.onUpdateProgress.subscribe(this) { progress, total ->
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
setProgress(progress, total);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set progress", e);
}
}
};
StateSubscriptions.instance.onSubscriptionsChanged.subscribe(this) { _, added ->
@@ -137,9 +137,8 @@ class SubscriptionsFeedFragment : MainFragment() {
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
recyclerData.lastLoad = OffsetDateTime.now();
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen) {
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
loadResults(false);
}
else if(recyclerData.results.size == 0) {
loadCache();
setLoading(false);
@@ -166,24 +165,12 @@ 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();
@@ -207,7 +194,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");
@@ -267,11 +254,6 @@ 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);
@@ -281,17 +263,8 @@ class SubscriptionsFeedFragment : MainFragment() {
if(g is SubscriptionGroup.Add)
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
else {
subGroup = g;
setProgress(0, 0);
if(Settings.instance.subscriptions.fetchOnTabOpen) {
loadCache();
loadResults(false);
}
else if(g != null && StateSubscriptions.instance.getFeed(g.id) != null) {
loadResults(false);
}
else
loadCache();
_subGroup = g;
loadCache(); //TODO: Proper subset update
}
};
_subscriptionBar?.onHoldGroup?.subscribe { g ->
@@ -332,7 +305,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);
@@ -432,7 +405,7 @@ class SubscriptionsFeedFragment : MainFragment() {
context?.let {
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
if (exs.size <= 3) {
if (exs.size <= 8) {
for (ex in exs) {
var toShow = ex;
var channel: String? = null;
@@ -442,17 +415,15 @@ class SubscriptionsFeedFragment : MainFragment() {
}
Logger.e(TAG, "Channel [${channel}] failed", ex);
if (toShow is PluginException)
UIDialogs.appToast(ToastView.Toast(
toShow.message +
(if(channel != null) "\nChannel: " + channel else ""), false, null,
"Plugin ${toShow.config.name} failed")
UIDialogs.toast(
it,
context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
);
else
UIDialogs.appToast(ex.message ?: "");
UIDialogs.toast(it, ex.message ?: "");
}
}
else {
val failedChannels = exs.filterIsInstance<ChannelException>().map { it.channelNameOrUrl }.distinct().toList();
val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
.filter { it != null }
@@ -460,10 +431,7 @@ class SubscriptionsFeedFragment : MainFragment() {
.map { it!! }
.toList();
for(distinctPluginFail in failedPlugins)
UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
if(failedChannels.isNotEmpty())
UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- ${it}" }.joinToString("\n") +
(if(failedChannels.size >= 3) "\nAnd ${failedChannels.size - 3} more" else ""), false, null, "Failed Channels"));
UIDialogs.toast(it, 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)
@@ -67,7 +67,7 @@ class TutorialFragment : MainFragment() {
addView(createHeader("Initial setup"))
initialSetupVideos.forEach {
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).apply {
addView(createTutorialPill(R.drawable.ic_movie, it.name).apply {
onClick.subscribe {
fragment.navigate<VideoDetailFragment>(it)
}
@@ -76,7 +76,7 @@ class TutorialFragment : MainFragment() {
addView(createHeader("Features"))
featuresVideos.forEach {
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).apply {
addView(createTutorialPill(R.drawable.ic_movie, it.name).apply {
onClick.subscribe {
fragment.navigate<VideoDetailFragment>(it)
}
@@ -95,11 +95,10 @@ class TutorialFragment : MainFragment() {
}
}
private fun createTutorialPill(iconPrefix: Int, t: String, d: String): WidePillButton {
private fun createTutorialPill(iconPrefix: Int, t: String): WidePillButton {
return WidePillButton(context).apply {
setIconPrefix(iconPrefix)
setText(t)
setDescription(d)
setIconSuffix(R.drawable.ic_play_notif)
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
setMargins(15.dp(resources), 0, 15.dp(resources), 12.dp(resources))
@@ -108,9 +107,9 @@ class TutorialFragment : MainFragment() {
}
}
class TutorialVideoSourceDescriptor(url: String, duration: Long, width: Int, height: Int) : VideoUnMuxedSourceDescriptor() {
class TutorialVideoSourceDescriptor(url: String, duration: Long) : VideoUnMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> = arrayOf(
VideoUrlSource("Original", url, width, height, duration, "video/mp4")
VideoUrlSource("1080p", url, 1920, 1080, duration, "video/mp4")
)
override val audioSources: Array<IAudioSource> = arrayOf()
}
@@ -121,9 +120,7 @@ class TutorialFragment : MainFragment() {
override val description: String,
thumbnailUrl: String,
videoUrl: String,
override val duration: Long,
width: Int = 1920,
height: Int = 1080
override val duration: Long
) : IPlatformVideoDetails {
override val id: PlatformID = PlatformID("tutorial", uuid)
override val contentType: ContentType = ContentType.MEDIA
@@ -132,15 +129,15 @@ class TutorialFragment : MainFragment() {
override val dash: IDashManifestSource? = null
override val hls: IHLSManifestSource? = null
override val subtitles: List<ISubtitleSource> = emptyList()
override val shareUrl: String = videoUrl
override val url: String = videoUrl
override val shareUrl: String = ""
override val url: String = ""
override val datetime: OffsetDateTime? = OffsetDateTime.parse("2023-12-18T00:00:00Z")
override val thumbnails: Thumbnails = Thumbnails(arrayOf(Thumbnail(thumbnailUrl)))
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("tutorial", "f422ced6-b551-4b62-818e-27a4f5f4918a"), "Grayjay", "", "https://releases.grayjay.app/tutorials/author.jpeg")
override val isLive: Boolean = false
override val rating: IRating = RatingLikes(-1)
override val viewCount: Long = -1
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration)
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
return EmptyPager()
}
@@ -166,7 +163,7 @@ class TutorialFragment : MainFragment() {
TutorialVideo(
uuid = "3b99ebfe-2640-4643-bfe0-a0cf04261fc5",
name = "Getting started",
description = "Learn how to get started with Grayjay. How do you install plugins?",
description = "Learn how to get started with Grayjay.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/getting-started.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/getting-started.mp4",
duration = 50
@@ -174,7 +171,7 @@ class TutorialFragment : MainFragment() {
TutorialVideo(
uuid = "793aa009-516c-4581-b82f-a8efdfef4c27",
name = "Is Grayjay free?",
description = "Learn how Grayjay is monetized. How do we make money?",
description = "Learn how Grayjay is monetized.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/pay.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/pay.mp4",
duration = 52
@@ -185,7 +182,7 @@ class TutorialFragment : MainFragment() {
TutorialVideo(
uuid = "d2238d88-4252-4a91-a12d-b90c049bb7cf",
name = "Searching",
description = "Learn about searching in Grayjay. How can I find channels, videos or playlists?",
description = "Learn about searching in Grayjay.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/search.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/search.mp4",
duration = 39
@@ -201,20 +198,10 @@ class TutorialFragment : MainFragment() {
TutorialVideo(
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
name = "Casting",
description = "Learn about casting in Grayjay. How do I show video on my TV?",
description = "Learn about casting in Grayjay.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-cast.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/how-to-cast.mp4",
duration = 79
),
TutorialVideo(
uuid = "5128c2e3-852b-4281-869b-efea2ec82a0e",
name = "Monetization",
description = "How can I monetize as a creator?",
thumbnailUrl = "https://releases.grayjay.app/tutorials/monetization.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/monetization.mp4",
duration = 47,
1080,
1920
)
)
}
@@ -25,6 +25,8 @@ 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 {
@@ -169,14 +171,14 @@ class VideoDetailFragment : MainFragment {
_view!!.transitionToStart();
}
fun maximizeVideoDetail(instant: Boolean = false) {
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
if(_maximizeProgress > 0.9f && state != State.MAXIMIZED) {
state = State.MAXIMIZED;
onMaximized.emit();
}
_view?.let {
if(!instant) {
if(!instant)
it.transitionToEnd();
} else {
else {
it.progress = 1f;
onTransitioning.emit(true);
}
@@ -370,6 +372,11 @@ 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");
@@ -424,7 +431,6 @@ class VideoDetailFragment : MainFragment {
changeOrientation(OrientationManager.Orientation.PORTRAIT);
}
isFullscreen = fullscreen;
_view?.allowMotion = !fullscreen;
}
private fun changeOrientation(orientation: OrientationManager.Orientation) {
Logger.i(TAG, "Orientation Change:" + orientation.name);
@@ -145,11 +145,10 @@ import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers
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
@@ -373,7 +372,7 @@ class VideoDetailView : ConstraintLayout {
_buttonSubscribe.onSubscribed.subscribe {
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
};
_container_content_liveChat.onRaidNow.subscribe {
@@ -438,20 +437,16 @@ class VideoDetailView : ConstraintLayout {
var buttonMore: RoundButton? = null;
buttonMore = RoundButton(context, R.drawable.ic_menu, context.getString(R.string.more), TAG_MORE) {
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE), false) {selected ->
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE)) {selected ->
_buttonPins.setButtons(*(selected + listOf(buttonMore!!)).toTypedArray());
_buttonPinStore.set(*selected.filter { it.tagRef is String }.map{ it.tagRef as String }.toTypedArray())
_buttonPinStore.save();
};
}
};
_buttonMore = buttonMore;
updateMoreButtons();
_channelButton.setOnClickListener {
if (video is TutorialFragment.TutorialVideo) {
return@setOnClickListener
}
(video?.author ?: _searchVideo?.author)?.let {
fragment.navigate<ChannelFragment>(it);
fragment.lifecycleScope.launch {
@@ -474,7 +469,7 @@ class VideoDetailView : ConstraintLayout {
if(!isScrub) {
if(chapter?.type == ChapterType.SKIPPABLE) {
_layoutSkip.visibility = VISIBLE;
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
} else if(chapter?.type == ChapterType.SKIP) {
val ad = StateCasting.instance.activeDevice
if (ad != null) {
ad.seekVideo(chapter.timeEnd)
@@ -761,9 +756,7 @@ class VideoDetailView : ConstraintLayout {
fun updateMoreButtons() {
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
(video ?: _searchVideo)?.let {
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer) {
_slideUpOverlay = it
};
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer);
}
},
if(video?.isLive ?: false)
@@ -776,7 +769,6 @@ class VideoDetailView : ConstraintLayout {
Logger.e(TAG, "Failed to reopen live chat", ex);
}
}
_slideUpOverlay?.hide();
} else null,
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
if(!allowBackground) {
@@ -789,7 +781,6 @@ class VideoDetailView : ConstraintLayout {
allowBackground = false;
it.text.text = resources.getString(R.string.background);
}
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
video?.let {
@@ -802,13 +793,11 @@ class VideoDetailView : ConstraintLayout {
preventPictureInPicture = true;
shareVideo();
};
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
this.startPictureInPicture();
fragment.forcePictureInPicture();
//PiPActivity.startPiP(context);
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
video?.let {
@@ -816,11 +805,9 @@ class VideoDetailView : ConstraintLayout {
fragment.navigate<BrowserFragment>(url);
fragment.minimizeVideoDetail();
};
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
reloadVideo();
_slideUpOverlay?.hide();
}).filterNotNull();
if(!_buttonPinStore.getAllValues().any())
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
@@ -856,19 +843,14 @@ class VideoDetailView : ConstraintLayout {
}
}
}
private val _historyIndexLock = Mutex(false);
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index? = withContext(Dispatchers.IO){
_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;
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;
}
return@withContext current;
}
@@ -1005,9 +987,6 @@ class VideoDetailView : ConstraintLayout {
fun setVideo(url: String, resumeSeconds: Long = 0, playWhenReady: Boolean = true) {
Logger.i(TAG, "setVideo url=$url resumeSeconds=$resumeSeconds playWhenReady=$playWhenReady")
if(this.video?.url == url)
return;
_searchVideo = null;
video = null;
_playbackTracker = null;
@@ -1038,9 +1017,6 @@ class VideoDetailView : ConstraintLayout {
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0) {
Logger.i(TAG, "setVideoOverview")
if(this.video?.url == video.url)
return;
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
if(cachedVideo != null) {
setVideoDetails(cachedVideo, true);
@@ -1135,13 +1111,10 @@ 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})")
if(newVideo && this.video?.url == videoDetail.url)
return;
if (newVideo) {
_lastVideoSource = null;
_lastAudioSource = null;
@@ -1161,7 +1134,6 @@ 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) {
@@ -1229,12 +1201,12 @@ class VideoDetailView : ConstraintLayout {
};
}
val ref = Models.referenceFromBuffer(video.url.toByteArray())
val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null }
_addCommentView.setContext(video.url, ref)
val ref = video.id.value?.let { Models.referenceFromBuffer(it.toByteArray()) };
_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);
@@ -1292,54 +1264,57 @@ class VideoDetailView : ConstraintLayout {
_rating.onLikeDislikeUpdated.remove(this);
_rating.visibility = View.GONE;
if (ref != null) {
_rating.visibility = View.GONE;
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
arrayListOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.like.data)).build(),
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.dislike.data)).build()
),
extraByteReferences = listOfNotNull(extraBytesRef)
);
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
arrayListOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.like.data)).build(),
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.dislike.data)).build()
)
);
val likes = queryReferencesResponse.countsList[0];
val dislikes = queryReferencesResponse.countsList[1];
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
val likes = queryReferencesResponse.countsList[0];
val dislikes = queryReferencesResponse.countsList[1];
val hasLiked = StatePolycentric.instance.hasLiked(ref);
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
withContext(Dispatchers.Main) {
_rating.visibility = View.VISIBLE;
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
if (args.hasLiked) {
args.processHandle.opinion(ref, Opinion.like);
} else if (args.hasDisliked) {
args.processHandle.opinion(ref, Opinion.dislike);
} else {
args.processHandle.opinion(ref, Opinion.neutral);
}
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
Logger.i(TAG, "Started backfill");
args.processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, "Failed to backfill servers", e)
withContext(Dispatchers.Main) {
_rating.visibility = View.VISIBLE;
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
if (args.hasLiked) {
args.processHandle.opinion(ref, Opinion.like);
} else if (args.hasDisliked) {
args.processHandle.opinion(ref, Opinion.dislike);
} else {
args.processHandle.opinion(ref, Opinion.neutral);
}
}
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
};
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
Logger.i(TAG, "Started backfill");
args.processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, "Failed to backfill servers", e)
}
}
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
};
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
_rating.visibility = View.GONE;
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
_rating.visibility = View.GONE;
}
} else {
_rating.visibility = View.GONE;
}
when (video.rating) {
@@ -1382,36 +1357,32 @@ class VideoDetailView : ConstraintLayout {
val toResume = _videoResumePositionMilliseconds;
_videoResumePositionMilliseconds = 0;
loadCurrentVideo(toResume);
if (!Settings.instance.gestureControls.useSystemVolume) {
_player.setGestureSoundFactor(1.0f);
}
_player.setGestureSoundFactor(1.0f);
updateQueueState();
if (video !is TutorialFragment.TutorialVideo) {
fragment.lifecycleScope.launch(Dispatchers.IO) {
val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
fragment.lifecycleScope.launch(Dispatchers.IO) {
val historyItem = getHistoryIndex(videoDetail);
withContext(Dispatchers.Main) {
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
_layoutResume.visibility = View.VISIBLE;
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
withContext(Dispatchers.Main) {
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
_layoutResume.visibility = View.VISIBLE;
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
delay(8000);
_layoutResume.visibility = View.GONE;
_textResume.text = "";
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set resume changes.", e);
}
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
delay(8000);
_layoutResume.visibility = View.GONE;
_textResume.text = "";
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set resume changes.", e);
}
} else {
_layoutResume.visibility = View.GONE;
_textResume.text = "";
}
} else {
_layoutResume.visibility = View.GONE;
_textResume.text = "";
}
}
}
@@ -1503,12 +1474,12 @@ class VideoDetailView : ConstraintLayout {
private fun loadCurrentVideo(resumePositionMs: Long = 0) {
_didStop = false;
val video = (videoLocal ?: video) ?: return;
val video = 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 ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
val subtitleSource = _lastSubtitleSource;
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
if(videoSource == null && audioSource == null) {
@@ -1536,8 +1507,6 @@ class VideoDetailView : ConstraintLayout {
_player.setArtwork(null);
_player.setSource(videoSource, audioSource, _playWhenReady, false);
if(subtitleSource != null)
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
_player.seekTo(resumePositionMs);
}
else
@@ -1545,7 +1514,6 @@ class VideoDetailView : ConstraintLayout {
_lastVideoSource = videoSource;
_lastAudioSource = audioSource;
_lastSubtitleSource = subtitleSource;
}
catch(ex: UnsupportedCastException) {
Logger.e(TAG, "Failed to load cast media", ex);
@@ -1980,15 +1948,13 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "fetchPolycentricComments")
val video = video;
val idValue = video?.id?.value
if (video?.url?.isEmpty() != false) {
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
if (idValue == null) {
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
_commentsList.clear()
return
}
val ref = Models.referenceFromBuffer(video.url.toByteArray())
val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null }
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); };
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, Models.referenceFromBuffer(idValue.toByteArray())); };
}
private fun fetchVideo() {
Logger.i(TAG, "fetchVideo")
@@ -2250,11 +2216,9 @@ class VideoDetailView : ConstraintLayout {
val v = video ?: return;
val currentTime = System.currentTimeMillis();
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
if (v !is TutorialFragment.TutorialVideo) {
fragment.lifecycleScope.launch(Dispatchers.IO) {
val history = getHistoryIndex(v) ?: return@launch;
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
}
fragment.lifecycleScope.launch(Dispatchers.IO) {
val history = getHistoryIndex(v);
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
}
_lastPositionSaveTime = currentTime;
}
@@ -2337,7 +2301,7 @@ class VideoDetailView : ConstraintLayout {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
val username = cachedPolycentricProfile?.profile?.systemState?.username
@@ -2361,7 +2325,7 @@ class VideoDetailView : ConstraintLayout {
}
else if(isOverlayed) {
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -2f, resources.displayMetrics).toInt();
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -6f, resources.displayMetrics).toInt();
};
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
}
@@ -2562,7 +2526,7 @@ class VideoDetailView : ConstraintLayout {
}
else
withContext(Dispatchers.Main) {
setVideoDetails(videoDetail, true);
setVideoDetails(videoDetail);
_liveTryJob = null;
}
}
@@ -13,17 +13,17 @@ import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageButton
import android.widget.TextView
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.R
import com.futo.platformplayer.stores.SearchHistoryStorage
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.fragment.mainactivity.main.*
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SearchHistoryStorage
class SearchTopBarFragment : TopFragment() {
private val TAG = "SearchTopBarFragment"
@@ -54,12 +54,11 @@ class SearchTopBarFragment : TopFragment() {
private val _searchDoneListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
val isEnterPress = event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN
if (actionId != EditorInfo.IME_ACTION_DONE && !isEnterPress)
if (actionId != EditorInfo.IME_ACTION_DONE)
return false
onDone()
return true
onDone();
return true;
}
};
@@ -12,7 +12,6 @@ import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex
import kotlinx.coroutines.runBlocking
class PlatformLinkMovementMethod : LinkMovementMethod {
private val _context: Context;
@@ -33,36 +32,33 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
val links = buffer.getSpans(off, off, URLSpan::class.java);
if (links.isNotEmpty()) {
runBlocking {
for (link in links) {
Logger.i(TAG) { "Link clicked '${link.url}'." };
for (link in links) {
Logger.i(TAG) { "Link clicked '${link.url}'." };
if (_context is MainActivity) {
if (_context.handleUrl(link.url)) {
continue;
}
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s =
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue;
}
}
if (_context is MainActivity) {
if (_context.handleUrl(link.url)) {
continue;
}
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue;
}
}
}
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
}
return true;
@@ -56,7 +56,7 @@ class PolycentricCache {
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
{ system ->
val signedEventsList = ApiMethods.getQueryLatest(
val signedProfileEvents = ApiMethods.getQueryLatest(
SERVER,
system.toProto(),
listOf(
@@ -72,9 +72,8 @@ class PolycentricCache {
ContentType.MEMBERSHIP_URLS.value,
ContentType.DONATION_DESTINATIONS.value
)
).eventsList.map { e -> SignedEvent.fromProto(e) };
val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType }
).eventsList.map { e -> SignedEvent.fromProto(e) }
.groupBy { e -> e.event.contentType }
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
val storageSystemState = StorageTypeSystemState.create()
@@ -152,7 +151,17 @@ class PolycentricCache {
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
{
val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported");
val urlData = if (it.startsWith("polycentric://")) {
it.substring("polycentric://".length)
} else it;
val urlBytes = urlData.base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
if (urlInfo.urlType != 4L) {
throw Exception("Only URLInfoDataLink is supported");
}
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
},
{ return@BatchedTaskHandler null },
@@ -316,10 +325,9 @@ class PolycentricCache {
.build();
private const val TAG = "PolycentricCache"
const val STAGING_SERVER = "https://srv1-stg.polycentric.io"
const val SERVER = "https://srv1-prod.polycentric.io"
const val SERVER = "https://srv1-stg.polycentric.io"
private var _instance: PolycentricCache? = null;
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
private val CACHE_EXPIRATION_SECONDS = 60 * 60 * 3;
@JvmStatic
val instance: PolycentricCache
@@ -335,20 +343,5 @@ class PolycentricCache {
it._scope.cancel("PolycentricCache finished");
}
}
fun getDataLinkFromUrl(it: String): Protocol.URLInfoDataLink? {
val urlData = if (it.startsWith("polycentric://")) {
it.substring("polycentric://".length)
} else it;
val urlBytes = urlData.base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
if (urlInfo.urlType != 4L) {
return null
}
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
return dataLink
}
}
}
@@ -4,18 +4,13 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AudioNoisyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
Logger.i(TAG, "Audio Noisy received");
MediaControlReceiver.onPauseReceived.emit();
}
Logger.i(TAG, "Audio Noisy received");
MediaControlReceiver.onPauseReceived.emit();
}
companion object {
@@ -143,7 +143,7 @@ class MediaPlaybackService : Service() {
override fun onDestroy() {
Logger.v(TAG, "onDestroy");
_instance = null;
MediaControlReceiver.onPauseReceived.emit();
MediaControlReceiver.onCloseReceived.emit();
super.onDestroy();
}
@@ -153,7 +153,7 @@ class MediaPlaybackService : Service() {
fun closeMediaSession() {
Logger.v(TAG, "closeMediaSession");
stopForeground(STOP_FOREGROUND_REMOVE);
stopForeground(STOP_FOREGROUND_DETACH);
val focusRequest = _focusRequest;
if (focusRequest != null) {
@@ -162,9 +162,7 @@ class MediaPlaybackService : Service() {
}
_hasFocus = false;
val notifManager = _notificationManager;
Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
notifManager?.cancel(MEDIA_NOTIF_ID);
_notificationManager?.cancel(MEDIA_NOTIF_ID);
_notif_last_video = null;
_notif_last_bitmap = null;
_mediaSession = null;
@@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import java.util.Random
import java.util.UUID
class StateAnnouncement {
@@ -251,6 +252,41 @@ class StateAnnouncement {
}
fun registerDidYouKnow() {
val random = Random();
val message: String? = when (random.nextInt(4 * 18 + 1)) {
0 -> "You can login to different platforms and unify your content experience. Check it out in the source settings!"
1 -> "Importing your playlists and subscriptions from other platforms to Grayjay is quick and easy. Check it out in the source settings!"
2 -> "Want to cast to a big screen? Try out FCast (https://fcast.org/)."
3 -> "Explore Grayjay's gesture controls. When in full-screen swipe on the left to change brightness, swipe on the right to change volume."
4 -> "Explore Grayjay's gesture controls. Swipe up in the center of a video to toggle full-screen."
5 -> "Grayjay's multi-platform search lets you find content from various sources."
6 -> "Grayjay's multi-platform search filters will unify filters across platforms. If your expected filters are not there, try toggling some platforms off in the search filters."
7 -> "You can share playlists with friends on the playlist page and make full-backups in the settings page."
8 -> "Discover Grayjay's offline playback feature. Save content for when you're on the go!"
9 -> "Paid content from your favorite creators gets seamlessly integrated into your Grayjay feed. Login to a platform to seamlessly see content you paid for."
10 -> "Explore Grayjay's plugin features! Login, import playlists, and tweak plugin settings for a tailored experience."
11 -> "Directly engage with content by liking, disliking, or leaving comments on the Polycentric network."
12 -> "With Grayjay's rotation lock, you can watch videos in your preferred orientation regardless of device settings. Check it out during playback!"
13 -> "Grayjay supports background play. Listen to your favorite content even while multitasking!"
14 -> "Use Grayjay's quality selection to adjust video resolution. Save data or watch in high definition it's up to you."
15 -> "Customize your Grayjay experience by changing playback speed. Watch content at your own pace."
16 -> "Save time by adding videos to your 'Watch Later' list. Perfect for catching up on content during your free time."
17 -> "On Grayjay, your playlists, subscriptions, and settings are stored offline for privacy and quick access."
18 -> "Explore and engage with live content using Grayjay's live stream feature."
else -> null
};
if (message != null) {
registerAnnouncement(
"did-you-know?",
"Did you know?",
message,
AnnouncementType.SESSION_RECURRING
);
}
}
fun registerDefaultHandlerAnnouncement() {
registerAnnouncement(
"default-url-handler",
@@ -13,7 +13,6 @@ import android.net.NetworkRequest
import android.net.Uri
import android.provider.DocumentsContract
import android.util.DisplayMetrics
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
@@ -39,9 +38,9 @@ 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
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis
@@ -381,15 +380,13 @@ 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();
@@ -426,6 +423,8 @@ class StateApp {
}
}
StateAnnouncement.instance.registerAnnouncement("fa4647d3-36fa-4c8c-832d-85b00fc72dca", "Disclaimer", "This is an early alpha build of the application, expect bugs and unfinished features.", AnnouncementType.DELETABLE, OffsetDateTime.now())
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
StateDeveloper.instance.runServer();
@@ -461,9 +460,7 @@ class StateApp {
//Foreground download
autoUpdateEnabled -> {
scopeOrNull?.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(context, false)
}
StateUpdate.instance.checkForUpdates(context, false);
}
else -> {
@@ -474,11 +471,7 @@ class StateApp {
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
_receiverBecomingNoisy?.let {
_receiverBecomingNoisy = null;
try {
context.unregisterReceiver(it);
} catch (e: Throwable) {
Log.e(TAG, "Failed to unregister receiver.", e)
}
context.unregisterReceiver(it);
}
_receiverBecomingNoisy = AudioNoisyReceiver();
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
@@ -558,31 +551,13 @@ class StateApp {
}
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
StateAnnouncement.instance.registerDidYouKnow();
Logger.i(TAG, "MainApp Started: Finished");
StatePlaylists.instance.toMigrateCheck();
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"
));
}
}
}
}
fun mainAppStartedWithExternalFiles(context: Context) {
@@ -644,11 +619,7 @@ class StateApp {
Logger.i(TAG, "App ended");
_receiverBecomingNoisy?.let {
_receiverBecomingNoisy = null;
try {
context.unregisterReceiver(it);
} catch (e: Throwable) {
Log.e(TAG, "Failed to unregister receiver.", e)
}
context.unregisterReceiver(it);
}
Logger.i(TAG, "Unregistered network callback on connectivityManager.")
@@ -49,17 +49,13 @@ class StateCache {
Logger.i(TAG, "Subscriptions CachePager get subscriptions");
val subs = StateSubscriptions.instance.getSubscriptions();
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
val allUrls = subs
.map {
val allUrls = subs.map {
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
if(!otherUrls.contains(it.channel.url))
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
else
return@map otherUrls;
}
.flatten()
.distinct()
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
}.flatten().distinct();
Logger.i(TAG, "Subscriptions CachePager get pagers");
val pagers: List<IPager<IPlatformContent>>;
@@ -352,10 +352,7 @@ 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 } +
e.subtitlesSources.map { it.filePath }});
val validFiles = HashSet(expected.flatMap { e -> e.videoSource.map { it.filePath } + e.audioSource.map { it.filePath } });
var totalDeleted: Long = 0;
var totalDeletedCount = 0;
@@ -1,6 +1,5 @@
package com.futo.platformplayer.states
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
@@ -93,20 +92,14 @@ class StateHistory {
}
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
val existing = historyIndex[video.url];
var result: DBHistory.Index? = null;
if(existing != null) {
result = _historyDBStore.getOrNull(existing.id!!);
if(result == null)
UIDialogs.toast("History item null?\nNo history tracking..");
}
if(existing != null)
return _historyDBStore.get(existing.id!!);
else if(create) {
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
val id = _historyDBStore.insert(newHistItem);
result = _historyDBStore.getOrNull(id);
if(result == null)
UIDialogs.toast("History creation failed?\nNo history tracking..");
return _historyDBStore.get(id);
}
return result;
return null;
}
fun removeHistory(url: String) {
@@ -5,7 +5,6 @@ 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
@@ -46,7 +45,6 @@ import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
@@ -80,7 +78,6 @@ 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
@@ -95,6 +92,11 @@ class StatePlatform {
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
private val _primaryClientPersistent = FragmentedStorage.get<StringStorage>("primaryClient");
private var _primaryClientObj : IPlatformClient? = null;
val primaryClient : IPlatformClient get() = _primaryClientObj ?: throw IllegalStateException("PlatformState not yet initialized");
private val _icons : HashMap<String, ImageVariable> = HashMap();
val hasClients: Boolean get() = _availableClients.size > 0;
@@ -167,13 +169,8 @@ class StatePlatform {
var enabled: Array<String>;
synchronized(_clientsLock) {
for(e in _enabledClients) {
try {
e.disable();
onSourceDisabled.emit(e);
}
catch(ex: Throwable) {
UIDialogs.appToast(ToastView.Toast("If this happens often, please inform the developers on Github", false, null, "Plugin [${e.name}] failed to disable"));
}
e.disable();
onSourceDisabled.emit(e);
}
_enabledClients.clear();
@@ -208,6 +205,20 @@ class StatePlatform {
.filter { id -> _availableClients.any { it.id == id } }
.toTypedArray();
}
val primary = _primaryClientPersistent.value;
if(primary.isEmpty() || primary == StateDeveloper.DEV_ID) {
selectPrimaryClient(enabled.firstOrNull() ?: _availableClients.first().id);
} else if(!_availableClients.any { it.id == primary }) {
selectPrimaryClient(_availableClients.firstOrNull()?.id!!);
} else {
selectPrimaryClient(primary);
}
if(!enabled.any { it == primaryClient.id }) {
enabled = enabled.concat(primaryClient.id);
}
}
selectClients(*enabled);
};
@@ -310,6 +321,8 @@ class StatePlatform {
newClient.initialize();
_enabledClients.add(newClient);
}
if (_primaryClientObj == client)
_primaryClientObj = newClient;
_availableClients.removeIf { it.id == id };
_availableClients.add(newClient);
@@ -318,11 +331,6 @@ class StatePlatform {
};
}
suspend fun enableClient(ids: List<String>) {
val currentClients = getEnabledClients().map { it.id };
selectClients(*(currentClients + ids).distinct().toTypedArray());
}
/**
* Selects the enabled clients, meaning all clients that data is actively requested from.
* If a client is disabled, NO requests are made to said client
@@ -355,6 +363,17 @@ class StatePlatform {
};
}
/**
* Selects the primary client, meaning the first target for requests.
* At the moment, since multi-client requests are not yet implemented, this is the goto client.
*/
fun selectPrimaryClient(id: String) {
synchronized(_clientsLock) {
_primaryClientObj = getClient(id);
_primaryClientPersistent.setAndSave(id);
}
}
fun getHome(): IPager<IPlatformContent> {
Logger.i(TAG, "Platform - getHome");
var clientIdsOngoing = mutableListOf<String>();
@@ -427,12 +446,14 @@ class StatePlatform {
toAwait.map { PlaceholderPager(5, { PlatformContentPlaceholder(it.first.id) }) });
}
fun getHomePrimary(): IPager<IPlatformContent> {
return primaryClient.getHome();
}
//Search
fun searchSuggestions(query: String): Array<String> {
Logger.i(TAG, "Platform - searchSuggestions");
//TODO: hasSearchSuggestions
return getEnabledClients().firstOrNull()?.searchSuggestions(query) ?: arrayOf();
return primaryClient.searchSuggestions(query);
}
fun search(query: String, type: String? = null, sort: String? = null, filters: Map<String, List<String>> = mapOf(), clientIds: List<String>? = null): IPager<IPlatformContent> {
@@ -818,7 +839,6 @@ class StatePlatform {
return urls;
}
fun hasEnabledPlaylistClient(url: String) : Boolean = getEnabledClients().any { it.isPlaylistUrl(url) };
fun getPlaylistClientOrNull(url: String): IPlatformClient? = getEnabledClients().find { it.isPlaylistUrl(url) }
fun getPlaylistClient(url: String): IPlatformClient = getEnabledClients().find { it.isPlaylistUrl(url) }
?: throw NoPlatformClientException("No client enabled that supports this playlist url (${url})");
@@ -864,6 +884,7 @@ class StatePlatform {
synchronized(_clientsLock) {
val enabledExisting = _enabledClients.filter { it is DevJSClient };
val isEnabled = !enabledExisting.isEmpty()
val isPrimary = _primaryClientObj is DevJSClient;
for (enabled in enabledExisting) {
enabled.disable();
@@ -878,7 +899,11 @@ class StatePlatform {
devId = newClient.devID;
try {
StateDeveloper.instance.initializeDev(devId!!);
if (isEnabled) {
if (isPrimary) {
_primaryClientObj = newClient;
_enabledClients.add(0, newClient);
newClient.initialize();
} else if (isEnabled) {
_enabledClients.add(newClient);
newClient.initialize();
}
@@ -907,67 +932,6 @@ 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().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
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
@@ -361,12 +361,6 @@ class StatePlayer {
if (queueShuffle) {
removeFromShuffledQueue(video);
}
if(currentVideo != null) {
val newPos = _queue.indexOfFirst { it.url == currentVideo?.url };
if(newPos >= 0)
_queuePosition = newPos;
}
}
onQueueChanged.emit(shouldSwapCurrentItem);
@@ -413,12 +407,6 @@ class StatePlayer {
if(_queue.size == 1) {
return null;
}
if(_queue.size <= _queuePosition && currentVideo != null) {
//Out of sync position
val newPos = _queue.indexOfFirst { it.url == currentVideo?.url }
if(newPos != -1)
_queuePosition = newPos;
}
val shuffledQueue = _queueShuffled;
val queue = if (queueShuffle && shuffledQueue != null) {
@@ -433,8 +421,6 @@ class StatePlayer {
}
//Standard Behavior
if(_queuePosition - 1 >= 0) {
if(queue.size <= _queuePosition)
return null;
return queue[_queuePosition - 1];
}
//Repeat Behavior (End of queue)
@@ -16,7 +16,6 @@ import com.futo.platformplayer.exceptions.ReconstructionException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.stores.v2.ReconstructStore
import kotlinx.serialization.encodeToString
@@ -36,8 +35,6 @@ class StatePlaylists {
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
})
.load();
private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order..
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
.withRestore(PlaylistBackup())
.load();
@@ -51,32 +48,26 @@ class StatePlaylists {
}
fun getWatchLater() : List<SerializedPlatformVideo> {
synchronized(_watchlistStore) {
val order = _watchlistOrderStore.getAllValues();
return _watchlistStore.getItems().sortedBy { order.indexOf(it.url) };
return _watchlistStore.getItems();
}
}
fun updateWatchLater(updated: List<SerializedPlatformVideo>) {
synchronized(_watchlistStore) {
_watchlistStore.deleteAll();
_watchlistStore.saveAllAsync(updated);
_watchlistOrderStore.set(*updated.map { it.url }.toTypedArray());
_watchlistOrderStore.save();
}
onWatchLaterChanged.emit();
}
fun removeFromWatchLater(video: SerializedPlatformVideo) {
synchronized(_watchlistStore) {
_watchlistStore.delete(video);
_watchlistOrderStore.set(*_watchlistOrderStore.values.filter { it != video.url }.toTypedArray());
_watchlistOrderStore.save();
}
onWatchLaterChanged.emit();
}
fun addToWatchLater(video: SerializedPlatformVideo) {
synchronized(_watchlistStore) {
_watchlistStore.saveAsync(video);
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
_watchlistOrderStore.save();
}
onWatchLaterChanged.emit();
}
@@ -10,6 +10,7 @@ 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
@@ -160,13 +161,6 @@ class StatePlugins {
val configJson = StateAssets.readAsset(context, assetConfigPath) ?: return null;
return SourcePluginConfig.fromJson(configJson, "");
}
fun getEmbeddedPluginConfigFromID(context: Context, pluginId: String): SourcePluginConfig? {
val embedded = getEmbeddedSources(context);
if(!embedded.containsKey(pluginId))
return null;
return getEmbeddedPluginConfig(context, embedded[pluginId]!!);
}
fun installEmbeddedPlugin(context: Context, assetConfigPath: String, id: String? = null): Boolean {
try {
val configJson = StateAssets.readAsset(context, assetConfigPath) ?:
@@ -473,6 +467,7 @@ class StatePlugins {
_plugins.save(descriptor);
}
@Serializable
private data class PluginConfig(
val SOURCES_EMBEDDED: Map<String, String>,
@@ -27,20 +27,7 @@ import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ClaimType
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.PublicKey
import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.SqlLiteDbHelper
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.base64ToByteArray
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toBase64
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.futo.polycentric.core.*
import com.google.protobuf.ByteString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@@ -48,10 +35,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import userpackage.Protocol
import userpackage.Protocol.Reference
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
import kotlin.Exception
class StatePolycentric {
private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean);
@@ -141,21 +128,21 @@ class StatePolycentric {
_likeDislikeMap[ref.toByteArray().toBase64()] = LikeDislikeEntry(System.currentTimeMillis(), hasLiked, hasDisliked);
}
fun hasDisliked(data: ByteArray): Boolean {
fun hasDisliked(ref: Protocol.Reference): Boolean {
if (!enabled) {
return false
}
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
return entry.hasDisliked;
}
fun hasLiked(data: ByteArray): Boolean {
fun hasLiked(ref: Protocol.Reference): Boolean {
if (!enabled) {
return false
}
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
return entry.hasLiked;
}
@@ -288,8 +275,7 @@ class StatePolycentric {
rating = RatingLikeDislikes(0, 0),
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
replyCount = 0,
eventPointer = se.toPointer(),
parentReference = se.event.references.getOrNull(0)
eventPointer = se.toPointer()
))
}
@@ -330,78 +316,7 @@ class StatePolycentric {
return LikesDislikesReplies(likes, dislikes, replyCount)
}
suspend fun getComment(contextUrl: String, reference: Reference): PolycentricPlatformComment {
ensureEnabled()
if (reference.referenceType != 2L) {
throw Exception("Not a pointer")
}
val pointer = Protocol.Pointer.parseFrom(reference.reference)
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
.setProcess(pointer.process)
.addRanges(Protocol.Range.newBuilder()
.setLow(pointer.logicalClock)
.setHigh(pointer.logicalClock)
.build())
.build())
.build())
val sev = SignedEvent.fromProto(events.getEvents(0))
val ev = sev.event
if (ev.contentType != ContentType.POST.value) {
throw Exception("This is not a comment")
}
val post = Protocol.Post.parseFrom(ev.content);
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
val dp_25 = 25.dp(StateApp.instance.context.resources)
val profileEvents = ApiMethods.getQueryLatest(
PolycentricCache.SERVER,
ev.system.toProto(),
listOf(
ContentType.AVATAR.value,
ContentType.USERNAME.value
)
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
val imageBundle = if (avatarEvent != null) {
val lwwElementValue = avatarEvent.event.lwwElement?.value;
if (lwwElementValue != null) {
Protocol.ImageBundle.parseFrom(lwwElementValue)
} else {
null
}
} else {
null
}
val ldr = getLikesDislikesReplies(reference)
return PolycentricPlatformComment(
contextUrl = contextUrl,
author = PlatformAuthorLink(
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
url = systemLinkUrl,
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
subscribers = null
),
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
rating = RatingLikeDislikes(ldr.likes, ldr.dislikes),
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
replyCount = ldr.replyCount.toInt(),
eventPointer = sev.toPointer(),
parentReference = sev.event.references.getOrNull(0)
)
}
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference): IPager<IPlatformComment> {
if (!enabled) {
return EmptyPager()
}
@@ -423,8 +338,7 @@ class StatePolycentric {
Protocol.QueryReferencesRequestCountReferences.newBuilder()
.setFromType(ContentType.POST.value)
.build())
.build(),
extraByteReferences = extraByteReferences
.build()
);
val results = mapQueryReferences(contextUrl, response);
@@ -493,8 +407,7 @@ class StatePolycentric {
ContentType.AVATAR.value,
ContentType.USERNAME.value
)
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
).eventsList.map { e -> SignedEvent.fromProto(e) };
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
@@ -526,8 +439,7 @@ class StatePolycentric {
rating = RatingLikeDislikes(likes, dislikes),
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
replyCount = replies.toInt(),
eventPointer = sev.toPointer(),
parentReference = sev.event.references.getOrNull(0)
eventPointer = sev.toPointer()
);
} catch (e: Throwable) {
return@mapNotNull null;
@@ -0,0 +1,52 @@
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()
}
}
@@ -9,7 +9,6 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.functional.CentralizedFeed
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
@@ -51,16 +50,10 @@ class StateSubscriptions {
val global: CentralizedFeed = CentralizedFeed();
val feeds: HashMap<String, CentralizedFeed> = hashMapOf();
val onFeedProgress = Event3<String?, Int, Int>();
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
init {
global.onUpdateProgress.subscribe { progress, total ->
onFeedProgress.emit(null, progress, total);
}
}
fun getOldestUpdateTime(): OffsetDateTime {
val subs = getSubscriptions();
if(subs.size == 0)
@@ -77,9 +70,6 @@ class StateSubscriptions {
var f = feeds[id];
if(f == null && createIfNew) {
f = CentralizedFeed();
f.onUpdateProgress.subscribe { progress, total ->
onFeedProgress.emit(id, progress, total)
};
feeds[id] = f;
}
return@synchronized f;
@@ -2,15 +2,15 @@ package com.futo.platformplayer.states
import android.content.Context
import android.os.Build
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.UIDialogs
import android.os.Environment
import com.futo.platformplayer.*
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,45 +155,47 @@ class StateUpdate {
}
}
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
try {
val client = ManagedHttpClient();
val latestVersion = downloadVersionCode(client);
fun checkForUpdates(context: Context, showUpToDateToast: Boolean) {
StateApp.instance.scopeOrNull?.launch(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, hideExceptionButtons);
} catch (e: Throwable) {
UIDialogs.toast(context, "Failed to show update dialog");
Logger.w(TAG, "Error occurred in update dialog.");
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");
}
}
}
} else {
if (showUpToDateToast) {
withContext(Dispatchers.Main) {
UIDialogs.toast(context, "Already on latest version");
}
Logger.w(TAG, "Failed to retrieve version from version URL.");
withContext(Dispatchers.Main) {
UIDialogs.toast(context, "Failed to retrieve version");
}
}
} else {
Logger.w(TAG, "Failed to retrieve version from version URL.");
} catch (e: Throwable) {
Logger.w(TAG, "Failed to check for updates.", e);
withContext(Dispatchers.Main) {
UIDialogs.toast(context, "Failed to retrieve version");
UIDialogs.toast(context, "Failed to check for updates");
}
}
} 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) {
@@ -12,7 +12,6 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.StoreSerializer
import kotlinx.serialization.KSerializer
import java.lang.IllegalArgumentException
import java.lang.reflect.Field
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
@@ -210,9 +209,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
fun getObject(id: Long) = get(id).obj!!;
fun get(id: Long): I {
val result = dbDaoBase.getNullable(_sqlGet(id))
?: throw IllegalArgumentException("DB [${name}] has no entry with id ${id}");
return deserializeIndex(result);
return deserializeIndex(dbDaoBase.get(_sqlGet(id)));
}
fun getOrNull(id: Long): I? {
val result = dbDaoBase.getNullable(_sqlGet(id));
@@ -13,7 +13,6 @@ import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.findNonRuntimeException
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
@@ -56,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.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
}
}
@@ -70,6 +69,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val cachedChannels = mutableListOf<String>()
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
val taskResults = arrayListOf<SubscriptionTaskResult>();
val timeTotal = measureTimeMillis {
for(task in forkTasks) {
@@ -126,7 +126,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val pager = MultiChronoContentPager(groupedPagers, allowFailure, 15);
pager.initialize();
return Result(DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id }), exs);
return Result(DedupContentPager(pager), exs);
}
fun executeSubscriptionTasks(tasks: List<SubscriptionTask>, failedPlugins: MutableList<String>, cachedChannels: MutableList<String>): List<ForkJoinTask<SubscriptionTaskResult>> {
@@ -200,7 +200,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
else {
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
pager = StateCache.instance.getChannelCachePager(task.sub.channel.url);
taskEx = channelEx;
taskEx = ex;
return@submit SubscriptionTaskResult(task, pager, taskEx);
}
}
@@ -1,456 +0,0 @@
package com.futo.platformplayer.views
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
import java.security.MessageDigest
import kotlin.math.max
import kotlin.math.min
class IdenticonView(context: Context, attrs: AttributeSet) : View(context, attrs) {
var hashString: String = "default"
set(value) {
field = value
hash = md5(value)
iconGenerator = null
invalidate()
}
private var hash = ByteArray(16)
private var iconGenerator: IconGenerator? = null
private val path = Path()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val radius = (width.coerceAtMost(height) / 2).toFloat()
val clipPath = path.apply {
reset()
addCircle(width / 2f, height / 2f, radius, Path.Direction.CW)
}
canvas.clipPath(clipPath)
if (iconGenerator == null) {
iconGenerator = IconGenerator(min(height, width).toFloat(), hash)
}
iconGenerator?.render(canvas)
}
private fun md5(input: String): ByteArray {
val md = MessageDigest.getInstance("MD5")
return md.digest(input.toByteArray(Charsets.UTF_8))
}
interface Shape {
fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint)
}
class CutCorner : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val k = size * 0.42f
val path = Path().apply {
moveTo(0f, 0f)
lineTo(size, 0f)
lineTo(size, size - k * 2)
lineTo(size - k, size)
lineTo(0f, size)
close()
}
canvas.drawPath(path, paint)
}
}
class SideTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val w = size / 2
val h = size * 0.8f
val path = Path().apply {
moveTo(size - w, 0f)
lineTo(size, h)
lineTo(size - w, h)
close()
}
canvas.drawPath(path, paint)
}
}
class MiddleSquare : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val s = size / 3
canvas.drawRect(s, s, size - s, size - s, paint)
}
}
class CornerSquare : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val inner = size * 0.1f
val outer = max(1f, size * 0.25f)
canvas.drawRect(outer, outer, size - inner - outer, size - inner - outer, paint)
}
}
class OffCenterCircle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val m = size * 0.15f
val s = size * 0.5f
canvas.drawCircle(size - s - m, size - s - m, s / 2, paint)
}
}
class NegativeTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val inner = size * 0.1f
val outer = inner * 4
val path = Path().apply {
addRect(0f, 0f, size, size, Path.Direction.CW)
moveTo(outer, outer)
lineTo(size - inner, outer)
lineTo(outer + (size - outer - inner) / 2, size - inner)
close()
}
canvas.drawPath(path, paint)
}
}
class CutSquare : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
moveTo(0f, 0f)
lineTo(size, 0f)
lineTo(size, size * 0.7f)
lineTo(size * 0.4f, size * 0.4f)
lineTo(size * 0.7f, size)
lineTo(0f, size)
close()
}
canvas.drawPath(path, paint)
}
}
class CornerPlusTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val halfSize = size / 2
canvas.drawRect(0f, 0f, size, halfSize, paint)
canvas.drawRect(0f, halfSize, halfSize, size, paint)
val path = Path().apply {
moveTo(halfSize, halfSize)
lineTo(size, halfSize)
lineTo(halfSize, size)
close()
}
canvas.drawPath(path, paint)
}
}
class NegativeSquare : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val inner = size * 0.14f
val outer = size * 0.35f
val path = Path().apply {
addRect(0f, 0f, size, size, Path.Direction.CW)
addRect(outer, outer, size - outer - inner, size - outer - inner, Path.Direction.CCW)
}
canvas.drawPath(path, paint)
}
}
class NegativeCircle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val inner = size * 0.12f
val outer = inner * 3
val path = Path().apply {
addRect(0f, 0f, size, size, Path.Direction.CW)
addCircle(outer, outer, (size - inner - outer) / 2, Path.Direction.CCW)
}
canvas.drawPath(path, paint)
}
}
class NegativeRhombus : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val m = size * 0.25f
val path = Path().apply {
addRect(0f, 0f, size, size, Path.Direction.CW)
moveTo(m, size / 2)
lineTo(size / 2, m)
lineTo(size - m, size / 2)
lineTo(size / 2, size - m)
close()
}
canvas.drawPath(path, paint)
}
}
class ConditionalCircle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
if (index == 0) {
val m = size * 0.4f
val s = size * 1.2f
canvas.drawCircle(m, m, s / 2, paint)
}
}
}
class HalfTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
moveTo(size / 2, size / 2)
lineTo(size, size / 2)
lineTo(size / 2, size)
close()
}
canvas.drawPath(path, paint)
}
}
class Triangle(val corner: Int = 0) : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
when (corner) {
0 -> {
moveTo(0f, 0f)
lineTo(size, 0f)
lineTo(0f, size)
}
1 -> {
moveTo(size, 0f)
lineTo(size, size)
lineTo(0f, size)
}
2 -> {
moveTo(0f, 0f)
lineTo(size, 0f)
lineTo(size, size)
}
3 -> {
moveTo(0f, 0f)
lineTo(0f, size)
lineTo(size, size)
}
}
close()
}
canvas.drawPath(path, paint)
}
}
class BottomHalfTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
moveTo(0f, size / 2)
lineTo(size, size / 2)
lineTo(size / 2, size)
close()
}
canvas.drawPath(path, paint)
}
}
class Rhombus : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
moveTo(size / 2, 0f)
lineTo(size, size / 2)
lineTo(size / 2, size)
lineTo(0f, size / 2)
close()
}
canvas.drawPath(path, paint)
}
}
class Circle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val m = size / 6
canvas.drawCircle(m, m, size / 2 - m, paint)
}
}
class IconGenerator(private val size: Float, private val hash: ByteArray) {
private val digits: ByteArray
private var selectedColors = arrayOf<Paint>()
init {
digits = ByteArray(max(12, hash.size * 2))
var index = 0
for (byte in hash) {
if (index >= digits.size) {
break
}
digits[index] = ((byte.toInt() shr 4) and 0x0f).toByte()
digits[index + 1] = (byte.toInt() and 0x0f).toByte()
index += 2
}
selectColors()
}
private fun selectColors() {
val value = hash.copyOfRange(hash.size - 4, hash.size).fold(0) { acc, byte ->
(acc shl 8) or (byte.toInt() and 0xFF)
} and 0x0FFFFFFF
val colorTheme = ColorTheme(hue = value.toFloat() / 0x0FFFFFFF)
val selectedColorIndices = mutableListOf<Int>()
for (i in 0 until 3) {
val index = (digits[8 + i].toInt() % colorTheme.colors.size)
selectedColorIndices.add(colorTheme.validateIndex(index, selectedColorIndices))
}
selectedColors = selectedColorIndices.map { index ->
Paint().apply {
color = colorTheme.colors[index]
style = Paint.Style.FILL
}
}.toTypedArray()
}
fun renderBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(size.toInt(), size.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
render(canvas)
return bitmap
}
fun render(canvas: Canvas) {
canvas.drawColor(Color.WHITE)
renderShape(canvas, 0, outerShapes, 2, 3, arrayOf(
PointF(1f, 0f),
PointF(2f, 0f),
PointF(2f, 3f),
PointF(1f, 3f),
PointF(0f, 1f),
PointF(3f, 1f),
PointF(3f, 2f),
PointF(0f, 2f),
))
renderShape(canvas, 1, outerShapes, 4, 5, arrayOf(
PointF(0f, 0f),
PointF(3f, 0f),
PointF(3f, 3f),
PointF(0f, 3f),
))
renderShape(canvas, 2, centerShapes, 1, null, arrayOf(
PointF(1f, 1f),
PointF(2f, 1f),
PointF(2f, 2f),
PointF(1f, 2f),
))
}
private fun renderShape(
canvas: Canvas,
colorIndex: Int,
shapes: Array<Shape>,
index: Int,
rotationIndex: Int?,
positions: Array<PointF>
) {
val cellSize = size / 4
var r = rotationIndex?.let { digits[it].toInt() } ?: 0
val shape = shapes[digits[index].toInt() % shapes.size]
val paint = Paint().apply {
color = selectedColors[colorIndex % selectedColors.size].color
style = Paint.Style.FILL
}
for ((idx, position) in positions.withIndex()) {
canvas.save()
canvas.translate(position.x * cellSize, position.y * cellSize)
canvas.translate(cellSize / 2, cellSize / 2)
canvas.rotate((r % 4) * 90f)
canvas.translate(-cellSize / 2, -cellSize / 2)
shape.draw(canvas, cellSize, idx, paint)
canvas.restore()
r++
}
}
class ColorTheme(val hue: Float, val saturation: Float = 0.5f) {
val colors: List<Int>
init {
colors = listOf(
// Dark gray
grayscaleColor(0f),
// Mid color
hslColor(hue, saturation, colorLightness(0.5f)),
// Light gray
grayscaleColor(1f),
// Light color
hslColor(hue, saturation, colorLightness(1f)),
// Dark color
hslColor(hue, saturation, colorLightness(0f))
)
}
fun validateIndex(index: Int, selected: List<Int>): Int {
return if (isDuplicate(index, listOf(0, 4), selected) || isDuplicate(index, listOf(2, 3), selected)) {
1
} else {
index
}
}
private fun isDuplicate(index: Int, values: List<Int>, selected: List<Int>): Boolean {
if (!values.contains(index)) return false
return values.any { selected.contains(it) }
}
private fun colorLightness(value: Float): Float = lightness(value, 0.4f, 0.8f)
private fun grayscaleLightness(value: Float): Float = lightness(value, 0.3f, 0.9f)
private fun lightness(value: Float, min: Float, max: Float): Float {
val lightness = min + value * (max - min)
return minOf(1f, maxOf(0f, lightness))
}
private fun grayscaleColor(lightness: Float): Int {
return Color.HSVToColor(floatArrayOf(0f, 0f, lightness))
}
private fun hslColor(hue: Float, saturation: Float, lightness: Float): Int {
return Color.HSVToColor(floatArrayOf(hue, saturation, lightness))
}
}
}
companion object {
val centerShapes = arrayOf(
CutCorner(),
SideTriangle(),
MiddleSquare(),
CornerSquare(),
OffCenterCircle(),
NegativeTriangle(),
CutSquare(),
HalfTriangle(),
CornerPlusTriangle(),
CutSquare(),
NegativeCircle(),
HalfTriangle(),
NegativeRhombus(),
ConditionalCircle()
)
val outerShapes = arrayOf(
Triangle(),
BottomHalfTriangle(),
Rhombus(),
Circle(),
)
private const val TAG = "IdenticonView"
}
}
@@ -1,91 +0,0 @@
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
);
}
@@ -9,21 +9,15 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.pills.PillButton
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
@@ -110,8 +104,7 @@ class CommentViewHolder : ViewHolder {
fun bind(comment: IPlatformComment, readonly: Boolean) {
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
_textAuthor.text = comment.author.name;
val date = comment.date;
@@ -168,8 +161,8 @@ class CommentViewHolder : ViewHolder {
_pillRatingLikesDislikes.visibility = View.VISIBLE;
if (comment is PolycentricPlatformComment) {
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference.toByteArray());
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference.toByteArray());
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference);
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference);
_pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked);
} else {
_pillRatingLikesDislikes.setRating(comment.rating);
@@ -55,7 +55,6 @@ class CommentWithReferenceViewHolder : ViewHolder {
var onRepliesClick = Event1<IPlatformComment>();
var onDelete = Event1<IPlatformComment>();
var onClick = Event1<IPlatformComment>();
var comment: IPlatformComment? = null
private set;
@@ -109,11 +108,6 @@ class CommentWithReferenceViewHolder : ViewHolder {
onDelete.emit(c);
}
_layoutComment.setOnClickListener {
val c = comment ?: return@setOnClickListener;
onClick.emit(c);
}
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
}
@@ -132,8 +126,7 @@ class CommentWithReferenceViewHolder : ViewHolder {
_taskGetLiveComment.cancel()
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
_textAuthor.text = comment.author.name;
val date = comment.date;
@@ -175,8 +168,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
if (likesDislikesReplies != null) {
Log.i(TAG, "updateLikesDislikesReplies set")
val hasLiked = StatePolycentric.instance.hasLiked(c.reference.toByteArray());
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference.toByteArray());
val hasLiked = StatePolycentric.instance.hasLiked(c.reference);
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference);
_pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
_buttonReplies.setLoading(false)
@@ -0,0 +1,42 @@
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,15 +1,17 @@
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;
@@ -36,16 +38,7 @@ class DisabledSourceView : LinearLayout {
client.icon?.setImageView(_imageSource);
_textSource.text = client.name;
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)
}
_textSourceSubtitle.text = context.getString(R.string.tap_to_open);
_buttonAdd.setOnClickListener { onAdd.emit(source) }
_root.setOnClickListener { onClick.emit(); };
@@ -0,0 +1,44 @@
package com.futo.platformplayer.views.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.constructs.Event0
class DisabledSourceViewHolder : ViewHolder {
private val _imageSource: ImageView;
private val _textSource: TextView;
private val _textSourceSubtitle: TextView;
private val _buttonAdd: LinearLayout;
var onClick = Event0();
var onAdd = Event0();
var source: IPlatformClient? = null
private set
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_source_disabled, viewGroup, false)) {
_imageSource = itemView.findViewById(R.id.image_source);
_textSource = itemView.findViewById(R.id.text_source);
_textSourceSubtitle = itemView.findViewById(R.id.text_source_subtitle);
_buttonAdd = itemView.findViewById(R.id.button_add);
val root = itemView.findViewById<LinearLayout>(R.id.root);
_buttonAdd.setOnClickListener { onAdd.emit() }
root.setOnClickListener { onClick.emit(); };
}
fun bind(client: IPlatformClient) {
client.icon?.setImageView(_imageSource);
_textSource.text = client.name;
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
source = client;
}
}
@@ -10,9 +10,7 @@ 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;
@@ -59,18 +57,8 @@ class EnabledSourceViewHolder : ViewHolder {
fun bind(client: IPlatformClient) {
client.icon?.setImageView(_imageSource);
_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)
}
_textSource.text = client.name;
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
source = client
}
}
@@ -7,7 +7,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@@ -18,8 +18,8 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.platform.PlatformIndicator
@@ -149,8 +149,7 @@ open class PlaylistView : LinearLayout {
_neopassAnimator?.cancel();
_neopassAnimator = null;
val firstClaim = claims?.ownedClaims?.firstOrNull();
val harborAvailable = firstClaim != null
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
if (harborAvailable) {
_imageNeopassChannel?.visibility = View.VISIBLE
if (animate) {
@@ -161,7 +160,7 @@ open class PlaylistView : LinearLayout {
_imageNeopassChannel?.visibility = View.GONE
}
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto())
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
}
companion object {
@@ -6,18 +6,21 @@ import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.constructs.Event0
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.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.toHumanBytesSpeed
import com.futo.platformplayer.toHumanTimeIndicator
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.platform.PlatformIndicator
@@ -104,7 +107,7 @@ class SubscriptionViewHolder : ViewHolder {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
@@ -15,7 +15,6 @@ 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
@@ -47,7 +46,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)
}).exception<Throwable> { Logger.e(TAG, "Failed to retrieve preview content.", it) }.success { previewContentDetails(it.first, it.second) }
}).success { previewContentDetails(it.first, it.second) }
constructor(context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
initialPlay: Boolean = false, viewsToPrepend: ArrayList<View> = arrayListOf(),
@@ -334,7 +334,7 @@ open class PreviewVideoView : LinearLayout {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
} else if (_imageChannel != null) {
val dp_28 = 28.dp(context.resources);
@@ -1,5 +1,6 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
@@ -7,10 +8,12 @@ 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.api.media.models.channels.SerializedChannel
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.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
@@ -73,7 +76,7 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
@@ -145,7 +148,7 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
@@ -98,7 +98,7 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
@@ -77,7 +77,7 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
@@ -3,31 +3,45 @@ package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
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);
@@ -44,9 +58,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)
@@ -54,11 +68,10 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
}
_textSubGroup.text = value.name;
if (value is SubscriptionGroup.Selectable && value.selected) {
_root.setBackgroundResource(R.drawable.background_primary_round_6dp)
} else {
_root.background = null
}
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));
}
companion object {
@@ -4,15 +4,11 @@ import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Matrix
import android.graphics.drawable.Animatable
import android.media.AudioManager
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
@@ -22,24 +18,12 @@ import android.widget.TextView
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
import androidx.core.view.GestureDetectorCompat
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.others.CircularProgressBar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
class GestureControlView : LinearLayout {
private val _scope = CoroutineScope(Dispatchers.Main);
@@ -67,8 +51,6 @@ class GestureControlView : LinearLayout {
private val _progressSound: CircularProgressBar;
private var _animatorSound: ObjectAnimator? = null;
private var _brightnessFactor = 1.0f;
private var _originalBrightnessFactor = 1.0f;
private var _originalBrightnessMode: Int = 0;
private var _adjustingBrightness: Boolean = false;
private val _layoutControlsBrightness: FrameLayout;
private val _progressBrightness: CircularProgressBar;
@@ -80,27 +62,10 @@ class GestureControlView : LinearLayout {
private var _fullScreenFactorUp = 1.0f;
private var _fullScreenFactorDown = 1.0f;
private var _scaleGestureDetector: ScaleGestureDetector
private var _scaleFactor = 1.0f
private var _translationX = 0.0f
private var _translationY = 0.0f
private val _layoutControlsZoom: FrameLayout
private val _textZoom: TextView
private var _isZooming = false
private var _isPanning = false
private var _isZoomPanEnabled = false
private var _surfaceView: View? = null
private var _layoutIndicatorFill: FrameLayout;
private var _layoutIndicatorFit: FrameLayout;
private val _gestureController: GestureDetectorCompat;
val isUserGesturing get() = _rewinding || _skipping || _adjustingBrightness || _adjustingSound || _adjustingFullscreenUp || _adjustingFullscreenDown || _isPanning || _isZooming;
val onSeek = Event1<Long>();
val onBrightnessAdjusted = Event1<Float>();
val onPan = Event2<Float, Float>();
val onZoom = Event1<Float>();
val onSoundAdjusted = Event1<Float>();
val onToggleFullscreen = Event0();
@@ -118,48 +83,8 @@ class GestureControlView : LinearLayout {
_layoutControlsSound = findViewById(R.id.layout_controls_sound);
_progressSound = findViewById(R.id.progress_sound);
_layoutControlsBrightness = findViewById(R.id.layout_controls_brightness);
_layoutControlsZoom = findViewById(R.id.layout_controls_zoom)
_textZoom = findViewById(R.id.text_zoom)
_progressBrightness = findViewById(R.id.progress_brightness);
_layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen);
_layoutIndicatorFill = findViewById(R.id.layout_indicator_fill);
_layoutIndicatorFit = findViewById(R.id.layout_indicator_fit);
_scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
if (!_isZoomPanEnabled || !_isFullScreen || !Settings.instance.gestureControls.zoom) {
return false
}
val newScaleFactor = (_scaleFactor * detector.scaleFactor).coerceAtLeast(1.0f).coerceAtMost(10.0f)
val scaleFactorChange = newScaleFactor / _scaleFactor
_scaleFactor = newScaleFactor
onZoom.emit(_scaleFactor)
val sx = detector.focusX
val sy = detector.focusY
val tx = _translationX + width / 2.0f
val ty = _translationY + height / 2.0f
val matrix = Matrix()
matrix.postTranslate(-sx, -sy)
matrix.postScale(scaleFactorChange, scaleFactorChange)
matrix.postTranslate(sx, sy)
val point = floatArrayOf(tx, ty)
matrix.mapPoints(point)
pan(point[0] - width / 2.0f, point[1] - height / 2.0f)
_layoutControlsZoom.visibility = View.VISIBLE
_textZoom.text = "${String.format("%.1f", _scaleFactor)}x"
_isZooming = true
updateSnappingVisibility()
return true
}
})
_gestureController = GestureDetectorCompat(context, object : GestureDetector.OnGestureListener {
override fun onDown(p0: MotionEvent): Boolean { return false; }
@@ -170,48 +95,40 @@ class GestureControlView : LinearLayout {
if(p0 == null)
return false;
if (!_isPanning && p1.pointerCount == 1) {
val minDistance = Math.min(width, height)
if (_isFullScreen && _adjustingBrightness) {
val adjustAmount = (distanceY * 2) / minDistance;
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_progressBrightness.progress = _brightnessFactor;
onBrightnessAdjusted.emit(_brightnessFactor);
} else if (_isFullScreen && _adjustingSound) {
val adjustAmount = (distanceY * 2) / minDistance;
_soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_progressSound.progress = _soundFactor;
onSoundAdjusted.emit(_soundFactor);
} else if (_adjustingFullscreenUp) {
val adjustAmount = (distanceY * 2) / minDistance;
_fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_layoutControlsFullscreen.alpha = _fullScreenFactorUp;
} else if (_adjustingFullscreenDown) {
val adjustAmount = (-distanceY * 2) / minDistance;
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
} else if (p0.pointerCount == 1) {
val rx = (p0.x + p1.x) / (2 * width);
val ry = (p0.y + p1.y) / (2 * height);
if (ry > 0.1 && ry < 0.9) {
if (Settings.instance.gestureControls.brightnessSlider && _isFullScreen && rx < 0.2) {
startAdjustingBrightness();
} else if (Settings.instance.gestureControls.volumeSlider && _isFullScreen && rx > 0.8) {
startAdjustingSound();
} else if (Settings.instance.gestureControls.toggleFullscreen && fullScreenGestureEnabled && rx in 0.3..0.7) {
if (_isFullScreen) {
startAdjustingFullscreenDown();
} else {
startAdjustingFullscreenUp();
}
if (_isFullScreen && _adjustingBrightness) {
val adjustAmount = (distanceY * 2) / height;
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_progressBrightness.progress = _brightnessFactor;
onBrightnessAdjusted.emit(_brightnessFactor);
} else if (_isFullScreen && _adjustingSound) {
val adjustAmount = (distanceY * 2) / height;
_soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_progressSound.progress = _soundFactor;
onSoundAdjusted.emit(_soundFactor);
} else if (_adjustingFullscreenUp) {
val adjustAmount = (distanceY * 2) / height;
_fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_layoutControlsFullscreen.alpha = _fullScreenFactorUp;
} else if (_adjustingFullscreenDown) {
val adjustAmount = (-distanceY * 2) / height;
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
} else {
val rx = (p0.x + p1.x) / (2 * width);
val ry = (p0.y + p1.y) / (2 * height);
if (ry > 0.1 && ry < 0.9) {
if (_isFullScreen && rx < 0.2) {
startAdjustingBrightness();
} else if (_isFullScreen && rx > 0.8) {
startAdjustingSound();
} else if (fullScreenGestureEnabled && rx in 0.3..0.7) {
if (_isFullScreen) {
startAdjustingFullscreenDown();
} else {
startAdjustingFullscreenUp();
}
}
}
} else if (_isZoomPanEnabled && _isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) {
_isPanning = true
stopAllGestures()
updateSnappingVisibility()
pan(_translationX - distanceX, _translationY - distanceY)
}
return true;
@@ -252,55 +169,6 @@ class GestureControlView : LinearLayout {
isClickable = true
}
fun updateSnappingVisibility() {
if (willSnapFill()) {
_layoutIndicatorFill.visibility = View.VISIBLE
_layoutIndicatorFit.visibility = View.GONE
} else if (willSnapFit()) {
_layoutIndicatorFill.visibility = View.GONE
_layoutIndicatorFit.visibility = View.VISIBLE
_surfaceView?.let {
val lp = _layoutIndicatorFit.layoutParams
lp.width = it.width
lp.height = it.height
_layoutIndicatorFit.layoutParams = lp
}
} else {
_layoutIndicatorFill.visibility = View.GONE
_layoutIndicatorFit.visibility = View.GONE
}
}
fun setZoomPanEnabled(view: View) {
_isZoomPanEnabled = true
_surfaceView = view
}
fun resetZoomPan() {
_scaleFactor = 1.0f
onZoom.emit(_scaleFactor)
_translationX = 0f
_translationY = 0f
onPan.emit(_translationX, _translationY)
}
private fun pan(translationX: Float, translationY: Float) {
val xc = width / 2.0f
val yc = height / 2.0f
val xmin = xc - width / 2.0f * _scaleFactor
val xmax = xc + width / 2.0f * _scaleFactor - width
val ymin = yc - height / 2.0f * _scaleFactor
val ymax = yc + height / 2.0f * _scaleFactor - height
_translationX = translationX.coerceAtLeast(xmin).coerceAtMost(xmax)
_translationY = translationY.coerceAtLeast(ymin).coerceAtMost(ymax)
onPan.emit(_translationX, _translationY)
}
fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) {
_layoutControls = layoutControls;
_background = background;
@@ -346,67 +214,12 @@ class GestureControlView : LinearLayout {
stopAdjustingFullscreenDown();
}
if ((_isPanning || _isZooming) && ev.action == MotionEvent.ACTION_UP) {
val surfaceView = _surfaceView
if (surfaceView != null && willSnapFill()) {
_scaleFactor = calculateZoomScaleFactor()
onZoom.emit(_scaleFactor)
_translationX = 0f
_translationY = 0f
onPan.emit(_translationX, _translationY)
} else if (willSnapFit()) {
_scaleFactor = 1f
onZoom.emit(_scaleFactor)
_translationX = 0f
_translationY = 0f
onPan.emit(_translationX, _translationY)
}
_layoutControlsZoom.visibility = View.GONE
_layoutIndicatorFill.visibility = View.GONE
_layoutIndicatorFit.visibility = View.GONE
_isZooming = false
_isPanning = false
}
startHideJobIfNecessary();
_gestureController.onTouchEvent(ev)
_scaleGestureDetector.onTouchEvent(ev)
return true;
}
private fun calculateZoomScaleFactor(): Float {
val w = _surfaceView?.width?.toFloat() ?: return 1.0f;
val h = _surfaceView?.height?.toFloat() ?: return 1.0f;
if (w == 0.0f || h == 0.0f) {
return 1.0f;
}
return Math.max(width / w, height / h)
}
private val _snapTranslationTolerance = 0.1f;
private val _snapZoomTolerance = 0.1f;
private fun willSnapFill(): Boolean {
val surfaceView = _surfaceView
if (surfaceView != null) {
val zoomScaleFactor = calculateZoomScaleFactor()
if (Math.abs(_scaleFactor - zoomScaleFactor) < _snapZoomTolerance && Math.abs(_translationX) / width < _snapTranslationTolerance && Math.abs(_translationY) / height < _snapTranslationTolerance) {
return true
}
}
return false
}
private fun willSnapFit(): Boolean {
return Math.abs(_scaleFactor - 1.0f) < _snapZoomTolerance && Math.abs(_translationX) / width < _snapTranslationTolerance && Math.abs(_translationY) / height < _snapTranslationTolerance
}
fun cancelHideJob() {
_jobHideControls?.cancel();
_jobHideControls = null;
@@ -736,56 +549,11 @@ class GestureControlView : LinearLayout {
}
fun setFullscreen(isFullScreen: Boolean) {
resetZoomPan()
if (isFullScreen) {
if (Settings.instance.gestureControls.useSystemBrightness) {
try {
_originalBrightnessMode = android.provider.Settings.System.getInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE)
val brightness = android.provider.Settings.System.getInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS)
_brightnessFactor = brightness / 255.0f;
Log.i(TAG, "Starting brightness brightness: $brightness, _brightnessFactor: $_brightnessFactor, _originalBrightnessMode: $_originalBrightnessMode")
_originalBrightnessFactor = _brightnessFactor
} catch (e: Throwable) {
Settings.instance.gestureControls.useSystemBrightness = false
Settings.instance.save()
UIDialogs.toast(context, "useSystemBrightness disabled due to an error")
}
}
if (!Settings.instance.gestureControls.useSystemBrightness) {
_brightnessFactor = 1.0f;
}
if (Settings.instance.gestureControls.useSystemVolume) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
_soundFactor = currentVolume.toFloat() / maxVolume.toFloat()
}
onBrightnessAdjusted.emit(_brightnessFactor);
onSoundAdjusted.emit(_soundFactor);
} else {
if (Settings.instance.gestureControls.useSystemBrightness) {
if (Settings.instance.gestureControls.restoreSystemBrightness) {
onBrightnessAdjusted.emit(_originalBrightnessFactor)
if (android.provider.Settings.System.canWrite(context)) {
Log.i(TAG, "Restoring system brightness mode _originalBrightnessMode: $_originalBrightnessMode")
android.provider.Settings.System.putInt(
context.contentResolver,
android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE,
_originalBrightnessMode
)
}
}
} else {
onBrightnessAdjusted.emit(1.0f);
}
onBrightnessAdjusted.emit(1.0f);
//onSoundAdjusted.emit(1.0f);
stopAdjustingBrightness();
stopAdjustingSound();
@@ -13,7 +13,6 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.PlatformLinkMovementMethod
import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex
import kotlinx.coroutines.runBlocking
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
constructor(context: Context) : super(context) {}
@@ -41,34 +40,32 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
if (text is Spannable) {
val links = text.getSpans(offset, offset, URLSpan::class.java)
if (links.isNotEmpty()) {
runBlocking {
for (link in links) {
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
for (link in links) {
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
val c = context;
if (c is MainActivity) {
if (c.handleUrl(link.url)) {
val c = context;
if (c is MainActivity) {
if (c.handleUrl(link.url)) {
continue;
}
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue;
}
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue;
}
}
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
}
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
}
}
@@ -24,8 +24,8 @@ import com.futo.platformplayer.casting.AirPlayCastingDevice
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.formatDuration
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.behavior.GestureControlView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -252,8 +252,8 @@ class CastView : ConstraintLayout {
.load(video.thumbnails.getHQThumbnail())
.placeholder(R.drawable.placeholder_video_thumbnail)
.into(_thumbnail);
_textPosition.text = (position * 1000).formatDuration();
_textDuration.text = (video.duration * 1000).formatDuration();
_textPosition.text = position.toHumanTime(false);
_textDuration.text = video.duration.toHumanTime(false);
_timeBar.setPosition(position);
_timeBar.setDuration(video.duration);
}
@@ -261,7 +261,7 @@ class CastView : ConstraintLayout {
@OptIn(UnstableApi::class)
fun setTime(ms: Long) {
updateCurrentChapter(ms);
_textPosition.text = ms.formatDuration();
_textPosition.text = ms.toHumanTime(true);
_timeBar.setPosition(ms / 1000);
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms);
}
@@ -71,10 +71,6 @@ class FieldForm : LinearLayout {
}
}
fun setSearchQuery(query: String) {
_editSearch.setText(query);
updateSettingsVisibility();
}
fun setSearchVisible(visible: Boolean) {
_containerSearch.visibility = if(visible) View.VISIBLE else View.GONE;
_editSearch.setText("");
@@ -12,16 +12,12 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.views.IdenticonView
import userpackage.Protocol
class CreatorThumbnail : ConstraintLayout {
private val _root: ConstraintLayout;
private val _imageChannelThumbnail: ImageView;
private val _imageNewActivity: ImageView;
private val _imageNeoPass: ImageView;
private val _identicon: IdenticonView;
private var _harborAnimator: ObjectAnimator? = null;
private var _imageAnimator: ObjectAnimator? = null;
@@ -32,23 +28,19 @@ class CreatorThumbnail : ConstraintLayout {
_root = findViewById(R.id.root);
_imageChannelThumbnail = findViewById(R.id.image_channel_thumbnail);
_identicon = findViewById(R.id.identicon);
_imageChannelThumbnail.clipToOutline = true;
_identicon.clipToOutline = true;
_imageChannelThumbnail.visibility = View.GONE
_imageNewActivity = findViewById(R.id.image_new_activity);
_imageNeoPass = findViewById(R.id.image_neopass);
if (!isInEditMode) {
setHarborAvailable(false, animate = false, system = null);
setHarborAvailable(false, animate = false);
setNewActivity(false);
}
}
fun clear() {
_imageChannelThumbnail.visibility = View.GONE;
_imageChannelThumbnail.setImageResource(R.drawable.placeholder_channel_thumbnail);
setHarborAvailable(false, animate = false, system = null);
setHarborAvailable(false, animate = false);
setNewActivity(false);
}
@@ -58,24 +50,13 @@ class CreatorThumbnail : ConstraintLayout {
return;
}
_imageChannelThumbnail.visibility = View.VISIBLE;
_harborAnimator?.cancel();
_harborAnimator = null;
_imageAnimator?.cancel();
_imageAnimator = null;
if (url.startsWith("polycentric://")) {
try {
val dataLink = PolycentricCache.getDataLinkFromUrl(url)
setHarborAvailable(true, animate, dataLink?.system);
} catch (e: Throwable) {
setHarborAvailable(false, animate, null);
}
} else {
setHarborAvailable(false, animate, null);
}
setHarborAvailable(url.startsWith("polycentric://"), animate);
if (animate) {
Glide.with(_imageChannelThumbnail)
@@ -91,7 +72,7 @@ class CreatorThumbnail : ConstraintLayout {
}
}
fun setHarborAvailable(available: Boolean, animate: Boolean, system: Protocol.PublicKey?) {
fun setHarborAvailable(available: Boolean, animate: Boolean) {
_harborAnimator?.cancel();
_harborAnimator = null;
@@ -104,13 +85,6 @@ class CreatorThumbnail : ConstraintLayout {
} else {
_imageNeoPass.visibility = View.GONE;
}
if (system != null) {
_identicon.hashString = system.toString()
_identicon.visibility = View.VISIBLE
} else {
_identicon.visibility = View.GONE
}
}
fun setChannelImageResource(resource: Int?, animate: Boolean) {
@@ -1,32 +1,55 @@
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.widget.FrameLayout
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 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: FrameLayout;
private val _buttonSelect: Button;
private val _topbar: OverlayTopbar;
private val _searchBar: SearchView;
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
private val _creators: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
private val _creatorsFiltered: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
private var _selected: MutableList<String> = mutableListOf();
@@ -43,7 +66,7 @@ class CreatorSelectOverlay: ConstraintLayout {
else
_creators.addAll(subs
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
filterCreators();
_recyclerCreators.notifyContentChanged();
}
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { }
init {
@@ -51,8 +74,7 @@ class CreatorSelectOverlay: ConstraintLayout {
_topbar = findViewById(R.id.topbar);
_buttonSelect = findViewById(R.id.button_select);
val dp6 = 6.dp(resources);
_searchBar = findViewById(R.id.search_bar);
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creatorsFiltered, RecyclerView.HORIZONTAL) { creatorView ->
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creators, RecyclerView.HORIZONTAL) { creatorView ->
creatorView.itemView.setPadding(0, dp6, 0, dp6);
creatorView.onClick.subscribe {
if(it.channel.thumbnail == null) {
@@ -70,33 +92,19 @@ class CreatorSelectOverlay: ConstraintLayout {
this.orientation = LinearLayoutManager.VERTICAL;
};
_buttonSelect.setOnClickListener {
if (_selected.isNotEmpty()) {
_selected?.let {
select();
}
};
_topbar.onClose.subscribe {
onClose.emit();
}
_searchBar.onSearchChanged.subscribe {
filterCreators();
};
updateSelected();
filterCreators();
}
fun updateSelected() {
val changed = arrayListOf<SelectableCreatorBarViewHolder.Selectable>()
for(creator in _creators) {
val act = _selected.contains(creator.channel.url);
if(creator.active != act) {
creator.active = act;
changed.add(creator);
}
}
for(change in changed) {
val index = _creatorsFiltered.indexOf(change);
_recyclerCreators.notifyContentChanged(index);
}
_creators.forEach { p -> p.active = _selected.contains(p.channel.url) };
_recyclerCreators.notifyContentChanged();
if(_selected.isNotEmpty())
_buttonSelect.alpha = 1f;
@@ -105,17 +113,6 @@ class CreatorSelectOverlay: ConstraintLayout {
}
private fun filterCreators(withUpdate: Boolean = true) {
val query = _searchBar.textSearch.text.toString().lowercase();
val filteredEnabled = _creators.filter { query.isEmpty() || it.channel.name.lowercase().contains(query) };
//Optimize
_creatorsFiltered.clear();
_creatorsFiltered.addAll(filteredEnabled);
if(withUpdate)
_recyclerCreators.notifyContentChanged();
}
fun select() {
if(_creators.isEmpty())
return;

Some files were not shown because too many files have changed in this diff Show More