mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 635749dfe4 | |||
| c4bd5626f3 | |||
| 568a0f6329 | |||
| 7ee67b5cd0 | |||
| fc94c6903c | |||
| a0af8805e7 | |||
| 9b64cde17d | |||
| f6931bcf8c | |||
| a4ff47d863 | |||
| 982d251126 | |||
| 8820a0ecc0 | |||
| b99a713ffc | |||
| dfc8c4b740 | |||
| c3df9e5259 | |||
| b9c7e0a8ca | |||
| 2c7f02a24d | |||
| 5cc8488d94 | |||
| 6f7304f59c | |||
| ea4fea4401 | |||
| 9b48664de4 | |||
| 8964dc68f0 | |||
| 4711b8055b | |||
| 84e3373fa7 | |||
| fdd7e32dd8 | |||
| e57119ebbd | |||
| ed29dd8365 | |||
| 196cacb452 | |||
| c025913fc8 | |||
| 48b2c68e72 | |||
| 689766a6ac | |||
| 9306024d17 | |||
| 195163840b | |||
| 788c54bf8f | |||
| 031aabd523 | |||
| 85db4cc4e6 | |||
| 745aad385b | |||
| ba87261f9f | |||
| 7d091382c0 | |||
| 781d0797e7 | |||
| ec12a06b88 | |||
| bf3e8867c3 | |||
| 176814a715 | |||
| 898637a616 | |||
| f1860126a7 | |||
| f8402676d7 | |||
| cf86ce1ab3 | |||
| f4cb1719e0 | |||
| 4898cb53ae | |||
| 0f60d4737e | |||
| 0dc33e1f2b | |||
| d86a997a88 | |||
| 34d4d92289 | |||
| 4cb1bf268f | |||
| 8488706ff9 | |||
| a348bb2662 | |||
| 60a17b3c67 | |||
| 386c58d4ad | |||
| 356ba01dc1 | |||
| ed2aa848da | |||
| c5dd90048f | |||
| ab04f334dc | |||
| 0d44f8a416 | |||
| d01a1545e2 | |||
| e599729ba1 | |||
| 3ac043740e | |||
| 89603d0ff3 | |||
| 05b6cd7c97 | |||
| ea5aad0631 | |||
| 96e034b9bf | |||
| 6141c36855 | |||
| 4084ab3ed0 | |||
| 34e733823a | |||
| f1d01642cd | |||
| d5551d7118 | |||
| d079a1e8e4 | |||
| c06c00ee9b | |||
| 1d8eababc2 | |||
| 75cf1ffbdd | |||
| 5499706a9b | |||
| ba57e32920 | |||
| df96c5b51c | |||
| 75f81d20db | |||
| 3fc92e4065 | |||
| 8ffd5f411f | |||
| 918161a299 | |||
| 9f50f72eaa | |||
| 2f66f124aa | |||
| 9a11717cf4 | |||
| 0d80424799 | |||
| ed9a65b2f0 | |||
| 8a53297be2 | |||
| 20862a27c8 | |||
| 95785e6c78 | |||
| e88c649578 | |||
| 09f91e64fb | |||
| b8923e59a1 | |||
| e722c0ce9a | |||
| 56248bf4b0 | |||
| 5af4787c45 | |||
| 0990247322 | |||
| 0154525578 | |||
| 1dc6eee242 | |||
| c63a63cb33 | |||
| c1967556ac | |||
| 309a57f5a1 |
+1
-1
@@ -169,7 +169,7 @@ dependencies {
|
||||
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||
|
||||
//JS
|
||||
implementation("com.caoccao.javet:javet-android:2.2.1")
|
||||
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||
|
||||
//Exoplayer
|
||||
implementation 'androidx.media3:media3-exoplayer:1.2.0'
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.FutoVideo"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="31"
|
||||
android:largeHeap="true">
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="@string/authority"
|
||||
|
||||
+120
-46
@@ -1,13 +1,37 @@
|
||||
|
||||
declare class ScriptException extends Error {
|
||||
//If only one parameter is provided, acts as msg
|
||||
constructor(type: string, msg: string);
|
||||
}
|
||||
declare class TimeoutException extends ScriptException {
|
||||
|
||||
declare class LoginRequiredException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
//Alias
|
||||
declare class ScriptLoginRequiredException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class CaptchaRequiredException extends ScriptException {
|
||||
constructor(url: string, body: string);
|
||||
}
|
||||
|
||||
declare class CriticalException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class UnavailableException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class AgeException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class TimeoutException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class ScriptImplementationException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
@@ -38,16 +62,23 @@ declare class FilterCapability {
|
||||
|
||||
|
||||
declare class PlatformAuthorLink {
|
||||
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?);
|
||||
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
|
||||
}
|
||||
|
||||
declare class PlatformAuthorMembershipLink {
|
||||
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
|
||||
}
|
||||
|
||||
declare interface PlatformContentDef {
|
||||
id: PlatformID,
|
||||
name: string,
|
||||
thumbnails: Thumbnails,
|
||||
author: PlatformAuthorLink,
|
||||
datetime: integer,
|
||||
url: string
|
||||
}
|
||||
declare interface PlatformContent {}
|
||||
|
||||
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
|
||||
contentUrl: string,
|
||||
contentName: string?,
|
||||
@@ -59,16 +90,26 @@ declare class PlatformNestedMediaContent {
|
||||
constructor(obj: PlatformNestedMediaContentDef);
|
||||
}
|
||||
|
||||
declare interface PlatformLockedContentDef extends PlatformContentDef {
|
||||
contentName: string?,
|
||||
contentThumbnails: Thumbnails?,
|
||||
unlockUrl: string,
|
||||
lockDescription: string?,
|
||||
}
|
||||
declare class PlatformLockedContent {
|
||||
constructor(obj: PlatformLockedContentDef);
|
||||
}
|
||||
|
||||
|
||||
declare interface PlatformVideoDef extends PlatformContentDef {
|
||||
thumbnails: Thumbnails,
|
||||
author: PlatformAuthorLink,
|
||||
|
||||
duration: int,
|
||||
viewCount: long,
|
||||
isLive: boolean
|
||||
isLive: boolean,
|
||||
shareUrl: string?
|
||||
}
|
||||
declare interface PlatformContent {}
|
||||
|
||||
declare class PlatformVideo implements PlatformContent {
|
||||
constructor(obj: PlatformVideoDef);
|
||||
}
|
||||
@@ -77,14 +118,15 @@ declare class PlatformVideo implements PlatformContent {
|
||||
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
|
||||
description: string,
|
||||
video: VideoSourceDescriptor,
|
||||
live: SubtitleSource[],
|
||||
rating: IRating
|
||||
live: IVideoSource,
|
||||
rating: IRating,
|
||||
subtitles: SubtitleSource[]
|
||||
}
|
||||
declare class PlatformVideoDetails extends PlatformVideo {
|
||||
constructor(obj: PlatformVideoDetailsDef);
|
||||
}
|
||||
|
||||
declare class PlatformPostDef extends PlatformContentDef {
|
||||
declare interface PlatformPostDef extends PlatformContentDef {
|
||||
thumbnails: string[],
|
||||
images: string[],
|
||||
description: string
|
||||
@@ -93,7 +135,7 @@ declare class PlatformPost extends PlatformContent {
|
||||
constructor(obj: PlatformPostDef)
|
||||
}
|
||||
|
||||
declare class PlatformPostDetailsDef extends PlatformPostDef {
|
||||
declare interface PlatformPostDetailsDef extends PlatformPostDef {
|
||||
rating: IRating,
|
||||
textType: int,
|
||||
content: String
|
||||
@@ -110,8 +152,8 @@ declare interface MuxVideoSourceDescriptorDef {
|
||||
isUnMuxed: boolean,
|
||||
videoSources: VideoSource[]
|
||||
}
|
||||
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor {
|
||||
constructor(obj: VideoSourceDescriptorDef);
|
||||
declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
|
||||
constructor(videoSourcesOrObj: VideoSource[]);
|
||||
}
|
||||
|
||||
declare interface UnMuxVideoSourceDescriptorDef {
|
||||
@@ -129,7 +171,7 @@ declare interface IVideoSource {
|
||||
declare interface IAudioSource {
|
||||
|
||||
}
|
||||
interface VideoUrlSourceDef implements IVideoSource {
|
||||
declare interface VideoUrlSourceDef implements IVideoSource {
|
||||
width: integer,
|
||||
height: integer,
|
||||
container: string,
|
||||
@@ -139,22 +181,22 @@ interface VideoUrlSourceDef implements IVideoSource {
|
||||
duration: integer,
|
||||
url: string
|
||||
}
|
||||
class VideoUrlSource {
|
||||
declare class VideoUrlSource {
|
||||
constructor(obj: VideoUrlSourceDef);
|
||||
|
||||
getRequestModifier(): RequestModifier?;
|
||||
}
|
||||
interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
||||
declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
||||
itagId: integer,
|
||||
initStart: integer,
|
||||
initEnd: integer,
|
||||
indexStart: integer,
|
||||
indexEnd: integer,
|
||||
}
|
||||
class VideoUrlRangeSource extends VideoUrlSource {
|
||||
declare class VideoUrlRangeSource extends VideoUrlSource {
|
||||
constructor(obj: YTVideoSourceDef);
|
||||
}
|
||||
interface AudioUrlSourceDef {
|
||||
declare interface AudioUrlSourceDef {
|
||||
name: string,
|
||||
bitrate: integer,
|
||||
container: string,
|
||||
@@ -163,24 +205,12 @@ interface AudioUrlSourceDef {
|
||||
url: string,
|
||||
language: string
|
||||
}
|
||||
class AudioUrlSource implements IAudioSource {
|
||||
declare class AudioUrlSource implements IAudioSource {
|
||||
constructor(obj: AudioUrlSourceDef);
|
||||
|
||||
getRequestModifier(): RequestModifier?;
|
||||
}
|
||||
interface IRequest {
|
||||
url: string,
|
||||
headers: Map<string, string>
|
||||
}
|
||||
interface IRequestModifierDef {
|
||||
allowByteSkip: boolean
|
||||
}
|
||||
class RequestModifier {
|
||||
constructor(obj: IRequestModifierDef) { }
|
||||
|
||||
modifyRequest(url: string, headers: Map<string, string>): IRequest;
|
||||
}
|
||||
interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||
declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||
itagId: integer,
|
||||
initStart: integer,
|
||||
initEnd: integer,
|
||||
@@ -188,28 +218,44 @@ interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||
indexEnd: integer,
|
||||
audioChannels: integer
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
declare class AudioUrlRangeSource extends AudioUrlSource {
|
||||
constructor(obj: AudioUrlRangeSourceDef);
|
||||
}
|
||||
interface HLSSourceDef {
|
||||
declare interface HLSSourceDef {
|
||||
name: string,
|
||||
duration: integer,
|
||||
url: string
|
||||
url: string,
|
||||
priority: boolean?,
|
||||
language: string?
|
||||
}
|
||||
class HLSSource implements IVideoSource {
|
||||
declare class HLSSource implements IVideoSource {
|
||||
constructor(obj: HLSSourceDef);
|
||||
}
|
||||
interface DashSourceDef {
|
||||
declare interface DashSourceDef {
|
||||
name: string,
|
||||
duration: integer,
|
||||
url: string
|
||||
url: string,
|
||||
language: string?
|
||||
}
|
||||
class DashSource implements IVideoSource {
|
||||
declare class DashSource implements IVideoSource {
|
||||
constructor(obj: DashSourceDef)
|
||||
}
|
||||
|
||||
declare interface IRequest {
|
||||
url: string,
|
||||
headers: Map<string, string>
|
||||
}
|
||||
declare interface IRequestModifierDef {
|
||||
allowByteSkip: boolean
|
||||
}
|
||||
declare class RequestModifier {
|
||||
constructor(obj: IRequestModifierDef) { }
|
||||
|
||||
modifyRequest(url: string, headers: Map<string, string>): IRequest;
|
||||
}
|
||||
|
||||
//Channel
|
||||
interface PlatformChannelDef {
|
||||
declare interface PlatformChannelDef {
|
||||
id: PlatformID,
|
||||
name: string,
|
||||
thumbnail: string,
|
||||
@@ -217,12 +263,29 @@ interface PlatformChannelDef {
|
||||
subscribers: integer,
|
||||
description: string,
|
||||
url: string,
|
||||
urlAlternatives: string[],
|
||||
links: Map<string>?
|
||||
}
|
||||
class PlatformChannel {
|
||||
declare class PlatformChannel {
|
||||
constructor(obj: PlatformChannelDef);
|
||||
}
|
||||
|
||||
//Playlist
|
||||
declare interface PlatformPlaylistDef implements PlatformContent {
|
||||
videoCount: integer,
|
||||
thumbnail: string
|
||||
}
|
||||
declare class PlatformPlaylist extends PlatformContent {
|
||||
constructor(obj: PlatformPlaylistDef);
|
||||
}
|
||||
declare interface PlatformPlaylistDetailsDef implements PlatformPlaylistDef {
|
||||
contents: ContentPager
|
||||
}
|
||||
declare class PlatformPlaylistDetails extends PlatformContent {
|
||||
constructor(obj: PlatformPlaylistDetailsDef);
|
||||
}
|
||||
|
||||
|
||||
//Ratings
|
||||
interface IRating {
|
||||
type: integer
|
||||
@@ -250,7 +313,11 @@ declare class PlatformComment {
|
||||
constructor(obj: CommentDef);
|
||||
}
|
||||
|
||||
declare class PlaybackTracker {
|
||||
constructor(interval: integer);
|
||||
|
||||
setProgress(seconds: integer);
|
||||
}
|
||||
|
||||
declare class LiveEventPager {
|
||||
nextRequest = 4000;
|
||||
@@ -261,8 +328,8 @@ declare class LiveEventPager {
|
||||
nextPage(): LiveEventPager; //Could be self
|
||||
}
|
||||
|
||||
class LiveEvent {
|
||||
type: String
|
||||
declare class LiveEvent {
|
||||
constructor(type: integer);
|
||||
}
|
||||
declare class LiveEventComment extends LiveEvent {
|
||||
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
|
||||
@@ -287,25 +354,31 @@ declare class ContentPager {
|
||||
constructor(results: PlatformContent[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean
|
||||
nextPage(): VideoPager; //Could be self
|
||||
nextPage(): ContentPager?; //Could be self
|
||||
}
|
||||
declare class VideoPager {
|
||||
constructor(results: PlatformVideo[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean
|
||||
nextPage(): VideoPager; //Could be self
|
||||
nextPage(): VideoPager?; //Could be self
|
||||
}
|
||||
declare class ChannelPager {
|
||||
constructor(results: PlatformChannel[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean;
|
||||
nextPage(): ChannelPager; //Could be self
|
||||
nextPage(): ChannelPager?; //Could be self
|
||||
}
|
||||
declare class PlaylistPager {
|
||||
constructor(results: PlatformPlaylist[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean;
|
||||
nextPage(): PlaylistPager?;
|
||||
}
|
||||
declare class CommentPager {
|
||||
constructor(results: PlatformComment[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean
|
||||
nextPage(): CommentPager; //Could be self
|
||||
nextPage(): CommentPager?; //Could be self
|
||||
}
|
||||
|
||||
interface Map<T> {
|
||||
@@ -341,8 +414,9 @@ interface Source {
|
||||
getChannelCapabilities(): ResultCapabilities;
|
||||
|
||||
isContentDetailsUrl(url: string): boolean;
|
||||
getContentDetails(url: string): PlatformVideoDetails;
|
||||
getContentDetails(url: string): PlatformContentDetails;
|
||||
|
||||
//Optional
|
||||
getLiveEvents(url: string): LiveEventPager;
|
||||
|
||||
//Optional
|
||||
|
||||
@@ -78,6 +78,11 @@ class ScriptLoginRequiredException extends ScriptException {
|
||||
super("ScriptLoginRequiredException", msg);
|
||||
}
|
||||
}
|
||||
class LoginRequiredException extends ScriptException {
|
||||
constructor(msg) {
|
||||
super("ScriptLoginRequiredException", msg);
|
||||
}
|
||||
}
|
||||
class CaptchaRequiredException extends Error {
|
||||
constructor(url, body) {
|
||||
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
||||
@@ -249,8 +254,8 @@ class PlatformVideoDetails extends PlatformVideo {
|
||||
|
||||
this.description = obj.description ?? "";//String
|
||||
this.video = obj.video ?? {}; //VideoSourceDescriptor
|
||||
this.dash = obj.dash ?? null; //DashSource
|
||||
this.hls = obj.hls ?? null; //HLSSource
|
||||
this.dash = obj.dash ?? null; //DashSource, deprecated
|
||||
this.hls = obj.hls ?? null; //HLSSource, deprecated
|
||||
this.live = obj.live ?? null; //VideoSource
|
||||
|
||||
this.rating = obj.rating ?? null; //IRating
|
||||
@@ -321,6 +326,8 @@ class VideoUrlSource {
|
||||
this.bitrate = obj.bitrate ?? 0;
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class VideoUrlRangeSource extends VideoUrlSource {
|
||||
@@ -346,6 +353,8 @@ class AudioUrlSource {
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
this.language = obj.language ?? Language.UNKNOWN;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
@@ -371,6 +380,8 @@ class HLSSource {
|
||||
this.priority = obj.priority ?? false;
|
||||
if(obj.language)
|
||||
this.language = obj.language;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class DashSource {
|
||||
@@ -382,13 +393,15 @@ class DashSource {
|
||||
this.url = obj.url;
|
||||
if(obj.language)
|
||||
this.language = obj.language;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
|
||||
class RequestModifier {
|
||||
constructor(obj) {
|
||||
obj = obj ?? {};
|
||||
this.allowByteSkip = obj.allowByteSkip;
|
||||
this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.util.Log
|
||||
import com.google.common.base.CharMatcher
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
@@ -9,7 +10,6 @@ import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
private const val IPV4_PART_COUNT = 4;
|
||||
@@ -216,15 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
||||
}
|
||||
|
||||
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
val timeout = 2000
|
||||
|
||||
if (addresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (addresses.size == 1) {
|
||||
val socket = Socket()
|
||||
|
||||
try {
|
||||
return Socket(addresses[0], port);
|
||||
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
||||
} catch (e: Throwable) {
|
||||
//Ignored.
|
||||
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||
socket.close()
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -249,7 +254,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
}
|
||||
}
|
||||
|
||||
socket.connect(InetSocketAddress(address, port));
|
||||
socket.connect(InetSocketAddress(address, port), timeout);
|
||||
|
||||
synchronized(syncObject) {
|
||||
if (connectedSocket == null) {
|
||||
@@ -263,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
//Ignore
|
||||
Log.i("getConnectedSocket", "Failed to connect to: $address", e)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.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
|
||||
@@ -47,6 +49,15 @@ 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
|
||||
|
||||
@@ -685,7 +685,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||
fun manualCheck() {
|
||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
StateUpdate.instance.checkForUpdates(it, true);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
StateUpdate.instance.checkForUpdates(it, true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
@@ -807,7 +809,36 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var polycentricEnabled: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
||||
@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 = true;
|
||||
|
||||
@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)
|
||||
var info = Info();
|
||||
@Serializable
|
||||
class Info {
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -398,13 +399,28 @@ class UIDialogs {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
StateApp.withContext {
|
||||
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
|
||||
toast(it, text, long);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to show toast.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
fun appToast(text: String, long: Boolean = false) {
|
||||
appToast(ToastView.Toast(text, long))
|
||||
}
|
||||
fun appToastError(text: String, long: Boolean) {
|
||||
StateApp.withContext {
|
||||
appToast(ToastView.Toast(text, long, it.getColor(R.color.pastel_red)));
|
||||
};
|
||||
}
|
||||
fun appToast(toast: ToastView.Toast) {
|
||||
StateApp.withContext {
|
||||
if(it is MainActivity) {
|
||||
it.showAppToast(toast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
|
||||
//TODO: Is not actually clickable...
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
@@ -68,7 +67,7 @@ class UISlideOverlays {
|
||||
return menu;
|
||||
}
|
||||
|
||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
|
||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||
val items = arrayListOf<View>();
|
||||
|
||||
val originalNotif = subscription.doNotifications;
|
||||
@@ -77,15 +76,13 @@ 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;
|
||||
@@ -119,7 +116,7 @@ class UISlideOverlays {
|
||||
}, false)*/
|
||||
).filterNotNull());
|
||||
|
||||
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
||||
menu.setItems(items);
|
||||
|
||||
if(subscription.doNotifications)
|
||||
menu.selectOption(null, "notifications", true, true);
|
||||
@@ -174,6 +171,8 @@ class UISlideOverlays {
|
||||
menu.show();
|
||||
}
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
|
||||
@@ -343,7 +342,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) {
|
||||
@@ -718,6 +717,13 @@ 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", {
|
||||
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), "",
|
||||
{
|
||||
@@ -739,7 +745,7 @@ class UISlideOverlays {
|
||||
}
|
||||
|
||||
|
||||
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
|
||||
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), invokeParents: Boolean = true, onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
|
||||
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
||||
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
||||
|
||||
@@ -747,7 +753,7 @@ class UISlideOverlays {
|
||||
hidden
|
||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
||||
btn.handler?.invoke(btn);
|
||||
}, true) as View }.toTypedArray(),
|
||||
}, invokeParents) 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
|
||||
|
||||
@@ -216,8 +216,10 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
|
||||
fun install(config: SourcePluginConfig, script: String) {
|
||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||
if(it)
|
||||
if(it) {
|
||||
StatePlatform.instance.clearUpdateAvailable(config)
|
||||
backToSources();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
lateinit var _buttonBack: ImageButton;
|
||||
|
||||
lateinit var _buttonQR: BigButton;
|
||||
lateinit var _buttonBrowse: BigButton;
|
||||
lateinit var _buttonURL: BigButton;
|
||||
lateinit var _buttonPlugins: BigButton;
|
||||
|
||||
@@ -56,6 +57,7 @@ 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);
|
||||
|
||||
@@ -74,6 +76,9 @@ 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));
|
||||
|
||||
@@ -9,9 +9,11 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.logging.LogLevel
|
||||
import com.futo.platformplayer.logging.Logging
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
@@ -39,6 +40,7 @@ class LoginActivity : AppCompatActivity() {
|
||||
_textUrl = findViewById(R.id.text_url);
|
||||
_buttonClose = findViewById(R.id.button_close);
|
||||
_buttonClose.setOnClickListener {
|
||||
UIDialogs.toast("Login cancelled", false);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
@@ -24,6 +23,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -45,6 +45,7 @@ import com.futo.platformplayer.states.*
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.*
|
||||
@@ -54,6 +55,7 @@ import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
@@ -65,6 +67,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
lateinit var rootView : MotionLayout;
|
||||
|
||||
private lateinit var _overlayContainer: FrameLayout;
|
||||
private lateinit var _toastView: ToastView;
|
||||
|
||||
//Segment Containers
|
||||
private lateinit var _fragContainerTopBar: FragmentContainerView;
|
||||
@@ -207,7 +210,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
|
||||
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
|
||||
_overlayContainer = findViewById(R.id.overlay_container);
|
||||
//_overlayContainer.visibility = View.GONE;
|
||||
_toastView = findViewById(R.id.toast_view);
|
||||
|
||||
//Initialize fragments
|
||||
|
||||
@@ -478,21 +481,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
|
||||
_isVisible = true;
|
||||
val videoToOpen = StateSaved.instance.videoToOpen;
|
||||
|
||||
if (_wasStopped) {
|
||||
_wasStopped = false;
|
||||
|
||||
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
||||
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
|
||||
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
|
||||
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
|
||||
_fragVideoDetail.maximizeVideoDetail(true);
|
||||
}
|
||||
|
||||
StateSaved.instance.setVideoToOpenNonBlocking(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -547,6 +535,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
navigate(_fragMainSources);
|
||||
}
|
||||
};
|
||||
"BROWSE_PLUGINS" -> {
|
||||
navigate(_fragBrowser, "https://plugins.grayjay.app");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -661,6 +652,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
};
|
||||
return true;
|
||||
}
|
||||
else if(StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||
navigate(_fragMainPlaylist, url);
|
||||
lifecycleScope.launch {
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
fun handleContent(file: String, mime: String? = null): Boolean {
|
||||
@@ -814,11 +813,9 @@ 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();
|
||||
}
|
||||
@@ -864,7 +861,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_orientationManager.disable();
|
||||
|
||||
StateApp.instance.mainAppDestroyed(this);
|
||||
StateSaved.instance.setVideoToOpenBlocking(null);
|
||||
}
|
||||
|
||||
inline fun <reified T> isFragmentActive(): Boolean {
|
||||
@@ -1052,6 +1048,43 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
private val _toastQueue = ConcurrentLinkedQueue<ToastView.Toast>();
|
||||
private var _toastJob: Job? = null;
|
||||
fun showAppToast(toast: ToastView.Toast) {
|
||||
synchronized(_toastQueue) {
|
||||
_toastQueue.add(toast);
|
||||
if(_toastJob?.isActive != true)
|
||||
_toastJob = lifecycleScope.launch(Dispatchers.Default) {
|
||||
launchAppToastJob();
|
||||
};
|
||||
}
|
||||
}
|
||||
private suspend fun launchAppToastJob() {
|
||||
Logger.i(TAG, "Starting appToast loop");
|
||||
while(!_toastQueue.isEmpty()) {
|
||||
val toast = _toastQueue.poll() ?: continue;
|
||||
Logger.i(TAG, "Showing next toast (${toast.msg})");
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (!_toastView.isVisible) {
|
||||
Logger.i(TAG, "First showing toast");
|
||||
_toastView.setToast(toast);
|
||||
_toastView.show(true);
|
||||
} else {
|
||||
_toastView.setToastAnimated(toast);
|
||||
}
|
||||
}
|
||||
if(toast.long)
|
||||
delay(5000);
|
||||
else
|
||||
delay(3000);
|
||||
}
|
||||
Logger.i(TAG, "Ending appToast loop");
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
_toastView.hide(true) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
|
||||
|
||||
+1
-2
@@ -17,9 +17,7 @@ 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
|
||||
|
||||
@@ -71,6 +69,7 @@ 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);
|
||||
|
||||
+52
-29
@@ -8,12 +8,15 @@ 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
|
||||
@@ -21,6 +24,9 @@ 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
|
||||
|
||||
@@ -29,6 +35,7 @@ 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)
|
||||
@@ -52,6 +59,7 @@ 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();
|
||||
@@ -94,42 +102,57 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
return;
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
_loaderOverlay.show()
|
||||
|
||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||
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 existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
||||
if (existingProcessSecret != null) {
|
||||
UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported));
|
||||
return;
|
||||
}
|
||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||
|
||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||
Store.instance.addProcessSecret(processSecret);
|
||||
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 processHandle = processSecret.toProcessHandle();
|
||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||
Store.instance.addProcessSecret(processSecret);
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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}'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+48
-20
@@ -1,6 +1,8 @@
|
||||
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
|
||||
@@ -12,6 +14,7 @@ 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
|
||||
@@ -21,14 +24,16 @@ 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
|
||||
@@ -46,6 +51,8 @@ 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?) {
|
||||
@@ -63,28 +70,13 @@ 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()
|
||||
@@ -120,6 +112,37 @@ 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() {
|
||||
@@ -128,13 +151,17 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
var hasChanges = false;
|
||||
val username = _editName.text.toString();
|
||||
if (username.length < 3) {
|
||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
|
||||
withContext(Dispatchers.Main) {
|
||||
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) {
|
||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
||||
}
|
||||
return@launch;
|
||||
}
|
||||
|
||||
@@ -219,6 +246,7 @@ 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);
|
||||
|
||||
+5
-2
@@ -19,8 +19,9 @@ 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, replyCount: Int? = null) {
|
||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) {
|
||||
this.contextUrl = contextUrl;
|
||||
this.author = author;
|
||||
this.message = msg;
|
||||
@@ -29,6 +30,7 @@ class PolycentricPlatformComment : IPlatformComment {
|
||||
this.replyCount = replyCount;
|
||||
this.eventPointer = eventPointer;
|
||||
this.reference = eventPointer.toReference();
|
||||
this.parentReference = parentReference;
|
||||
}
|
||||
|
||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
@@ -36,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
|
||||
}
|
||||
|
||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount);
|
||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PolycentricPlatformComment"
|
||||
val MAX_COMMENT_SIZE = 2000
|
||||
}
|
||||
}
|
||||
+1
@@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.modifier
|
||||
interface IModifierOptions {
|
||||
val applyAuthClient: String?;
|
||||
val applyCookieClient: String?;
|
||||
val applyOtherHeaders: Boolean;
|
||||
}
|
||||
+1
@@ -11,4 +11,5 @@ class SourcePluginAuthConfig(
|
||||
val userAgent: String? = null,
|
||||
val loginButton: String? = null,
|
||||
val domainHeadersToFind: Map<String, List<String>>? = null,
|
||||
val loginWarning: String? = null
|
||||
) { }
|
||||
+23
-2
@@ -2,6 +2,9 @@ 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
|
||||
@@ -55,7 +58,16 @@ class SourcePluginDescriptor {
|
||||
onCaptchaChanged.emit();
|
||||
}
|
||||
fun getCaptchaData(): SourceCaptchaData? {
|
||||
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAuth(str: SourceAuth?) {
|
||||
@@ -63,7 +75,16 @@ class SourcePluginDescriptor {
|
||||
onAuthChanged.emit();
|
||||
}
|
||||
fun getAuth(): SourceAuth? {
|
||||
return SourceAuth.fromEncrypted(authEncrypted);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
+23
-5
@@ -23,21 +23,31 @@ class JSRequest : IRequest {
|
||||
_v8Options = options;
|
||||
initialize(plugin, originalUrl, originalHeaders);
|
||||
}
|
||||
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?) {
|
||||
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, applyOtherHeadersByDefault: Boolean = false) {
|
||||
val contextName = "ModifyRequestResponse";
|
||||
val config = plugin.config;
|
||||
_v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
|
||||
_v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
|
||||
_v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
|
||||
Options(config, it);
|
||||
}
|
||||
Options(config, it, applyOtherHeadersByDefault);
|
||||
} ?: Options(null, null, applyOtherHeadersByDefault);
|
||||
initialize(plugin, originalUrl, originalHeaders);
|
||||
}
|
||||
|
||||
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
|
||||
val config = plugin.config;
|
||||
url = _v8Url ?: originalUrl;
|
||||
headers = _v8Headers ?: originalHeaders ?: mapOf();
|
||||
|
||||
if(_v8Options?.applyOtherHeaders ?: false) {
|
||||
val headersToSet = _v8Headers?.toMutableMap() ?: mutableMapOf();
|
||||
if (originalHeaders != null)
|
||||
for (head in originalHeaders)
|
||||
if (!headersToSet.containsKey(head.key))
|
||||
headersToSet[head.key] = head.value;
|
||||
headers = headersToSet;
|
||||
}
|
||||
else
|
||||
headers = _v8Headers ?: originalHeaders ?: mapOf();
|
||||
|
||||
if(_v8Options != null) {
|
||||
if(_v8Options.applyCookieClient != null && url != null) {
|
||||
@@ -68,10 +78,18 @@ class JSRequest : IRequest {
|
||||
class Options: IModifierOptions {
|
||||
override val applyAuthClient: String?;
|
||||
override val applyCookieClient: String?;
|
||||
override val applyOtherHeaders: Boolean;
|
||||
|
||||
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
|
||||
|
||||
constructor(config: IV8PluginConfig, obj: V8ValueObject, applyOtherHeadersByDefault: Boolean = false) {
|
||||
applyAuthClient = obj.getOrDefault(config, "applyAuthClient", "JSRequestModifier.options.applyAuthClient", null);
|
||||
applyCookieClient = obj.getOrDefault(config, "applyCookieClient", "JSRequestModifier.options.applyCookieClient", null);
|
||||
applyOtherHeaders = obj.getOrDefault(config, "applyOtherHeaders", "JSRequestModifier.options.applyOtherHeaders", applyOtherHeadersByDefault) ?: applyOtherHeadersByDefault;
|
||||
}
|
||||
constructor(applyAuthClient: String? = null, applyCookieClient: String? = null, applyOtherHeaders: Boolean = false) {
|
||||
this.applyAuthClient = applyAuthClient;
|
||||
this.applyCookieClient = applyCookieClient;
|
||||
this.applyOtherHeaders = applyOtherHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ class JSRequestModifier: IRequestModifier {
|
||||
} as V8ValueObject;
|
||||
|
||||
val req = JSRequest(_plugin, result, url, headers);
|
||||
result.close();
|
||||
return req;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ abstract class JSSource {
|
||||
this.type = type;
|
||||
|
||||
_requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
|
||||
JSRequest(plugin, it, null, null);
|
||||
JSRequest(plugin, it, null, null, true);
|
||||
}
|
||||
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ class AirPlayCastingDevice : CastingDevice {
|
||||
try {
|
||||
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
||||
if (connectedSocket == null) {
|
||||
delay(3000);
|
||||
delay(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import org.json.JSONObject
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocket
|
||||
@@ -303,17 +305,18 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
_thread = Thread {
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
|
||||
var connectedSocket: Socket? = null
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
||||
if (connectedSocket == null) {
|
||||
Thread.sleep(3000);
|
||||
val resultSocket = getConnectedSocket(adrs.toList(), port);
|
||||
if (resultSocket == null) {
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
connectedSocket = resultSocket
|
||||
usedRemoteAddress = connectedSocket.inetAddress;
|
||||
localAddress = connectedSocket.localAddress;
|
||||
connectedSocket.close();
|
||||
break;
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e)
|
||||
@@ -325,6 +328,8 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
val factory = sslContext.socketFactory;
|
||||
|
||||
val address = InetSocketAddress(usedRemoteAddress, port)
|
||||
|
||||
//Connection loop
|
||||
while (_scopeIO?.isActive == true) {
|
||||
Logger.i(TAG, "Connecting to Chromecast.");
|
||||
@@ -332,7 +337,16 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
try {
|
||||
_socket?.close()
|
||||
_socket = factory.createSocket(usedRemoteAddress, port) as SSLSocket;
|
||||
if (connectedSocket != null) {
|
||||
Logger.i(TAG, "Using connected socket.")
|
||||
_socket = factory.createSocket(connectedSocket, connectedSocket.inetAddress.hostAddress, connectedSocket.port, true) as SSLSocket
|
||||
connectedSocket = null
|
||||
} else {
|
||||
Logger.i(TAG, "Using new socket.")
|
||||
val s = Socket().apply { this.connect(address, 2000) }
|
||||
_socket = factory.createSocket(s, s.inetAddress.hostAddress, s.port, true) as SSLSocket
|
||||
}
|
||||
|
||||
_socket?.startHandshake();
|
||||
Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port");
|
||||
|
||||
@@ -347,7 +361,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
Logger.i(TAG, "Failed to connect to Chromecast.", e);
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -363,7 +377,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
_socket?.close();
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -415,7 +429,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
Logger.i(TAG, "Socket disconnected.");
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped connection loop.");
|
||||
@@ -432,10 +446,11 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
|
||||
Thread.sleep(5000);
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send ping.");
|
||||
}
|
||||
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped ping loop.");
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
|
||||
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
|
||||
import com.futo.platformplayer.casting.models.FCastVersionMessage
|
||||
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
|
||||
import com.futo.platformplayer.ensureNotMainThread
|
||||
import com.futo.platformplayer.getConnectedSocket
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
@@ -27,11 +28,12 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.math.BigInteger
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPair
|
||||
@@ -81,12 +83,15 @@ class FCastCastingDevice : CastingDevice {
|
||||
var port: Int = 0;
|
||||
|
||||
private var _socket: Socket? = null;
|
||||
private var _outputStream: DataOutputStream? = null;
|
||||
private var _inputStream: DataInputStream? = null;
|
||||
private var _outputStream: OutputStream? = null;
|
||||
private var _inputStream: InputStream? = null;
|
||||
private var _scopeIO: CoroutineScope? = null;
|
||||
private var _started: Boolean = false;
|
||||
private var _version: Long = 1;
|
||||
private var _thread: Thread? = null
|
||||
private var _pingThread: Thread? = null
|
||||
private var _lastPongTime = -1L
|
||||
private var _outputStreamLock = Object()
|
||||
|
||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||
this.name = name;
|
||||
@@ -206,7 +211,13 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
|
||||
if(Looper.getMainLooper().thread == Thread.currentThread()) {
|
||||
_scopeIO?.launch { action(); }
|
||||
_scopeIO?.launch {
|
||||
try {
|
||||
action();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to invoke in IO scope.", e)
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -240,77 +251,113 @@ class FCastCastingDevice : CastingDevice {
|
||||
val adrs = addresses ?: return;
|
||||
|
||||
val thread = _thread
|
||||
if (thread == null || !thread.isAlive) {
|
||||
Log.i(TAG, "Restarting thread because the thread has died")
|
||||
val pingThread = _pingThread
|
||||
if (_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.")
|
||||
}
|
||||
|
||||
_scopeIO?.cancel();
|
||||
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
|
||||
_scopeIO = CoroutineScope(Dispatchers.IO);
|
||||
|
||||
_thread = Thread {
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Log.i(TAG, "Connection thread started.")
|
||||
|
||||
var connectedSocket: Socket? = null
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
||||
if (connectedSocket == null) {
|
||||
Thread.sleep(3000);
|
||||
Log.i(TAG, "getConnectedSocket (adrs = [ ${adrs.joinToString(", ")} ], port = ${port}).")
|
||||
|
||||
val resultSocket = getConnectedSocket(adrs.toList(), port);
|
||||
|
||||
if (resultSocket == null) {
|
||||
Log.i(TAG, "Connection failed, waiting 1 seconds.")
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
usedRemoteAddress = connectedSocket.inetAddress;
|
||||
localAddress = connectedSocket.localAddress;
|
||||
connectedSocket.close();
|
||||
Log.i(TAG, "Connection succeeded.")
|
||||
|
||||
connectedSocket = resultSocket
|
||||
usedRemoteAddress = connectedSocket.inetAddress
|
||||
localAddress = connectedSocket.localAddress
|
||||
break;
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(ChromecastCastingDevice.TAG, "Failed to get setup initial connection to FastCast device.", e)
|
||||
Logger.w(TAG, "Failed to get setup initial connection to FastCast device.", e)
|
||||
}
|
||||
}
|
||||
|
||||
val address = InetSocketAddress(usedRemoteAddress, port)
|
||||
|
||||
//Connection loop
|
||||
while (_scopeIO?.isActive == true) {
|
||||
Logger.i(TAG, "Connecting to FastCast.");
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
|
||||
try {
|
||||
_socket = Socket(usedRemoteAddress, port);
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
if (connectedSocket != null) {
|
||||
Logger.i(TAG, "Using connected socket.");
|
||||
_socket = connectedSocket
|
||||
connectedSocket = null
|
||||
} else {
|
||||
Logger.i(TAG, "Using new socket.");
|
||||
_socket = Socket().apply { this.connect(address, 2000) };
|
||||
}
|
||||
Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port");
|
||||
|
||||
_outputStream = DataOutputStream(_socket?.outputStream);
|
||||
_inputStream = DataInputStream(_socket?.inputStream);
|
||||
_outputStream = _socket?.outputStream;
|
||||
_inputStream = _socket?.inputStream;
|
||||
} catch (e: IOException) {
|
||||
_socket?.close();
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
Logger.i(TAG, "Failed to connect to FastCast.", e);
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
localAddress = _socket?.localAddress;
|
||||
connectionState = CastConnectionState.CONNECTED;
|
||||
_lastPongTime = -1L
|
||||
|
||||
val buffer = ByteArray(4096);
|
||||
|
||||
Logger.i(TAG, "Started receiving.");
|
||||
var exceptionOccurred = false;
|
||||
while (_scopeIO?.isActive == true && !exceptionOccurred) {
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
val inputStream = _inputStream ?: break;
|
||||
Log.d(TAG, "Receiving next packet...");
|
||||
val b1 = inputStream.readUnsignedByte();
|
||||
val b2 = inputStream.readUnsignedByte();
|
||||
val b3 = inputStream.readUnsignedByte();
|
||||
val b4 = inputStream.readUnsignedByte();
|
||||
val size = ((b4.toLong() shl 24) or (b3.toLong() shl 16) or (b2.toLong() shl 8) or b1.toLong()).toInt();
|
||||
|
||||
var headerBytesRead = 0
|
||||
while (headerBytesRead < 4) {
|
||||
val read = inputStream.read(buffer, headerBytesRead, 4 - headerBytesRead)
|
||||
if (read == -1)
|
||||
throw Exception("Stream closed")
|
||||
headerBytesRead += read
|
||||
}
|
||||
|
||||
val size = ((buffer[3].toLong() shl 24) or (buffer[2].toLong() shl 16) or (buffer[1].toLong() shl 8) or buffer[0].toLong()).toInt();
|
||||
if (size > buffer.size) {
|
||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||
inputStream.skip(size.toLong());
|
||||
continue;
|
||||
Logger.w(TAG, "Packets larger than $size bytes are not supported.")
|
||||
break
|
||||
}
|
||||
|
||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||
inputStream.read(buffer, 0, size);
|
||||
var bytesRead = 0
|
||||
while (bytesRead < size) {
|
||||
val read = inputStream.read(buffer, bytesRead, size - bytesRead)
|
||||
if (read == -1)
|
||||
throw Exception("Stream closed")
|
||||
bytesRead += read
|
||||
}
|
||||
|
||||
val messageBytes = buffer.sliceArray(IntRange(0, size));
|
||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||
@@ -324,31 +371,68 @@ class FCastCastingDevice : CastingDevice {
|
||||
try {
|
||||
handleMessage(Opcode.find(opcode), json);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to handle message.", e);
|
||||
Logger.w(TAG, "Failed to handle message.", e)
|
||||
break
|
||||
}
|
||||
} catch (e: java.net.SocketException) {
|
||||
Logger.e(TAG, "Socket exception while receiving.", e);
|
||||
exceptionOccurred = true;
|
||||
break
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Exception while receiving.", e);
|
||||
exceptionOccurred = true;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_socket?.close();
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
Logger.i(TAG, "Socket disconnected.");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to close socket.", e)
|
||||
}
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped connection loop.");
|
||||
connectionState = CastConnectionState.DISCONNECTED;
|
||||
}.apply { start() };
|
||||
}.apply { start() }
|
||||
|
||||
_pingThread = Thread {
|
||||
Logger.i(TAG, "Started ping loop.")
|
||||
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
send(Opcode.Ping)
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send ping.")
|
||||
|
||||
try {
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to close socket.", e)
|
||||
}
|
||||
}
|
||||
|
||||
/*if (_lastPongTime != -1L && System.currentTimeMillis() - _lastPongTime > 6000) {
|
||||
Logger.w(TAG, "Closing socket due to last pong time being larger than 6 seconds.")
|
||||
|
||||
try {
|
||||
_socket?.close()
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to close socket.", e)
|
||||
}
|
||||
}*/
|
||||
|
||||
Thread.sleep(2000)
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped ping loop.");
|
||||
}.apply { start() }
|
||||
} else {
|
||||
Log.i(TAG, "Thread was still alive, not restarted")
|
||||
}
|
||||
@@ -401,39 +485,44 @@ class FCastCastingDevice : CastingDevice {
|
||||
Logger.i(TAG, "Remote version received: $version")
|
||||
}
|
||||
Opcode.Ping -> send(Opcode.Pong)
|
||||
Opcode.Pong -> _lastPongTime = System.currentTimeMillis()
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun send(opcode: Opcode, message: String? = null) {
|
||||
try {
|
||||
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
||||
val size = 1 + data.size
|
||||
val outputStream = _outputStream
|
||||
if (outputStream == null) {
|
||||
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
|
||||
return
|
||||
ensureNotMainThread()
|
||||
|
||||
synchronized (_outputStreamLock) {
|
||||
try {
|
||||
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
||||
val size = 1 + data.size
|
||||
val outputStream = _outputStream
|
||||
if (outputStream == null) {
|
||||
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
|
||||
return
|
||||
}
|
||||
|
||||
val serializedSizeLE = ByteArray(4)
|
||||
serializedSizeLE[0] = (size and 0xff).toByte()
|
||||
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
|
||||
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
|
||||
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
|
||||
outputStream.write(serializedSizeLE)
|
||||
|
||||
val opcodeBytes = ByteArray(1)
|
||||
opcodeBytes[0] = opcode.value
|
||||
outputStream.write(opcodeBytes)
|
||||
|
||||
if (data.isNotEmpty()) {
|
||||
outputStream.write(data)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to send message.", e)
|
||||
throw e
|
||||
}
|
||||
|
||||
val serializedSizeLE = ByteArray(4)
|
||||
serializedSizeLE[0] = (size and 0xff).toByte()
|
||||
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
|
||||
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
|
||||
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
|
||||
outputStream.write(serializedSizeLE)
|
||||
|
||||
val opcodeBytes = ByteArray(1)
|
||||
opcodeBytes[0] = opcode.value
|
||||
outputStream.write(opcodeBytes)
|
||||
|
||||
if (data.isNotEmpty()) {
|
||||
outputStream.write(data)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to send message.", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,6 +542,7 @@ class FCastCastingDevice : CastingDevice {
|
||||
_started = false;
|
||||
//TODO: Kill and/or join thread?
|
||||
_thread = null;
|
||||
_pingThread = null;
|
||||
|
||||
val socket = _socket;
|
||||
val scopeIO = _scopeIO;
|
||||
@@ -462,6 +552,8 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
scopeIO.launch {
|
||||
socket.close();
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
connectionState = CastConnectionState.DISCONNECTED;
|
||||
scopeIO.cancel();
|
||||
Logger.i(TAG, "Cancelled scopeIO with open socket.")
|
||||
|
||||
@@ -6,11 +6,12 @@ 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.*
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
@@ -25,7 +26,11 @@ 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.*
|
||||
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.google.android.material.button.MaterialButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -93,7 +98,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, null, ref)
|
||||
val eventPointer = processHandle.post(comment, ref)
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
@@ -118,7 +123,8 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||
msg = comment,
|
||||
rating = RatingLikeDislikes(0, 0),
|
||||
date = OffsetDateTime.now(),
|
||||
eventPointer = eventPointer
|
||||
eventPointer = eventPointer,
|
||||
parentReference = ref
|
||||
));
|
||||
|
||||
dismiss();
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -26,8 +27,8 @@ import kotlinx.coroutines.launch
|
||||
class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
private lateinit var _imageLoader: ImageView;
|
||||
private lateinit var _buttonClose: Button;
|
||||
private lateinit var _buttonAdd: Button;
|
||||
private lateinit var _buttonScanQR: Button;
|
||||
private lateinit var _buttonAdd: ImageButton;
|
||||
private lateinit var _buttonScanQR: ImageButton;
|
||||
private lateinit var _textNoDevicesFound: TextView;
|
||||
private lateinit var _textNoDevicesRemembered: TextView;
|
||||
private lateinit var _recyclerDevices: RecyclerView;
|
||||
|
||||
@@ -143,7 +143,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
||||
_sliderPosition.valueTo = it.toFloat().coerceAtLeast(1.0f);
|
||||
val dur = it.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur);
|
||||
_sliderPosition.valueTo = dur
|
||||
};
|
||||
|
||||
_device = StateCasting.instance.activeDevice;
|
||||
@@ -185,8 +187,10 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
_sliderPosition.valueFrom = 0.0f;
|
||||
_sliderVolume.valueFrom = 0.0f;
|
||||
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||
_sliderPosition.valueTo = d.duration.toFloat().coerceAtLeast(1.0f);
|
||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||
|
||||
val dur = d.duration.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||
_sliderPosition.valueTo = dur
|
||||
|
||||
if (d.canSetVolume) {
|
||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||
|
||||
@@ -337,8 +337,10 @@ class VideoDownload {
|
||||
});
|
||||
}
|
||||
|
||||
var wasSuccesful = false;
|
||||
try {
|
||||
awaitAll(*sourcesToDownload.toTypedArray());
|
||||
wasSuccesful = true;
|
||||
}
|
||||
catch(runtimeEx: RuntimeException) {
|
||||
if(runtimeEx.cause != null)
|
||||
@@ -349,6 +351,29 @@ class VideoDownload {
|
||||
catch(ex: Throwable) {
|
||||
throw ex;
|
||||
}
|
||||
finally {
|
||||
if(!wasSuccesful) {
|
||||
try {
|
||||
if(videoFilePath != null) {
|
||||
val remainingVideo = File(videoFilePath!!);
|
||||
if (remainingVideo.exists()) {
|
||||
Logger.i(TAG, "Deleting remaining video file");
|
||||
remainingVideo.delete();
|
||||
}
|
||||
}
|
||||
if(audioFilePath != null) {
|
||||
val remainingAudio = File(audioFilePath!!);
|
||||
if (remainingAudio.exists()) {
|
||||
Logger.i(TAG, "Deleting remaining audio file");
|
||||
remainingAudio.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(iex: Throwable) {
|
||||
Logger.e(TAG, "Failed to delete files after failure:\n${iex.message}", iex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
|
||||
@@ -5,8 +5,11 @@ 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
|
||||
|
||||
@@ -65,7 +68,7 @@ class PackageDOMParser : V8Package {
|
||||
return result;
|
||||
}
|
||||
@V8Property
|
||||
fun attributes(): Map<String, String> = _element.attributes().dataset();
|
||||
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||
@V8Property
|
||||
fun innerHTML(): String = _element.html();
|
||||
@V8Property
|
||||
@@ -138,10 +141,32 @@ 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -27,6 +27,7 @@ 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
|
||||
@@ -336,8 +337,11 @@ 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${kv.value.message}", false);
|
||||
UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n" +
|
||||
(if(!channel.isNullOrEmpty()) "(${channel}) " else "") +
|
||||
"${kv.value.message}", false);
|
||||
else
|
||||
UIDialogs.toast(it, kv.value.message ?: "", false);
|
||||
} catch (e: Throwable) {
|
||||
|
||||
+12
-5
@@ -289,10 +289,17 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
buttonDefinitions.find { d -> d.id == it.id }
|
||||
}.toMutableList()
|
||||
|
||||
if (!StatePayment.instance.hasPaid) {
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||
//Add unconfigured tabs with default values
|
||||
buttonDefinitions.forEach { buttonDefinition ->
|
||||
if (!Settings.instance.tabs.any { it.id == buttonDefinition.id }) {
|
||||
newCurrentButtonDefinitions.add(buttonDefinition)
|
||||
}
|
||||
}
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.faq, canToggle = false, { false }, {
|
||||
|
||||
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(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
|
||||
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||
}))
|
||||
|
||||
@@ -349,8 +356,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
|
||||
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
|
||||
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }),
|
||||
ButtonDefinition(10, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
|
||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
|
||||
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
|
||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
||||
val c = it.context ?: return@ButtonDefinition;
|
||||
Logger.i(TAG, "settings preventPictureInPicture()");
|
||||
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
||||
|
||||
+31
-6
@@ -22,15 +22,17 @@ 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 = object: WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
this.webViewClient = _webviewWithoutHandling;
|
||||
this.settings.javaScriptEnabled = true;
|
||||
CookieManager.getInstance().setAcceptCookie(true);
|
||||
this.settings.domStorageEnabled = true;
|
||||
@@ -41,8 +43,26 @@ class BrowserFragment : MainFragment() {
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack)
|
||||
|
||||
if(parameter is String)
|
||||
if(parameter is String) {
|
||||
_webview?.webViewClient = _webviewWithoutHandling;
|
||||
_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");
|
||||
}
|
||||
@@ -59,4 +79,9 @@ class BrowserFragment : MainFragment() {
|
||||
companion object {
|
||||
fun newInstance() = BrowserFragment().apply {}
|
||||
}
|
||||
|
||||
class NavigateOptions(
|
||||
val url: String,
|
||||
val urlHandlers: Map<String, (WebResourceRequest)->Unit>? = null
|
||||
)
|
||||
}
|
||||
+2
-1
@@ -418,6 +418,7 @@ class ChannelFragment : MainFragment() {
|
||||
|
||||
_buttonSubscribe.setSubscribeChannel(channel);
|
||||
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
||||
_textChannel.text = channel.name;
|
||||
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else "";
|
||||
|
||||
//TODO: Find a better way to access the adapter fragments..
|
||||
@@ -465,7 +466,7 @@ class ChannelFragment : MainFragment() {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
||||
|
||||
+12
@@ -117,6 +117,7 @@ class CommentsFragment : MainFragment() {
|
||||
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
|
||||
holder.onDelete.subscribe(::onDelete);
|
||||
holder.onRepliesClick.subscribe(::onRepliesClick);
|
||||
holder.onClick.subscribe(::onClick);
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
);
|
||||
@@ -200,6 +201,17 @@ 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 = "";
|
||||
|
||||
+8
-2
@@ -154,8 +154,14 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
};
|
||||
|
||||
onSearch.subscribe(this) {
|
||||
if(it.isHttpUrl())
|
||||
navigate<VideoDetailFragment>(it);
|
||||
if(it.isHttpUrl()) {
|
||||
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||
navigate<PlaylistFragment>(it);
|
||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||
navigate<ChannelFragment>(it);
|
||||
else
|
||||
navigate<VideoDetailFragment>(it);
|
||||
}
|
||||
else
|
||||
setQuery(it, true);
|
||||
};
|
||||
|
||||
+43
-1
@@ -8,6 +8,7 @@ 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
|
||||
@@ -17,15 +18,20 @@ 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 java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@@ -147,6 +153,40 @@ 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/", mapOf(
|
||||
Pair("grayjay") { req ->
|
||||
StateApp.instance.contextOrNull?.let {
|
||||
if(it is MainActivity) {
|
||||
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();
|
||||
}
|
||||
@@ -161,13 +201,15 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-17
@@ -314,8 +314,8 @@ class PostDetailFragment : MainFragment {
|
||||
private fun updatePolycentricRating() {
|
||||
_rating.visibility = View.GONE;
|
||||
|
||||
val value = _post?.id?.value ?: _postOverview?.id?.value ?: return;
|
||||
val ref = Models.referenceFromBuffer(value.toByteArray());
|
||||
val ref = Models.referenceFromBuffer((_post?.url ?: _postOverview?.url)?.toByteArray() ?: return)
|
||||
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
||||
val version = _version;
|
||||
|
||||
_rating.onLikeDislikeUpdated.remove(this);
|
||||
@@ -333,7 +333,8 @@ class PostDetailFragment : MainFragment {
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||
ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||
)
|
||||
),
|
||||
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||
);
|
||||
|
||||
if (version != _version) {
|
||||
@@ -342,8 +343,8 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (version != _version) {
|
||||
@@ -468,9 +469,7 @@ class PostDetailFragment : MainFragment {
|
||||
if (_postOverview == null) {
|
||||
fetchPolycentricProfile();
|
||||
updatePolycentricRating();
|
||||
|
||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
||||
_addCommentView.setContext(value.url, ref);
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
}
|
||||
|
||||
updateCommentType(true);
|
||||
@@ -489,9 +488,7 @@ 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);
|
||||
|
||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
||||
_addCommentView.setContext(value.url, ref);
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
|
||||
updatePolycentricRating();
|
||||
fetchPolycentricProfile();
|
||||
@@ -636,12 +633,12 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
if (cachedPolycentricProfile?.profile == null) {
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
_creatorThumbnail.setHarborAvailable(false, animate);
|
||||
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||
return;
|
||||
}
|
||||
|
||||
_layoutMonetization.visibility = View.VISIBLE;
|
||||
_creatorThumbnail.setHarborAvailable(true, animate);
|
||||
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
|
||||
}
|
||||
|
||||
private fun fetchPost() {
|
||||
@@ -665,14 +662,16 @@ class PostDetailFragment : MainFragment {
|
||||
private fun fetchPolycentricComments() {
|
||||
Logger.i(TAG, "fetchPolycentricComments")
|
||||
val post = _post;
|
||||
val idValue = post?.id?.value
|
||||
if (idValue == null) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
|
||||
val ref = (_post?.url ?: _postOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) }
|
||||
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
||||
|
||||
if (ref == null) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
|
||||
_commentsList.clear();
|
||||
return
|
||||
}
|
||||
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post.url, Models.referenceFromBuffer(idValue.toByteArray())); };
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
|
||||
}
|
||||
|
||||
private fun updateCommentType(reloadComments: Boolean) {
|
||||
|
||||
+34
-10
@@ -295,17 +295,24 @@ 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),
|
||||
BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
|
||||
if(!isEmbedded) 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) {
|
||||
@@ -325,7 +332,6 @@ 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) {
|
||||
|
||||
@@ -333,9 +339,15 @@ class SourceDetailFragment : MainFragment() {
|
||||
this.alpha = 0.5f;
|
||||
},
|
||||
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
||||
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
|
||||
reloadSource(config.id);
|
||||
UIDialogs.toast(context, "Embedded plugin reinstalled, may require 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));
|
||||
}.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);
|
||||
@@ -354,11 +366,22 @@ class SourceDetailFragment : MainFragment() {
|
||||
if(config.authentication == null)
|
||||
return;
|
||||
|
||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||
|
||||
reloadSource(config.id);
|
||||
};
|
||||
if(config.authentication.loginWarning != null) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Login Warning",
|
||||
config.authentication.loginWarning, null, 0,
|
||||
UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Login", {
|
||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||
reloadSource(config.id);
|
||||
};
|
||||
}, UIDialogs.ActionStyle.PRIMARY))
|
||||
}
|
||||
else
|
||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||
reloadSource(config.id);
|
||||
};
|
||||
}
|
||||
private fun logoutSource(clear: Boolean = true) {
|
||||
val config = _config ?: return;
|
||||
@@ -454,6 +477,7 @@ class SourceDetailFragment : MainFragment() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fun checkForUpdatesSource() {
|
||||
val c = _config ?: return;
|
||||
val sourceUrl = c.sourceUrl ?: return;
|
||||
|
||||
+3
-1
@@ -39,10 +39,12 @@ class SourcesFragment : MainFragment() {
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack)
|
||||
|
||||
if(topBar is AddTopBarFragment)
|
||||
if(topBar is AddTopBarFragment) {
|
||||
(topBar as AddTopBarFragment).onAdd.clear();
|
||||
(topBar as AddTopBarFragment).onAdd.subscribe {
|
||||
startActivity(Intent(requireContext(), AddSourceOptionsActivity::class.java));
|
||||
};
|
||||
}
|
||||
|
||||
_view?.reloadSources();
|
||||
}
|
||||
|
||||
+36
-7
@@ -4,19 +4,15 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -25,14 +21,13 @@ import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.SearchView
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
||||
import com.futo.platformplayer.views.overlays.CreatorSelectOverlay
|
||||
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
|
||||
@@ -64,6 +59,11 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
return view;
|
||||
}
|
||||
|
||||
override fun onHide() {
|
||||
super.onHide();
|
||||
_view?.onHide();
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SourcesFragment";
|
||||
fun newInstance() = SubscriptionGroupFragment().apply {}
|
||||
@@ -86,7 +86,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
private val _buttonSettings: ImageButton;
|
||||
private val _buttonDelete: ImageButton;
|
||||
|
||||
private val _buttonAddCreator: Button;
|
||||
private val _buttonAddCreator: FrameLayout;
|
||||
|
||||
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
|
||||
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
|
||||
@@ -97,6 +97,8 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
|
||||
private var _group: SubscriptionGroup? = null;
|
||||
|
||||
private var _didDelete: Boolean = false;
|
||||
|
||||
constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
|
||||
inflate(context, R.layout.fragment_subscriptions_group, this);
|
||||
_fragment = fragment;
|
||||
@@ -137,6 +139,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
UIDialogs.Action("Delete", {
|
||||
_group?.let {
|
||||
it.urls.remove(channel.url);
|
||||
save();
|
||||
reloadCreators(it);
|
||||
}
|
||||
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||
@@ -178,6 +181,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
UIDialogs.Action("Cancel", {}),
|
||||
UIDialogs.Action("Delete", {
|
||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
|
||||
_didDelete = true;
|
||||
fragment.close(true);
|
||||
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||
};
|
||||
@@ -188,6 +192,12 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
filterCreators();
|
||||
}
|
||||
|
||||
_topbar.setButtons(
|
||||
Pair(R.drawable.ic_share) {
|
||||
UIDialogs.toast(context, "Coming soon");
|
||||
}
|
||||
);
|
||||
|
||||
setGroup(null);
|
||||
}
|
||||
|
||||
@@ -240,6 +250,18 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
_overlay.animate().alpha(1f).setDuration(300).start();
|
||||
overlay.onSelected.subscribe {
|
||||
_group?.let { g ->
|
||||
if(g.urls.isEmpty() && g.image == null) {
|
||||
//Obtain image
|
||||
for(sub in it) {
|
||||
val sub = StateSubscriptions.instance.getSubscription(sub);
|
||||
if(sub != null && sub.channel.thumbnail != null) {
|
||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||
g.image?.setImageView(_imageGroup);
|
||||
g.image?.setImageView(_imageGroupBackground);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(url in it) {
|
||||
if(!g.urls.contains(url))
|
||||
g.urls.add(url);
|
||||
@@ -256,6 +278,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
|
||||
|
||||
fun setGroup(group: SubscriptionGroup?) {
|
||||
_didDelete = false;
|
||||
_group = group;
|
||||
_textGroupTitle.text = group?.name;
|
||||
|
||||
@@ -272,6 +295,12 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
reloadCreators(group);
|
||||
}
|
||||
|
||||
fun onHide() {
|
||||
if(!_didDelete && _group != null && StateSubscriptionGroups.instance.getSubscriptionGroup(_group!!.id) === null) {
|
||||
UIDialogs.toast(context, "Group creation cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun reloadCreators(group: SubscriptionGroup?) {
|
||||
_enabledCreators.clear();
|
||||
|
||||
+3
-1
@@ -107,12 +107,14 @@ class SubscriptionGroupListFragment : MainFragment() {
|
||||
updateGroups();
|
||||
}
|
||||
|
||||
if(topBar is AddTopBarFragment)
|
||||
if(topBar is AddTopBarFragment) {
|
||||
(topBar as AddTopBarFragment).onAdd.clear();
|
||||
(topBar as AddTopBarFragment).onAdd.subscribe {
|
||||
_overlay?.let {
|
||||
UISlideOverlays.showCreateSubscriptionGroup(it)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGroups() {
|
||||
|
||||
+49
-23
@@ -10,6 +10,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
@@ -30,6 +31,7 @@ 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
|
||||
@@ -43,6 +45,7 @@ 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
|
||||
|
||||
@@ -52,6 +55,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
override val hasBottomBar: Boolean get() = true;
|
||||
|
||||
private var _view: SubscriptionsFeedView? = null;
|
||||
private var _group: SubscriptionGroup? = null;
|
||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
@@ -72,6 +76,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = SubscriptionsFeedView(this, inflater, _cachedRecyclerData);
|
||||
_view = view;
|
||||
if(_group != null)
|
||||
view.selectSubgroup(_group);
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -80,6 +86,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
val view = _view;
|
||||
if (view != null) {
|
||||
_cachedRecyclerData = view.recyclerData;
|
||||
_group = view.subGroup;
|
||||
view.cleanup();
|
||||
_view = null;
|
||||
}
|
||||
@@ -100,20 +107,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
|
||||
|
||||
private var _subGroup: SubscriptionGroup? = null;
|
||||
var subGroup: SubscriptionGroup? = null;
|
||||
|
||||
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||
StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
|
||||
if(_subGroup?.id == id)
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
setProgress(progress, total);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set progress", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
|
||||
};
|
||||
|
||||
@@ -140,8 +137,9 @@ 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);
|
||||
@@ -168,12 +166,24 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
|
||||
finishRefreshLayoutLoader();
|
||||
}
|
||||
|
||||
StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
|
||||
if(subGroup?.id == id)
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
setProgress(progress, total);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set progress", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
super.cleanup()
|
||||
StateSubscriptions.instance.global.onUpdateProgress.remove(this);
|
||||
StateSubscriptions.instance.onSubscriptionsChanged.remove(this);
|
||||
StateSubscriptions.instance.onFeedProgress.remove(this);
|
||||
}
|
||||
|
||||
override val feedStyle: FeedStyle get() = Settings.instance.subscriptions.getSubscriptionsFeedStyle();
|
||||
@@ -197,7 +207,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");
|
||||
@@ -257,6 +267,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
}
|
||||
};
|
||||
|
||||
fun selectSubgroup(g: SubscriptionGroup?) {
|
||||
if(g != null)
|
||||
_subscriptionBar?.selectGroup(g);
|
||||
}
|
||||
|
||||
private fun initializeToolbarContent() {
|
||||
_subscriptionBar = SubscriptionBar(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
@@ -266,11 +281,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
if(g is SubscriptionGroup.Add)
|
||||
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
|
||||
else {
|
||||
_subGroup = g;
|
||||
subGroup = g;
|
||||
setProgress(0, 0);
|
||||
if(Settings.instance.subscriptions.fetchOnTabOpen)
|
||||
if(Settings.instance.subscriptions.fetchOnTabOpen) {
|
||||
loadCache();
|
||||
loadResults(false);
|
||||
else loadCache();
|
||||
}
|
||||
else if(g != null && StateSubscriptions.instance.getFeed(g.id) != null) {
|
||||
loadResults(false);
|
||||
}
|
||||
else
|
||||
loadCache();
|
||||
}
|
||||
};
|
||||
_subscriptionBar?.onHoldGroup?.subscribe { g ->
|
||||
@@ -311,7 +332,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);
|
||||
|
||||
@@ -411,7 +432,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
context?.let {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
if (exs.size <= 8) {
|
||||
if (exs.size <= 3) {
|
||||
for (ex in exs) {
|
||||
var toShow = ex;
|
||||
var channel: String? = null;
|
||||
@@ -421,15 +442,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
}
|
||||
Logger.e(TAG, "Channel [${channel}] failed", ex);
|
||||
if (toShow is PluginException)
|
||||
UIDialogs.toast(
|
||||
it,
|
||||
context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
|
||||
UIDialogs.appToast(ToastView.Toast(
|
||||
toShow.message +
|
||||
(if(channel != null) "\nChannel: " + channel else ""), false, null,
|
||||
"Plugin ${toShow.config.name} failed")
|
||||
);
|
||||
else
|
||||
UIDialogs.toast(it, ex.message ?: "");
|
||||
UIDialogs.appToast(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 }
|
||||
@@ -437,7 +460,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
.map { it!! }
|
||||
.toList();
|
||||
for(distinctPluginFail in failedPlugins)
|
||||
UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
|
||||
UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
|
||||
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"));
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to handle exceptions", e)
|
||||
|
||||
+26
-13
@@ -67,7 +67,7 @@ class TutorialFragment : MainFragment() {
|
||||
|
||||
addView(createHeader("Initial setup"))
|
||||
initialSetupVideos.forEach {
|
||||
addView(createTutorialPill(R.drawable.ic_movie, it.name).apply {
|
||||
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).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).apply {
|
||||
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).apply {
|
||||
onClick.subscribe {
|
||||
fragment.navigate<VideoDetailFragment>(it)
|
||||
}
|
||||
@@ -95,10 +95,11 @@ class TutorialFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTutorialPill(iconPrefix: Int, t: String): WidePillButton {
|
||||
private fun createTutorialPill(iconPrefix: Int, t: String, d: 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))
|
||||
@@ -107,9 +108,9 @@ class TutorialFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
class TutorialVideoSourceDescriptor(url: String, duration: Long) : VideoUnMuxedSourceDescriptor() {
|
||||
class TutorialVideoSourceDescriptor(url: String, duration: Long, width: Int, height: Int) : VideoUnMuxedSourceDescriptor() {
|
||||
override val videoSources: Array<IVideoSource> = arrayOf(
|
||||
VideoUrlSource("1080p", url, 1920, 1080, duration, "video/mp4")
|
||||
VideoUrlSource("Original", url, width, height, duration, "video/mp4")
|
||||
)
|
||||
override val audioSources: Array<IAudioSource> = arrayOf()
|
||||
}
|
||||
@@ -120,7 +121,9 @@ class TutorialFragment : MainFragment() {
|
||||
override val description: String,
|
||||
thumbnailUrl: String,
|
||||
videoUrl: String,
|
||||
override val duration: Long
|
||||
override val duration: Long,
|
||||
width: Int = 1920,
|
||||
height: Int = 1080
|
||||
) : IPlatformVideoDetails {
|
||||
override val id: PlatformID = PlatformID("tutorial", uuid)
|
||||
override val contentType: ContentType = ContentType.MEDIA
|
||||
@@ -129,15 +132,15 @@ class TutorialFragment : MainFragment() {
|
||||
override val dash: IDashManifestSource? = null
|
||||
override val hls: IHLSManifestSource? = null
|
||||
override val subtitles: List<ISubtitleSource> = emptyList()
|
||||
override val shareUrl: String = ""
|
||||
override val url: String = ""
|
||||
override val shareUrl: String = videoUrl
|
||||
override val url: String = videoUrl
|
||||
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)
|
||||
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
return EmptyPager()
|
||||
}
|
||||
@@ -163,7 +166,7 @@ class TutorialFragment : MainFragment() {
|
||||
TutorialVideo(
|
||||
uuid = "3b99ebfe-2640-4643-bfe0-a0cf04261fc5",
|
||||
name = "Getting started",
|
||||
description = "Learn how to get started with Grayjay.",
|
||||
description = "Learn how to get started with Grayjay. How do you install plugins?",
|
||||
thumbnailUrl = "https://releases.grayjay.app/tutorials/getting-started.jpg",
|
||||
videoUrl = "https://releases.grayjay.app/tutorials/getting-started.mp4",
|
||||
duration = 50
|
||||
@@ -171,7 +174,7 @@ class TutorialFragment : MainFragment() {
|
||||
TutorialVideo(
|
||||
uuid = "793aa009-516c-4581-b82f-a8efdfef4c27",
|
||||
name = "Is Grayjay free?",
|
||||
description = "Learn how Grayjay is monetized.",
|
||||
description = "Learn how Grayjay is monetized. How do we make money?",
|
||||
thumbnailUrl = "https://releases.grayjay.app/tutorials/pay.jpg",
|
||||
videoUrl = "https://releases.grayjay.app/tutorials/pay.mp4",
|
||||
duration = 52
|
||||
@@ -182,7 +185,7 @@ class TutorialFragment : MainFragment() {
|
||||
TutorialVideo(
|
||||
uuid = "d2238d88-4252-4a91-a12d-b90c049bb7cf",
|
||||
name = "Searching",
|
||||
description = "Learn about searching in Grayjay.",
|
||||
description = "Learn about searching in Grayjay. How can I find channels, videos or playlists?",
|
||||
thumbnailUrl = "https://releases.grayjay.app/tutorials/search.jpg",
|
||||
videoUrl = "https://releases.grayjay.app/tutorials/search.mp4",
|
||||
duration = 39
|
||||
@@ -198,10 +201,20 @@ class TutorialFragment : MainFragment() {
|
||||
TutorialVideo(
|
||||
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
|
||||
name = "Casting",
|
||||
description = "Learn about casting in Grayjay.",
|
||||
description = "Learn about casting in Grayjay. How do I show video on my TV?",
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+4
-10
@@ -25,8 +25,6 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StateSaved
|
||||
import com.futo.platformplayer.states.VideoToOpen
|
||||
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||
|
||||
class VideoDetailFragment : MainFragment {
|
||||
@@ -171,14 +169,14 @@ class VideoDetailFragment : MainFragment {
|
||||
_view!!.transitionToStart();
|
||||
}
|
||||
fun maximizeVideoDetail(instant: Boolean = false) {
|
||||
if(_maximizeProgress > 0.9f && state != State.MAXIMIZED) {
|
||||
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
|
||||
state = State.MAXIMIZED;
|
||||
onMaximized.emit();
|
||||
}
|
||||
_view?.let {
|
||||
if(!instant)
|
||||
if(!instant) {
|
||||
it.transitionToEnd();
|
||||
else {
|
||||
} else {
|
||||
it.progress = 1f;
|
||||
onTransitioning.emit(true);
|
||||
}
|
||||
@@ -372,11 +370,6 @@ class VideoDetailFragment : MainFragment {
|
||||
|
||||
Logger.v(TAG, "shouldStop: $shouldStop");
|
||||
if(shouldStop) {
|
||||
_viewDetail?.let {
|
||||
val v = it.video ?: return@let;
|
||||
StateSaved.instance.setVideoToOpenBlocking(VideoToOpen(v.url, (it.lastPositionMilliseconds / 1000.0f).toLong()));
|
||||
}
|
||||
|
||||
_viewDetail?.onStop();
|
||||
StateCasting.instance.onStop();
|
||||
Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
|
||||
@@ -431,6 +424,7 @@ 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);
|
||||
|
||||
+121
-87
@@ -145,10 +145,11 @@ 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
|
||||
@@ -372,7 +373,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||
};
|
||||
|
||||
_container_content_liveChat.onRaidNow.subscribe {
|
||||
@@ -437,16 +438,20 @@ 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)) {selected ->
|
||||
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE), false) {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 {
|
||||
@@ -769,6 +774,7 @@ 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) {
|
||||
@@ -781,6 +787,7 @@ 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 {
|
||||
@@ -793,11 +800,13 @@ 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 {
|
||||
@@ -805,9 +814,11 @@ 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());
|
||||
@@ -843,14 +854,19 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val _historyIndexLock = Mutex(false);
|
||||
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
|
||||
val current = _historyIndex;
|
||||
if(current == null || current.url != video.url) {
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
||||
_historyIndex = index;
|
||||
return@withContext index;
|
||||
_historyIndexLock.withLock {
|
||||
val current = _historyIndex;
|
||||
if(current == null || current.url != video.url) {
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
||||
_historyIndex = index;
|
||||
return@withContext index;
|
||||
}
|
||||
return@withContext current;
|
||||
}
|
||||
return@withContext current;
|
||||
}
|
||||
|
||||
|
||||
@@ -987,6 +1003,9 @@ 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;
|
||||
@@ -1017,6 +1036,9 @@ 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);
|
||||
@@ -1111,10 +1133,13 @@ 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;
|
||||
@@ -1134,6 +1159,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(videoDetail is VideoLocal) {
|
||||
videoLocal = videoDetail;
|
||||
video = videoDetail;
|
||||
this.video = video;
|
||||
val videoTask = StatePlatform.instance.getContentDetails(videoDetail.url);
|
||||
videoTask.invokeOnCompletion { ex ->
|
||||
if(ex != null) {
|
||||
@@ -1201,12 +1227,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
};
|
||||
}
|
||||
|
||||
val ref = video.id.value?.let { Models.referenceFromBuffer(it.toByteArray()) };
|
||||
_addCommentView.setContext(video.url, ref);
|
||||
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
val extraBytesRef = video.id.value?.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);
|
||||
@@ -1264,57 +1290,54 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_rating.onLikeDislikeUpdated.remove(this);
|
||||
|
||||
if (ref != null) {
|
||||
_rating.visibility = View.GONE;
|
||||
_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()
|
||||
)
|
||||
);
|
||||
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)
|
||||
);
|
||||
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
||||
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*/;
|
||||
|
||||
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);
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
_rating.visibility = View.GONE;
|
||||
}
|
||||
|
||||
when (video.rating) {
|
||||
@@ -1357,32 +1380,36 @@ class VideoDetailView : ConstraintLayout {
|
||||
val toResume = _videoResumePositionMilliseconds;
|
||||
_videoResumePositionMilliseconds = 0;
|
||||
loadCurrentVideo(toResume);
|
||||
_player.setGestureSoundFactor(1.0f);
|
||||
if (!Settings.instance.gestureControls.useSystemVolume) {
|
||||
_player.setGestureSoundFactor(1.0f);
|
||||
}
|
||||
|
||||
updateQueueState();
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val historyItem = getHistoryIndex(videoDetail);
|
||||
if (video !is TutorialFragment.TutorialVideo) {
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1474,12 +1501,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
private fun loadCurrentVideo(resumePositionMs: Long = 0) {
|
||||
_didStop = false;
|
||||
|
||||
val video = video ?: return;
|
||||
val video = (videoLocal ?: video) ?: return;
|
||||
|
||||
try {
|
||||
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
|
||||
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
val subtitleSource = _lastSubtitleSource;
|
||||
val subtitleSource = _lastSubtitleSource ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
|
||||
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
|
||||
|
||||
if(videoSource == null && audioSource == null) {
|
||||
@@ -1507,6 +1534,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
_player.setArtwork(null);
|
||||
|
||||
_player.setSource(videoSource, audioSource, _playWhenReady, false);
|
||||
if(subtitleSource != null)
|
||||
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
|
||||
_player.seekTo(resumePositionMs);
|
||||
}
|
||||
else
|
||||
@@ -1514,6 +1543,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_lastVideoSource = videoSource;
|
||||
_lastAudioSource = audioSource;
|
||||
_lastSubtitleSource = subtitleSource;
|
||||
}
|
||||
catch(ex: UnsupportedCastException) {
|
||||
Logger.e(TAG, "Failed to load cast media", ex);
|
||||
@@ -1954,7 +1984,9 @@ class VideoDetailView : ConstraintLayout {
|
||||
return
|
||||
}
|
||||
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, Models.referenceFromBuffer(idValue.toByteArray())); };
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
val extraBytesRef = video.id.value?.toByteArray()
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); };
|
||||
}
|
||||
private fun fetchVideo() {
|
||||
Logger.i(TAG, "fetchVideo")
|
||||
@@ -2216,9 +2248,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
val v = video ?: return;
|
||||
val currentTime = System.currentTimeMillis();
|
||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val history = getHistoryIndex(v);
|
||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||
if (v !is TutorialFragment.TutorialVideo) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val history = getHistoryIndex(v);
|
||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||
}
|
||||
}
|
||||
_lastPositionSaveTime = currentTime;
|
||||
}
|
||||
@@ -2301,7 +2335,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
||||
@@ -2325,7 +2359,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
else if(isOverlayed) {
|
||||
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
|
||||
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -6f, resources.displayMetrics).toInt();
|
||||
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -2f, resources.displayMetrics).toInt();
|
||||
};
|
||||
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||
}
|
||||
@@ -2526,7 +2560,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
else
|
||||
withContext(Dispatchers.Main) {
|
||||
setVideoDetails(videoDetail);
|
||||
setVideoDetails(videoDetail, true);
|
||||
_liveTryJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -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.*
|
||||
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.models.SearchType
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.SearchHistoryStorage
|
||||
|
||||
class SearchTopBarFragment : TopFragment() {
|
||||
private val TAG = "SearchTopBarFragment"
|
||||
@@ -54,11 +54,12 @@ class SearchTopBarFragment : TopFragment() {
|
||||
|
||||
private val _searchDoneListener = object : TextView.OnEditorActionListener {
|
||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||
if (actionId != EditorInfo.IME_ACTION_DONE)
|
||||
val isEnterPress = event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN
|
||||
if (actionId != EditorInfo.IME_ACTION_DONE && !isEnterPress)
|
||||
return false
|
||||
|
||||
onDone();
|
||||
return true;
|
||||
onDone()
|
||||
return true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class PolycentricCache {
|
||||
|
||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
||||
{ system ->
|
||||
val signedProfileEvents = ApiMethods.getQueryLatest(
|
||||
val signedEventsList = ApiMethods.getQueryLatest(
|
||||
SERVER,
|
||||
system.toProto(),
|
||||
listOf(
|
||||
@@ -72,8 +72,9 @@ class PolycentricCache {
|
||||
ContentType.MEMBERSHIP_URLS.value,
|
||||
ContentType.DONATION_DESTINATIONS.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) }
|
||||
.groupBy { e -> e.event.contentType }
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||
|
||||
val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType }
|
||||
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
||||
|
||||
val storageSystemState = StorageTypeSystemState.create()
|
||||
@@ -151,17 +152,7 @@ class PolycentricCache {
|
||||
|
||||
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
||||
{
|
||||
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);
|
||||
val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported");
|
||||
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
||||
},
|
||||
{ return@BatchedTaskHandler null },
|
||||
@@ -325,9 +316,10 @@ class PolycentricCache {
|
||||
.build();
|
||||
|
||||
private const val TAG = "PolycentricCache"
|
||||
const val SERVER = "https://srv1-stg.polycentric.io"
|
||||
const val STAGING_SERVER = "https://srv1-stg.polycentric.io"
|
||||
const val SERVER = "https://srv1-prod.polycentric.io"
|
||||
private var _instance: PolycentricCache? = null;
|
||||
private val CACHE_EXPIRATION_SECONDS = 60 * 60 * 3;
|
||||
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||
|
||||
@JvmStatic
|
||||
val instance: PolycentricCache
|
||||
@@ -343,5 +335,20 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ class MediaPlaybackService : Service() {
|
||||
override fun onDestroy() {
|
||||
Logger.v(TAG, "onDestroy");
|
||||
_instance = null;
|
||||
MediaControlReceiver.onCloseReceived.emit();
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ class MediaPlaybackService : Service() {
|
||||
|
||||
fun closeMediaSession() {
|
||||
Logger.v(TAG, "closeMediaSession");
|
||||
stopForeground(STOP_FOREGROUND_DETACH);
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
|
||||
val focusRequest = _focusRequest;
|
||||
if (focusRequest != null) {
|
||||
@@ -162,7 +162,9 @@ class MediaPlaybackService : Service() {
|
||||
}
|
||||
_hasFocus = false;
|
||||
|
||||
_notificationManager?.cancel(MEDIA_NOTIF_ID);
|
||||
val notifManager = _notificationManager;
|
||||
Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
|
||||
notifManager?.cancel(MEDIA_NOTIF_ID);
|
||||
_notif_last_video = null;
|
||||
_notif_last_bitmap = null;
|
||||
_mediaSession = null;
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Color
|
||||
import android.media.AudioManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
@@ -38,6 +39,7 @@ import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
||||
import com.futo.platformplayer.services.DownloadService
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
@@ -380,13 +382,15 @@ class StateApp {
|
||||
|
||||
Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]");
|
||||
StatePolycentric.instance.load(context);
|
||||
Logger.i(TAG, "MainApp Starting: Initializing [Saved]");
|
||||
StateSaved.instance.load();
|
||||
|
||||
Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]");
|
||||
displayMetrics = context.resources.displayMetrics;
|
||||
ensureConnectivityManager(context);
|
||||
|
||||
Logger.i(TAG, "MainApp Starting: Cleaning up unused downloads");
|
||||
StateDownloads.instance.cleanupDownloads();
|
||||
|
||||
|
||||
Logger.i(TAG, "MainApp Starting: Initializing [Telemetry]");
|
||||
if (!BuildConfig.DEBUG) {
|
||||
StateTelemetry.instance.initialize();
|
||||
@@ -460,7 +464,9 @@ class StateApp {
|
||||
|
||||
//Foreground download
|
||||
autoUpdateEnabled -> {
|
||||
StateUpdate.instance.checkForUpdates(context, false);
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
StateUpdate.instance.checkForUpdates(context, false)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -558,6 +564,40 @@ class StateApp {
|
||||
|
||||
if(StateHistory.instance.shouldMigrateLegacyHistory())
|
||||
StateHistory.instance.migrateLegacyHistory();
|
||||
|
||||
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
|
||||
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val updateAvailable = StatePlatform.instance.checkForUpdates()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (updateAvailable.isNotEmpty()) {
|
||||
UIDialogs.appToast(
|
||||
ToastView.Toast(updateAvailable
|
||||
.map { " - " + it.name }
|
||||
.joinToString("\n"),
|
||||
true,
|
||||
null,
|
||||
"Plugin updates available"
|
||||
));
|
||||
|
||||
StateAnnouncement.instance.registerAnnouncement(
|
||||
"plugin-update",
|
||||
"Plugin updates available",
|
||||
"There are ${updateAvailable.size} plugin updates available.",
|
||||
AnnouncementType.SESSION_RECURRING
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
UIDialogs.appToast("This is a test", false);
|
||||
UIDialogs.appToast("This is a test 2", false);
|
||||
UIDialogs.appToastError("This is a test 3 (Error)", false);
|
||||
UIDialogs.appToast(ToastView.Toast("This is a test 4, with title", false, Color.WHITE, "Test title"));
|
||||
UIDialogs.appToast("This is a test 5 Long text\nWith enters\nasdh asfh fds h rwe h fxh sdfh sdf h dsfh sdf hasdfhsdhg ads as", true);
|
||||
*/
|
||||
}
|
||||
|
||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||
|
||||
@@ -49,13 +49,17 @@ 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();
|
||||
}
|
||||
.flatten()
|
||||
.distinct()
|
||||
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
||||
|
||||
Logger.i(TAG, "Subscriptions CachePager get pagers");
|
||||
val pagers: List<IPager<IPlatformContent>>;
|
||||
|
||||
@@ -352,7 +352,10 @@ class StateDownloads {
|
||||
|
||||
fun cleanupDownloads(): Pair<Int, Long> {
|
||||
val expected = getDownloadedVideos();
|
||||
val validFiles = HashSet(expected.flatMap { e -> e.videoSource.map { it.filePath } + e.audioSource.map { it.filePath } });
|
||||
val validFiles = HashSet(expected.flatMap { e ->
|
||||
e.videoSource.map { it.filePath } +
|
||||
e.audioSource.map { it.filePath } +
|
||||
e.subtitlesSources.map { it.filePath }});
|
||||
|
||||
var totalDeleted: Long = 0;
|
||||
var totalDeletedCount = 0;
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.collection.LruCache
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
||||
@@ -78,6 +79,7 @@ class StatePlatform {
|
||||
private val _clientsLock = Object();
|
||||
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
||||
private val _enabledClients : ArrayList<IPlatformClient> = ArrayList();
|
||||
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
||||
|
||||
//ClientPools are used to isolate plugin usage of certain components from others
|
||||
//This prevents for example a background task like subscriptions from blocking a user from opening a video
|
||||
@@ -839,6 +841,7 @@ 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})");
|
||||
@@ -932,6 +935,67 @@ class StatePlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
return updatesAvailableMap.contains(c.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
|
||||
var configs = mutableListOf<SourcePluginConfig>()
|
||||
val updatesAvailableFor = hashSetOf<String>()
|
||||
for (availableClient in getAvailableClients()) {
|
||||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (checkForUpdates(availableClient.config)) {
|
||||
configs.add(availableClient.config);
|
||||
updatesAvailableFor.add(availableClient.config.id)
|
||||
}
|
||||
}
|
||||
|
||||
_updatesAvailableMap = updatesAvailableFor
|
||||
return@withContext configs;
|
||||
}
|
||||
|
||||
fun clearUpdateAvailable(c: SourcePluginConfig) {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
updatesAvailableMap.remove(c.id)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkForUpdates(c: SourcePluginConfig): Boolean = withContext(Dispatchers.IO) {
|
||||
val sourceUrl = c.sourceUrl ?: return@withContext false;
|
||||
|
||||
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val response = client.get(sourceUrl);
|
||||
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
|
||||
|
||||
if (!response.isOk || response.body == null) {
|
||||
return@withContext false;
|
||||
}
|
||||
|
||||
val configJson = response.body.string();
|
||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||
|
||||
val config = SourcePluginConfig.fromJson(configJson);
|
||||
if (config.version <= c.version) {
|
||||
return@withContext false;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
|
||||
return@withContext true;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to check for updates.", e);
|
||||
return@withContext false;
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var _instance : StatePlatform? = null;
|
||||
val instance : StatePlatform
|
||||
|
||||
@@ -361,6 +361,12 @@ 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);
|
||||
@@ -407,6 +413,12 @@ 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) {
|
||||
@@ -421,6 +433,8 @@ class StatePlayer {
|
||||
}
|
||||
//Standard Behavior
|
||||
if(_queuePosition - 1 >= 0) {
|
||||
if(queue.size <= _queuePosition)
|
||||
return null;
|
||||
return queue[_queuePosition - 1];
|
||||
}
|
||||
//Repeat Behavior (End of queue)
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -35,6 +36,8 @@ 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();
|
||||
@@ -48,26 +51,32 @@ class StatePlaylists {
|
||||
}
|
||||
fun getWatchLater() : List<SerializedPlatformVideo> {
|
||||
synchronized(_watchlistStore) {
|
||||
return _watchlistStore.getItems();
|
||||
val order = _watchlistOrderStore.getAllValues();
|
||||
return _watchlistStore.getItems().sortedBy { order.indexOf(it.url) };
|
||||
}
|
||||
}
|
||||
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,7 +10,6 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||
import com.futo.platformplayer.developer.DeveloperEndpoints
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
@@ -161,6 +160,13 @@ 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) ?:
|
||||
@@ -467,7 +473,6 @@ class StatePlugins {
|
||||
_plugins.save(descriptor);
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
private data class PluginConfig(
|
||||
val SOURCES_EMBEDDED: Map<String, String>,
|
||||
|
||||
@@ -27,7 +27,20 @@ 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.*
|
||||
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.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
@@ -35,10 +48,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);
|
||||
@@ -128,21 +141,21 @@ class StatePolycentric {
|
||||
_likeDislikeMap[ref.toByteArray().toBase64()] = LikeDislikeEntry(System.currentTimeMillis(), hasLiked, hasDisliked);
|
||||
}
|
||||
|
||||
fun hasDisliked(ref: Protocol.Reference): Boolean {
|
||||
fun hasDisliked(data: ByteArray): Boolean {
|
||||
if (!enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
||||
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||
return entry.hasDisliked;
|
||||
}
|
||||
|
||||
fun hasLiked(ref: Protocol.Reference): Boolean {
|
||||
fun hasLiked(data: ByteArray): Boolean {
|
||||
if (!enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
||||
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||
return entry.hasLiked;
|
||||
}
|
||||
|
||||
@@ -275,7 +288,8 @@ 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()
|
||||
eventPointer = se.toPointer(),
|
||||
parentReference = se.event.references.getOrNull(0)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -316,7 +330,78 @@ class StatePolycentric {
|
||||
return LikesDislikesReplies(likes, dislikes, replyCount)
|
||||
}
|
||||
|
||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference): IPager<IPlatformComment> {
|
||||
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> {
|
||||
if (!enabled) {
|
||||
return EmptyPager()
|
||||
}
|
||||
@@ -338,7 +423,8 @@ class StatePolycentric {
|
||||
Protocol.QueryReferencesRequestCountReferences.newBuilder()
|
||||
.setFromType(ContentType.POST.value)
|
||||
.build())
|
||||
.build()
|
||||
.build(),
|
||||
extraByteReferences = extraByteReferences
|
||||
);
|
||||
|
||||
val results = mapQueryReferences(contextUrl, response);
|
||||
@@ -407,7 +493,8 @@ class StatePolycentric {
|
||||
ContentType.AVATAR.value,
|
||||
ContentType.USERNAME.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||
).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 };
|
||||
@@ -439,7 +526,8 @@ 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()
|
||||
eventPointer = sev.toPointer(),
|
||||
parentReference = sev.event.references.getOrNull(0)
|
||||
);
|
||||
} catch (e: Throwable) {
|
||||
return@mapNotNull null;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringStorage
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class VideoToOpen(val url: String, val timeSeconds: Long);
|
||||
|
||||
class StateSaved {
|
||||
var videoToOpen: VideoToOpen? = null;
|
||||
|
||||
private val _videoToOpen = FragmentedStorage.get<StringStorage>("videoToOpen")
|
||||
|
||||
fun load() {
|
||||
val videoToOpenString = _videoToOpen.value;
|
||||
if (videoToOpenString.isNotEmpty()) {
|
||||
try {
|
||||
val v = Serializer.json.decodeFromString<VideoToOpen>(videoToOpenString);
|
||||
videoToOpen = v;
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to load video to open", e)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "loaded videoToOpen=$videoToOpen");
|
||||
}
|
||||
|
||||
fun setVideoToOpenNonBlocking(v: VideoToOpen? = null) {
|
||||
Logger.i(TAG, "set videoToOpen=$v");
|
||||
|
||||
videoToOpen = v;
|
||||
_videoToOpen.setAndSave(if (v != null) Serializer.json.encodeToString(v) else "");
|
||||
}
|
||||
|
||||
|
||||
fun setVideoToOpenBlocking(v: VideoToOpen? = null) {
|
||||
Logger.i(TAG, "set videoToOpen=$v");
|
||||
|
||||
videoToOpen = v;
|
||||
_videoToOpen.setAndSaveBlocking(if (v != null) Serializer.json.encodeToString(v) else "");
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "StateSaved"
|
||||
|
||||
val instance: StateSaved = StateSaved()
|
||||
}
|
||||
}
|
||||
@@ -51,10 +51,16 @@ class StateSubscriptions {
|
||||
|
||||
val global: CentralizedFeed = CentralizedFeed();
|
||||
val feeds: HashMap<String, CentralizedFeed> = hashMapOf();
|
||||
val onFeedProgress = Event3<String, Int, Int>();
|
||||
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)
|
||||
|
||||
@@ -2,15 +2,15 @@ package com.futo.platformplayer.states
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.copyToOutputStream
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
@@ -155,47 +155,45 @@ class StateUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
fun checkForUpdates(context: Context, showUpToDateToast: Boolean) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val latestVersion = downloadVersionCode(client);
|
||||
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val latestVersion = downloadVersionCode(client);
|
||||
|
||||
if (latestVersion != null) {
|
||||
val currentVersion = BuildConfig.VERSION_CODE;
|
||||
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
|
||||
if (latestVersion != null) {
|
||||
val currentVersion = BuildConfig.VERSION_CODE;
|
||||
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
|
||||
|
||||
if (latestVersion > currentVersion) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.toast(context, "Failed to show update dialog");
|
||||
Logger.w(TAG, "Error occurred in update dialog.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (showUpToDateToast) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Already on latest version");
|
||||
}
|
||||
if (latestVersion > currentVersion) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.toast(context, "Failed to show update dialog");
|
||||
Logger.w(TAG, "Error occurred in update dialog.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.w(TAG, "Failed to retrieve version from version URL.");
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to retrieve version");
|
||||
if (showUpToDateToast) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Already on latest version");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to check for updates.", e);
|
||||
} else {
|
||||
Logger.w(TAG, "Failed to retrieve version from version URL.");
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to check for updates");
|
||||
UIDialogs.toast(context, "Failed to retrieve version");
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to check for updates.", e);
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to check for updates");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadApkToFile(client: ManagedHttpClient, destinationFile: File, isCancelled: (() -> Boolean)? = null) {
|
||||
|
||||
+4
-4
@@ -13,6 +13,7 @@ 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
|
||||
@@ -55,7 +56,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
||||
val limit = clientTasks.key.getSubscriptionRateLimit();
|
||||
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
|
||||
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
||||
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +70,6 @@ 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), exs);
|
||||
return Result(DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id }), 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 = ex;
|
||||
taskEx = channelEx;
|
||||
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
|
||||
class ToastView : LinearLayout {
|
||||
private val root: LinearLayout;
|
||||
private val title: TextView;
|
||||
private val text: TextView;
|
||||
init {
|
||||
inflate(context, R.layout.toast, this);
|
||||
root = findViewById(R.id.root);
|
||||
title = findViewById(R.id.title);
|
||||
text = findViewById(R.id.text);
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
setToast(ToastView.Toast("", false))
|
||||
root.visibility = GONE;
|
||||
}
|
||||
|
||||
fun hide(animate: Boolean, onFinished: (()->Unit)? = null) {
|
||||
Logger.i("MainActivity", "Hiding toast");
|
||||
if(!animate) {
|
||||
root.visibility = GONE;
|
||||
alpha = 0f;
|
||||
onFinished?.invoke();
|
||||
}
|
||||
else {
|
||||
animate()
|
||||
.alpha(0f)
|
||||
.setDuration(700)
|
||||
.translationY(20.dp(context.resources).toFloat())
|
||||
.withEndAction { root.visibility = GONE; onFinished?.invoke(); }
|
||||
.start();
|
||||
}
|
||||
}
|
||||
fun show(animate: Boolean) {
|
||||
Logger.i("MainActivity", "Showing toast");
|
||||
if(!animate) {
|
||||
root.visibility = VISIBLE;
|
||||
alpha = 1f;
|
||||
}
|
||||
else {
|
||||
alpha = 0f;
|
||||
root.visibility = VISIBLE;
|
||||
translationY = 20.dp(context.resources).toFloat();
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.setDuration(700)
|
||||
.translationY(0f)
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun setToast(toast: Toast) {
|
||||
if(toast.title.isNullOrEmpty())
|
||||
title.isVisible = false;
|
||||
else {
|
||||
title.text = toast.title;
|
||||
title.isVisible = true;
|
||||
}
|
||||
text.text = toast.msg;
|
||||
if(toast.color != null)
|
||||
text.setTextColor(toast.color);
|
||||
else
|
||||
text.setTextColor(Color.WHITE);
|
||||
}
|
||||
fun setToastAnimated(toast: Toast) {
|
||||
hide(true) {
|
||||
setToast(toast);
|
||||
show(true);
|
||||
};
|
||||
}
|
||||
|
||||
class Toast(
|
||||
val msg: String,
|
||||
val long: Boolean,
|
||||
val color: Int? = null,
|
||||
val title: String? = null
|
||||
);
|
||||
}
|
||||
@@ -9,15 +9,21 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
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
|
||||
@@ -104,7 +110,8 @@ class CommentViewHolder : ViewHolder {
|
||||
|
||||
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
||||
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
|
||||
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
|
||||
_textAuthor.text = comment.author.name;
|
||||
|
||||
val date = comment.date;
|
||||
@@ -161,8 +168,8 @@ class CommentViewHolder : ViewHolder {
|
||||
_pillRatingLikesDislikes.visibility = View.VISIBLE;
|
||||
|
||||
if (comment is PolycentricPlatformComment) {
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference);
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference.toByteArray());
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference.toByteArray());
|
||||
_pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked);
|
||||
} else {
|
||||
_pillRatingLikesDislikes.setRating(comment.rating);
|
||||
|
||||
+10
-3
@@ -55,6 +55,7 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onDelete = Event1<IPlatformComment>();
|
||||
var onClick = Event1<IPlatformComment>();
|
||||
var comment: IPlatformComment? = null
|
||||
private set;
|
||||
|
||||
@@ -108,6 +109,11 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
onDelete.emit(c);
|
||||
}
|
||||
|
||||
_layoutComment.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onClick.emit(c);
|
||||
}
|
||||
|
||||
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
|
||||
}
|
||||
|
||||
@@ -126,7 +132,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
_taskGetLiveComment.cancel()
|
||||
|
||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
||||
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
|
||||
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
|
||||
_textAuthor.text = comment.author.name;
|
||||
|
||||
val date = comment.date;
|
||||
@@ -168,8 +175,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
if (likesDislikesReplies != null) {
|
||||
Log.i(TAG, "updateLikesDislikesReplies set")
|
||||
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(c.reference);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference);
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(c.reference.toByteArray());
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference.toByteArray());
|
||||
_pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
|
||||
|
||||
_buttonReplies.setLoading(false)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.futo.platformplayer.views.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
|
||||
class DisabledSourceAdapter : RecyclerView.Adapter<DisabledSourceViewHolder> {
|
||||
private val _sources: MutableList<IPlatformClient>;
|
||||
|
||||
var onClick = Event1<IPlatformClient>();
|
||||
var onAdd = Event1<IPlatformClient>();
|
||||
|
||||
constructor(sources: MutableList<IPlatformClient>) : super() {
|
||||
_sources = sources;
|
||||
}
|
||||
|
||||
override fun getItemCount() = _sources.size
|
||||
|
||||
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): DisabledSourceViewHolder {
|
||||
val holder = DisabledSourceViewHolder(viewGroup);
|
||||
holder.onAdd.subscribe {
|
||||
val source = holder.source;
|
||||
if (source != null) {
|
||||
onAdd.emit(source);
|
||||
}
|
||||
}
|
||||
holder.onClick.subscribe {
|
||||
val source = holder.source;
|
||||
if (source != null) {
|
||||
onClick.emit(source);
|
||||
}
|
||||
};
|
||||
return holder;
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: DisabledSourceViewHolder, position: Int) {
|
||||
viewHolder.bind(_sources[position])
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
package com.futo.platformplayer.views.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
|
||||
class DisabledSourceView : LinearLayout {
|
||||
private val _root: LinearLayout;
|
||||
@@ -38,7 +36,16 @@ class DisabledSourceView : LinearLayout {
|
||||
client.icon?.setImageView(_imageSource);
|
||||
|
||||
_textSource.text = client.name;
|
||||
_textSourceSubtitle.text = context.getString(R.string.tap_to_open);
|
||||
|
||||
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
||||
_textSourceSubtitle.text = context.getString(R.string.update_available_exclamation)
|
||||
_textSourceSubtitle.setTextColor(context.getColor(R.color.light_blue_400))
|
||||
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_regular)
|
||||
} else {
|
||||
_textSourceSubtitle.text = context.getString(R.string.tap_to_open)
|
||||
_textSourceSubtitle.setTextColor(context.getColor(R.color.gray_ac))
|
||||
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_extra_light)
|
||||
}
|
||||
|
||||
_buttonAdd.setOnClickListener { onAdd.emit(source) }
|
||||
_root.setOnClickListener { onClick.emit(); };
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.futo.platformplayer.views.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
|
||||
class DisabledSourceViewHolder : ViewHolder {
|
||||
private val _imageSource: ImageView;
|
||||
private val _textSource: TextView;
|
||||
private val _textSourceSubtitle: TextView;
|
||||
|
||||
private val _buttonAdd: LinearLayout;
|
||||
|
||||
var onClick = Event0();
|
||||
var onAdd = Event0();
|
||||
var source: IPlatformClient? = null
|
||||
private set
|
||||
|
||||
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_source_disabled, viewGroup, false)) {
|
||||
_imageSource = itemView.findViewById(R.id.image_source);
|
||||
_textSource = itemView.findViewById(R.id.text_source);
|
||||
_textSourceSubtitle = itemView.findViewById(R.id.text_source_subtitle);
|
||||
_buttonAdd = itemView.findViewById(R.id.button_add);
|
||||
|
||||
val root = itemView.findViewById<LinearLayout>(R.id.root);
|
||||
_buttonAdd.setOnClickListener { onAdd.emit() }
|
||||
root.setOnClickListener { onClick.emit(); };
|
||||
}
|
||||
|
||||
fun bind(client: IPlatformClient) {
|
||||
client.icon?.setImageView(_imageSource);
|
||||
|
||||
_textSource.text = client.name;
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
|
||||
source = client;
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -10,7 +10,9 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
|
||||
class EnabledSourceViewHolder : ViewHolder {
|
||||
private val _imageSource: ImageView;
|
||||
@@ -57,8 +59,18 @@ class EnabledSourceViewHolder : ViewHolder {
|
||||
fun bind(client: IPlatformClient) {
|
||||
client.icon?.setImageView(_imageSource);
|
||||
|
||||
_textSource.text = client.name;
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
|
||||
_textSource.text = client.name
|
||||
|
||||
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.update_available_exclamation)
|
||||
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.light_blue_400))
|
||||
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
|
||||
} else {
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open)
|
||||
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.gray_ac))
|
||||
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_extra_light)
|
||||
}
|
||||
|
||||
source = client
|
||||
}
|
||||
}
|
||||
@@ -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.*
|
||||
import com.futo.platformplayer.R
|
||||
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.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
|
||||
|
||||
@@ -149,7 +149,8 @@ open class PlaylistView : LinearLayout {
|
||||
_neopassAnimator?.cancel();
|
||||
_neopassAnimator = null;
|
||||
|
||||
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
|
||||
val firstClaim = claims?.ownedClaims?.firstOrNull();
|
||||
val harborAvailable = firstClaim != null
|
||||
if (harborAvailable) {
|
||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
@@ -160,7 +161,7 @@ open class PlaylistView : LinearLayout {
|
||||
_imageNeopassChannel?.visibility = View.GONE
|
||||
}
|
||||
|
||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
|
||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -6,21 +6,18 @@ 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.StateSubscriptions
|
||||
import com.futo.platformplayer.toHumanBytesSpeed
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanTimeIndicator
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
@@ -107,7 +104,7 @@ class SubscriptionViewHolder : ViewHolder {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.debug.Stopwatch
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
@@ -46,7 +47,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
val contentDetails = StatePlatform.instance.getContentDetails(video.url).await();
|
||||
stopwatch.logAndNext(TAG, "Retrieving video detail (IO thread)")
|
||||
return@TaskHandler Pair(viewHolder, contentDetails)
|
||||
}).success { previewContentDetails(it.first, it.second) }
|
||||
}).exception<Throwable> { Logger.e(TAG, "Failed to retrieve preview content.", it) }.success { previewContentDetails(it.first, it.second) }
|
||||
|
||||
constructor(context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
|
||||
initialPlay: Boolean = false, viewsToPrepend: ArrayList<View> = arrayListOf(),
|
||||
|
||||
+1
-1
@@ -334,7 +334,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
} else if (_imageChannel != null) {
|
||||
val dp_28 = 28.dp(context.resources);
|
||||
|
||||
+2
-5
@@ -1,6 +1,5 @@
|
||||
package com.futo.platformplayer.views.adapters.viewholders
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
@@ -8,12 +7,10 @@ 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
|
||||
@@ -76,7 +73,7 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
@@ -148,7 +145,7 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
||||
+11
-24
@@ -3,45 +3,31 @@ package com.futo.platformplayer.views.adapters.viewholders
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
||||
class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
|
||||
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_group_bar, _viewGroup, false)) {
|
||||
private var _group: SubscriptionGroup? = null;
|
||||
|
||||
private val _root: FrameLayout;
|
||||
private val _image: ShapeableImageView;
|
||||
private val _textSubGroup: TextView;
|
||||
|
||||
|
||||
val onClick = Event1<SubscriptionGroup>();
|
||||
val onClickLong = Event1<SubscriptionGroup>();
|
||||
|
||||
init {
|
||||
_root = _view.findViewById(R.id.root);
|
||||
_image = _view.findViewById(R.id.image);
|
||||
_textSubGroup = _view.findViewById(R.id.text_sub_group);
|
||||
|
||||
val dp6 = 6.dp(_view.resources);
|
||||
_image.shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
|
||||
.build()
|
||||
|
||||
_view.setOnClickListener {
|
||||
_group?.let {
|
||||
onClick.emit(it);
|
||||
@@ -58,9 +44,9 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
override fun bind(value: SubscriptionGroup) {
|
||||
_group = value;
|
||||
val img = value.image;
|
||||
if(img != null)
|
||||
if(img != null) {
|
||||
img.setImageView(_image)
|
||||
else {
|
||||
} else {
|
||||
_image.setImageResource(0);
|
||||
|
||||
if(value is SubscriptionGroup.Add)
|
||||
@@ -68,10 +54,11 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
}
|
||||
_textSubGroup.text = value.name;
|
||||
|
||||
if(value is SubscriptionGroup.Selectable && value.selected)
|
||||
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
|
||||
else
|
||||
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
|
||||
if (value is SubscriptionGroup.Selectable && value.selected) {
|
||||
_root.setBackgroundResource(R.drawable.background_primary_round_6dp)
|
||||
} else {
|
||||
_root.background = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -3,12 +3,16 @@ package com.futo.platformplayer.views.behavior
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.media.AudioManager
|
||||
import android.util.AttributeSet
|
||||
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
|
||||
@@ -18,12 +22,23 @@ 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.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.*
|
||||
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
|
||||
|
||||
|
||||
class GestureControlView : LinearLayout {
|
||||
private val _scope = CoroutineScope(Dispatchers.Main);
|
||||
@@ -51,6 +66,7 @@ class GestureControlView : LinearLayout {
|
||||
private val _progressSound: CircularProgressBar;
|
||||
private var _animatorSound: ObjectAnimator? = null;
|
||||
private var _brightnessFactor = 1.0f;
|
||||
private var _originalBrightnessFactor = 1.0f;
|
||||
private var _adjustingBrightness: Boolean = false;
|
||||
private val _layoutControlsBrightness: FrameLayout;
|
||||
private val _progressBrightness: CircularProgressBar;
|
||||
@@ -62,10 +78,27 @@ 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();
|
||||
|
||||
@@ -83,8 +116,48 @@ 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; }
|
||||
@@ -95,40 +168,50 @@ class GestureControlView : LinearLayout {
|
||||
if(p0 == null)
|
||||
return false;
|
||||
|
||||
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();
|
||||
Logger.i(TAG, "p0.pointerCount: " + p0.pointerCount)
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (_isZoomPanEnabled && _isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) {
|
||||
_isPanning = true
|
||||
stopAllGestures()
|
||||
updateSnappingVisibility()
|
||||
pan(_translationX - distanceX, _translationY - distanceY)
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -169,6 +252,55 @@ 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;
|
||||
@@ -214,12 +346,67 @@ 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.04f;
|
||||
private val _snapZoomTolerance = 0.04f;
|
||||
|
||||
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;
|
||||
@@ -549,11 +736,39 @@ class GestureControlView : LinearLayout {
|
||||
}
|
||||
|
||||
fun setFullscreen(isFullScreen: Boolean) {
|
||||
resetZoomPan()
|
||||
|
||||
if (isFullScreen) {
|
||||
val c = context
|
||||
if (c is Activity && Settings.instance.gestureControls.useSystemBrightness) {
|
||||
_brightnessFactor = c.window.attributes.screenBrightness
|
||||
if (_brightnessFactor == -1.0f) {
|
||||
_brightnessFactor = android.provider.Settings.System.getInt(
|
||||
context.contentResolver,
|
||||
android.provider.Settings.System.SCREEN_BRIGHTNESS
|
||||
) / 255.0f;
|
||||
}
|
||||
_originalBrightnessFactor = _brightnessFactor
|
||||
}
|
||||
|
||||
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 {
|
||||
onBrightnessAdjusted.emit(1.0f);
|
||||
val c = context
|
||||
if (c is Activity && Settings.instance.gestureControls.useSystemBrightness) {
|
||||
if (Settings.instance.gestureControls.restoreSystemBrightness) {
|
||||
onBrightnessAdjusted.emit(_originalBrightnessFactor);
|
||||
}
|
||||
} else {
|
||||
onBrightnessAdjusted.emit(1.0f);
|
||||
}
|
||||
//onSoundAdjusted.emit(1.0f);
|
||||
stopAdjustingBrightness();
|
||||
stopAdjustingSound();
|
||||
|
||||
@@ -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.toHumanTime(false);
|
||||
_textDuration.text = video.duration.toHumanTime(false);
|
||||
_textPosition.text = (position * 1000).formatDuration();
|
||||
_textDuration.text = (video.duration * 1000).formatDuration();
|
||||
_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.toHumanTime(true);
|
||||
_textPosition.text = ms.formatDuration();
|
||||
_timeBar.setPosition(ms / 1000);
|
||||
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms);
|
||||
}
|
||||
|
||||
@@ -12,12 +12,16 @@ 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;
|
||||
|
||||
@@ -28,19 +32,23 @@ 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);
|
||||
setHarborAvailable(false, animate = false, system = null);
|
||||
setNewActivity(false);
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_imageChannelThumbnail.visibility = View.GONE;
|
||||
_imageChannelThumbnail.setImageResource(R.drawable.placeholder_channel_thumbnail);
|
||||
setHarborAvailable(false, animate = false);
|
||||
setHarborAvailable(false, animate = false, system = null);
|
||||
setNewActivity(false);
|
||||
}
|
||||
|
||||
@@ -50,13 +58,24 @@ class CreatorThumbnail : ConstraintLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
_imageChannelThumbnail.visibility = View.VISIBLE;
|
||||
|
||||
_harborAnimator?.cancel();
|
||||
_harborAnimator = null;
|
||||
|
||||
_imageAnimator?.cancel();
|
||||
_imageAnimator = null;
|
||||
|
||||
setHarborAvailable(url.startsWith("polycentric://"), animate);
|
||||
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);
|
||||
}
|
||||
|
||||
if (animate) {
|
||||
Glide.with(_imageChannelThumbnail)
|
||||
@@ -72,7 +91,7 @@ class CreatorThumbnail : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun setHarborAvailable(available: Boolean, animate: Boolean) {
|
||||
fun setHarborAvailable(available: Boolean, animate: Boolean, system: Protocol.PublicKey?) {
|
||||
_harborAnimator?.cancel();
|
||||
_harborAnimator = null;
|
||||
|
||||
@@ -85,6 +104,13 @@ 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,55 +1,32 @@
|
||||
package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.shapes.Shape
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import android.widget.FrameLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.PresetImages
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
||||
import com.futo.platformplayer.views.SearchView
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SelectableCreatorBarViewHolder
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import java.io.File
|
||||
|
||||
class CreatorSelectOverlay: ConstraintLayout {
|
||||
private val _buttonSelect: Button;
|
||||
private val _buttonSelect: FrameLayout;
|
||||
private val _topbar: OverlayTopbar;
|
||||
|
||||
private val _searchBar: SearchView;
|
||||
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();
|
||||
|
||||
@@ -66,7 +43,7 @@ class CreatorSelectOverlay: ConstraintLayout {
|
||||
else
|
||||
_creators.addAll(subs
|
||||
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
|
||||
_recyclerCreators.notifyContentChanged();
|
||||
filterCreators();
|
||||
}
|
||||
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { }
|
||||
init {
|
||||
@@ -74,7 +51,8 @@ class CreatorSelectOverlay: ConstraintLayout {
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_buttonSelect = findViewById(R.id.button_select);
|
||||
val dp6 = 6.dp(resources);
|
||||
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creators, RecyclerView.HORIZONTAL) { creatorView ->
|
||||
_searchBar = findViewById(R.id.search_bar);
|
||||
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creatorsFiltered, RecyclerView.HORIZONTAL) { creatorView ->
|
||||
creatorView.itemView.setPadding(0, dp6, 0, dp6);
|
||||
creatorView.onClick.subscribe {
|
||||
if(it.channel.thumbnail == null) {
|
||||
@@ -92,19 +70,33 @@ class CreatorSelectOverlay: ConstraintLayout {
|
||||
this.orientation = LinearLayoutManager.VERTICAL;
|
||||
};
|
||||
_buttonSelect.setOnClickListener {
|
||||
_selected?.let {
|
||||
if (_selected.isNotEmpty()) {
|
||||
select();
|
||||
}
|
||||
};
|
||||
_topbar.onClose.subscribe {
|
||||
onClose.emit();
|
||||
}
|
||||
_searchBar.onSearchChanged.subscribe {
|
||||
filterCreators();
|
||||
};
|
||||
updateSelected();
|
||||
filterCreators();
|
||||
}
|
||||
|
||||
fun updateSelected() {
|
||||
_creators.forEach { p -> p.active = _selected.contains(p.channel.url) };
|
||||
_recyclerCreators.notifyContentChanged();
|
||||
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);
|
||||
}
|
||||
|
||||
if(_selected.isNotEmpty())
|
||||
_buttonSelect.alpha = 1f;
|
||||
@@ -113,6 +105,17 @@ 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;
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.net.toFile
|
||||
@@ -48,7 +49,7 @@ class ImageVariableOverlay: ConstraintLayout {
|
||||
private val _buttonGallery: BigButton;
|
||||
private val _imageGallerySelected: ImageView;
|
||||
private val _imageGallerySelectedContainer: LinearLayout;
|
||||
private val _buttonSelect: Button;
|
||||
private val _buttonSelect: TextView;
|
||||
private val _topbar: OverlayTopbar;
|
||||
private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>;
|
||||
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.views.overlays
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
@@ -16,6 +17,21 @@ class LoaderOverlay(context: Context, attrs: AttributeSet?) : FrameLayout(contex
|
||||
inflate(context, R.layout.overlay_loader, this);
|
||||
_container = findViewById(R.id.container);
|
||||
_loader = findViewById(R.id.loader);
|
||||
|
||||
val centerLoader: Boolean;
|
||||
if (attrs != null) {
|
||||
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.LoaderOverlay, 0, 0);
|
||||
centerLoader = attrArr.getBoolean(R.styleable.LoaderOverlay_centerLoader, false);
|
||||
attrArr.recycle();
|
||||
} else {
|
||||
centerLoader = false;
|
||||
}
|
||||
|
||||
if (centerLoader) {
|
||||
(_loader.layoutParams as LayoutParams).apply {
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun show() {
|
||||
|
||||
@@ -3,10 +3,13 @@ package com.futo.platformplayer.views.overlays
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.marginRight
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||
|
||||
class OverlayTopbar : ConstraintLayout {
|
||||
@@ -16,6 +19,8 @@ class OverlayTopbar : ConstraintLayout {
|
||||
|
||||
private val _button_close: ImageView;
|
||||
|
||||
private val _button_list: LinearLayout;
|
||||
|
||||
val onClose = Event0();
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
@@ -24,6 +29,7 @@ class OverlayTopbar : ConstraintLayout {
|
||||
_name = findViewById(R.id.text_name);
|
||||
_meta = findViewById(R.id.text_meta);
|
||||
_button_close = findViewById(R.id.button_close);
|
||||
_button_list = findViewById(R.id.button_list);
|
||||
|
||||
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.OverlayTopbar, 0, 0);
|
||||
val attrText = attrArr.getText(R.styleable.OverlayTopbar_title) ?: "";
|
||||
@@ -42,4 +48,20 @@ class OverlayTopbar : ConstraintLayout {
|
||||
_name.text = name;
|
||||
_meta.text = meta;
|
||||
}
|
||||
|
||||
fun setButtons(vararg buttons: Pair<Int, ()->Unit>) {
|
||||
_button_list.removeAllViews();
|
||||
val dp40 = 40.dp(resources);
|
||||
val dp5 = 5.dp(resources);
|
||||
for(button in buttons) {
|
||||
_button_list.addView(ImageView(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(dp40, dp40)
|
||||
setPadding(dp5, dp5, dp5 * 2, dp5);
|
||||
setImageResource(button.first);
|
||||
setOnClickListener {
|
||||
button.second();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
@@ -20,6 +25,13 @@ import com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
import com.futo.platformplayer.views.comments.AddCommentView
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.segments.CommentsList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import userpackage.Protocol
|
||||
|
||||
class RepliesOverlay : LinearLayout {
|
||||
@@ -34,7 +46,11 @@ class RepliesOverlay : LinearLayout {
|
||||
private val _creatorThumbnail: CreatorThumbnail;
|
||||
private val _layoutParentComment: ConstraintLayout;
|
||||
private var _readonly = false;
|
||||
private var _loading = true;
|
||||
private var _parentComment: IPlatformComment? = null;
|
||||
private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null;
|
||||
private val _loaderOverlay: LoaderOverlay
|
||||
private val _client = ManagedHttpClient()
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.overlay_replies, this)
|
||||
@@ -46,6 +62,8 @@ class RepliesOverlay : LinearLayout {
|
||||
_textAuthor = findViewById(R.id.text_author)
|
||||
_creatorThumbnail = findViewById(R.id.image_thumbnail)
|
||||
_layoutParentComment = findViewById(R.id.layout_parent_comment)
|
||||
_loaderOverlay = findViewById(R.id.loader_overlay)
|
||||
setLoading(false);
|
||||
|
||||
_addCommentView.onCommentAdded.subscribe {
|
||||
_commentsList.addComment(it);
|
||||
@@ -72,11 +90,21 @@ class RepliesOverlay : LinearLayout {
|
||||
}
|
||||
};
|
||||
|
||||
_layoutParentComment.setOnClickListener {
|
||||
val p = _parentComment
|
||||
if (p !is PolycentricPlatformComment) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
val ref = p.parentReference ?: return@setOnClickListener
|
||||
handleParentClick(p.contextUrl, ref)
|
||||
}
|
||||
|
||||
_topbar.onClose.subscribe(this, onClose::emit);
|
||||
_topbar.setInfo(context.getString(R.string.Replies), "");
|
||||
}
|
||||
|
||||
fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) {
|
||||
fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null, onParentClick: ((comment: IPlatformComment) -> Unit)? = null) {
|
||||
_readonly = readonly;
|
||||
if (readonly) {
|
||||
_addCommentView.visibility = View.GONE;
|
||||
@@ -102,12 +130,143 @@ class RepliesOverlay : LinearLayout {
|
||||
}
|
||||
|
||||
_creatorThumbnail.setThumbnail(parentComment.author.thumbnail, false);
|
||||
_creatorThumbnail.setHarborAvailable(parentComment is PolycentricPlatformComment,false);
|
||||
val polycentricPlatformComment = if (parentComment is PolycentricPlatformComment) parentComment else null
|
||||
_creatorThumbnail.setHarborAvailable(polycentricPlatformComment != null,false, polycentricPlatformComment?.eventPointer?.system?.toProto());
|
||||
}
|
||||
|
||||
_topbar.setInfo(context.getString(R.string.Replies), metadata);
|
||||
_commentsList.load(readonly, loader);
|
||||
_onCommentAdded = onCommentAdded;
|
||||
_parentComment = parentComment;
|
||||
}
|
||||
|
||||
fun handleParentClick(contextUrl: String, ref: Protocol.Reference): Boolean {
|
||||
val ctx = context
|
||||
if (ctx !is MainActivity) {
|
||||
return false
|
||||
}
|
||||
|
||||
return when (ref.referenceType) {
|
||||
2L -> {
|
||||
setLoading(true)
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val parentComment = StatePolycentric.instance.getComment(contextUrl, ref)
|
||||
val replyCount = parentComment.replyCount ?: 0;
|
||||
var metadata = "";
|
||||
if (replyCount > 0) {
|
||||
metadata += "$replyCount " + context.getString(R.string.replies);
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
setLoading(false)
|
||||
|
||||
load(false, metadata, parentComment.contextUrl, parentComment.reference, parentComment,
|
||||
{ StatePolycentric.instance.getCommentPager(contextUrl, ref) })
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main) {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
Logger.e(TAG, "Failed to load parent comment.", e)
|
||||
UIDialogs.toast("Failed to load comment")
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
3L -> {
|
||||
StateApp.instance.scopeOrNull?.launch {
|
||||
try {
|
||||
val url = referenceToUrl(_client, ref) ?: return@launch
|
||||
withContext(Dispatchers.Main) {
|
||||
ctx.handleUrl(url)
|
||||
onClose.emit()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to open ref.", e)
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun referenceToUrl(client: ManagedHttpClient, parentRef: Protocol.Reference): String? {
|
||||
val refBytes = parentRef.reference?.toByteArray() ?: return null
|
||||
val ref = refBytes.decodeToString()
|
||||
|
||||
try {
|
||||
Uri.parse(ref)
|
||||
return ref
|
||||
} catch (e: Throwable) {
|
||||
try {
|
||||
return oldReferenceToUrl(client, ref)
|
||||
} catch (f: Throwable) {
|
||||
Logger.i(TAG, "Failed to handle URL.", f)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun oldReferenceToUrl(client: ManagedHttpClient, reference: String): String? {
|
||||
return when {
|
||||
reference.startsWith("video_episode:") -> {
|
||||
val response = client.get("https://content.api.nebula.app/video_episodes/$reference")
|
||||
if (!response.isOk) {
|
||||
throw Exception("Failed to resolve nebula video (${response.code}).")
|
||||
}
|
||||
|
||||
val respString = response.body?.string()
|
||||
val jsonElement = respString?.let { Json.parseToJsonElement(it) }
|
||||
return jsonElement?.jsonObject?.get("share_url")?.jsonPrimitive?.content
|
||||
}
|
||||
|
||||
reference.length == 11 -> "https://www.youtube.com/watch?v=$reference"
|
||||
|
||||
reference.length == 40 -> {
|
||||
val response = client.post("https://api.na-backend.odysee.com/api/v1/proxy?m=claim_search", hashMapOf(
|
||||
"Content-Type" to "application/json"
|
||||
))
|
||||
|
||||
if (!response.isOk) {
|
||||
throw Exception("Failed to resolve claim (${response.code}).")
|
||||
}
|
||||
|
||||
val jsonElement = response.body?.string()?.let { Json.parseToJsonElement(it) }
|
||||
val canonicalUrl = jsonElement?.jsonObject?.get("result")
|
||||
?.jsonObject?.get("items")
|
||||
?.jsonArray?.get(0)
|
||||
?.jsonObject?.get("canonical_url")
|
||||
?.jsonPrimitive?.content
|
||||
|
||||
canonicalUrl ?: throw Exception("Failed to get canonical URL.")
|
||||
}
|
||||
|
||||
reference.startsWith("v") && (reference.length == 7 || reference.length == 6) -> "https://rumble.com/$reference"
|
||||
|
||||
Regex("^\\d+\$").matches(reference) -> "https://www.twitch.tv/videos/$reference"
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(loading: Boolean) {
|
||||
if (_loading == loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
_loading = loading;
|
||||
if (!loading) {
|
||||
_loaderOverlay.hide()
|
||||
} else {
|
||||
_loaderOverlay.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
@@ -115,4 +274,8 @@ class RepliesOverlay : LinearLayout {
|
||||
_onCommentAdded = null;
|
||||
_commentsList.cancel();
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "RepliesOverlay"
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class WidePillButton : LinearLayout {
|
||||
private val _iconPrefix: ImageView
|
||||
private val _iconSuffix: ImageView
|
||||
private val _text: TextView
|
||||
private val _textDescription: TextView
|
||||
val onClick = Event0()
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
@@ -21,11 +22,13 @@ class WidePillButton : LinearLayout {
|
||||
_iconPrefix = findViewById(R.id.image_prefix)
|
||||
_iconSuffix = findViewById(R.id.image_suffix)
|
||||
_text = findViewById(R.id.text)
|
||||
_textDescription = findViewById(R.id.text_description)
|
||||
|
||||
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.WidePillButton, 0, 0)
|
||||
setIconPrefix(attrArr.getResourceId(R.styleable.WidePillButton_widePillIconPrefix, -1))
|
||||
setIconSuffix(attrArr.getResourceId(R.styleable.WidePillButton_widePillIconSuffix, -1))
|
||||
setText(attrArr.getText(R.styleable.PillButton_pillText) ?: "")
|
||||
setText(attrArr.getText(R.styleable.WidePillButton_widePillText) ?: "")
|
||||
setDescription(attrArr.getText(R.styleable.WidePillButton_widePillDescription))
|
||||
attrArr.recycle()
|
||||
|
||||
findViewById<LinearLayout>(R.id.root).setOnClickListener {
|
||||
@@ -54,4 +57,13 @@ class WidePillButton : LinearLayout {
|
||||
fun setText(t: CharSequence) {
|
||||
_text.text = t
|
||||
}
|
||||
|
||||
fun setDescription(t: CharSequence?) {
|
||||
if (!t.isNullOrEmpty()) {
|
||||
_textDescription.visibility = View.VISIBLE
|
||||
_textDescription.text = t
|
||||
} else {
|
||||
_textDescription.visibility= View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.KeyCharacterMap.UnavailableException
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
@@ -12,10 +11,8 @@ import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
@@ -25,6 +22,8 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
@@ -87,8 +86,6 @@ class CommentsList : ConstraintLayout {
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onCommentsLoaded = Event1<Int>();
|
||||
|
||||
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class SourceHeaderView : LinearLayout {
|
||||
private val _sourcePlatformUrl: TextView;
|
||||
private val _sourceRepositoryUrl: TextView;
|
||||
private val _sourceScriptUrl: TextView;
|
||||
private val _sourceScriptConfig: TextView;
|
||||
private val _sourceSignature: TextView;
|
||||
|
||||
private val _sourcePlatformUrlContainer: LinearLayout;
|
||||
@@ -45,6 +46,7 @@ class SourceHeaderView : LinearLayout {
|
||||
_sourcePlatformUrl = findViewById(R.id.source_platform);
|
||||
_sourcePlatformUrlContainer = findViewById(R.id.source_platform_container);
|
||||
_sourceScriptUrl = findViewById(R.id.source_script);
|
||||
_sourceScriptConfig = findViewById(R.id.source_config);
|
||||
_sourceSignature = findViewById(R.id.source_signature);
|
||||
|
||||
_sourceBy.setOnClickListener {
|
||||
@@ -59,6 +61,10 @@ class SourceHeaderView : LinearLayout {
|
||||
if(!_config?.absoluteScriptUrl.isNullOrEmpty())
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.absoluteScriptUrl)));
|
||||
};
|
||||
_sourceScriptConfig.setOnClickListener {
|
||||
if(!_config?.sourceUrl.isNullOrEmpty())
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.sourceUrl)));
|
||||
}
|
||||
_sourcePlatformUrl.setOnClickListener {
|
||||
if(!_config?.platformUrl.isNullOrEmpty())
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.platformUrl)));
|
||||
@@ -82,6 +88,7 @@ class SourceHeaderView : LinearLayout {
|
||||
_sourceVersion.text = config.version.toString();
|
||||
_sourceScriptUrl.text = config.absoluteScriptUrl;
|
||||
_sourceRepositoryUrl.text = config.repositoryUrl;
|
||||
_sourceScriptConfig.text = config.sourceUrl
|
||||
_sourceAuthorID.text = "";
|
||||
|
||||
_sourcePlatformUrl.text = config.platformUrl ?: "";
|
||||
|
||||
@@ -2,12 +2,14 @@ package com.futo.platformplayer.views.subscriptions
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
@@ -24,7 +26,8 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class SubscriptionBar : LinearLayout {
|
||||
private var _adapterView: AnyAdapterView<Subscription, SubscriptionBarViewHolder>? = null;
|
||||
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>
|
||||
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>;
|
||||
private var _subGroupsExplore: SubscriptionExploreButton;
|
||||
private val _tagsContainer: LinearLayout;
|
||||
|
||||
private val _groups: ArrayList<SubscriptionGroup>;
|
||||
@@ -64,7 +67,32 @@ class SubscriptionBar : LinearLayout {
|
||||
onHoldGroup.emit(g);
|
||||
}
|
||||
}
|
||||
_subGroupsExplore = findViewById(R.id.subgroup_explore);
|
||||
_tagsContainer = findViewById(R.id.container_tags);
|
||||
|
||||
_subGroupsExplore.onClick.subscribe {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_subscriptions, "Subscription Groups",
|
||||
"Subscription groups are an easy way to navigate your subscriptions.\n\nDefine your own subsets, and in the near future share them with others.", null, 0,
|
||||
UIDialogs.Action("Hide Bar", {
|
||||
Settings.instance.subscriptions.showSubscriptionGroups = false;
|
||||
Settings.instance.save();
|
||||
reloadGroups();
|
||||
|
||||
UIDialogs.showDialogOk(context, R.drawable.ic_quiz, "Subscription groups can be re-enabled in settings")
|
||||
}),
|
||||
UIDialogs.Action("Create", {
|
||||
onToggleGroup.emit(SubscriptionGroup.Add()); //Shortcut..
|
||||
}, UIDialogs.ActionStyle.PRIMARY))
|
||||
};
|
||||
|
||||
updateExplore();
|
||||
}
|
||||
|
||||
fun selectGroup(group: SubscriptionGroup) {
|
||||
val relevantGroup = _groups.find { it.id == group.id };
|
||||
if(relevantGroup != null && _group != relevantGroup) {
|
||||
groupClicked(relevantGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private fun groupClicked(g: SubscriptionGroup) {
|
||||
@@ -100,6 +128,8 @@ class SubscriptionBar : LinearLayout {
|
||||
_groups.clear();
|
||||
_groups.addAll(results);
|
||||
_subGroups.notifyContentChanged();
|
||||
|
||||
updateExplore();
|
||||
}
|
||||
private fun getGroups(): List<SubscriptionGroup> {
|
||||
return if(Settings.instance.subscriptions.showSubscriptionGroups)
|
||||
@@ -110,6 +140,18 @@ class SubscriptionBar : LinearLayout {
|
||||
else listOf();
|
||||
}
|
||||
|
||||
fun updateExplore() {
|
||||
val show = Settings.instance.subscriptions.showSubscriptionGroups &&
|
||||
_groups.all { it is SubscriptionGroup.Add };
|
||||
if(show) {
|
||||
_subGroupsExplore.visibility = View.VISIBLE;
|
||||
_subGroups.view.visibility = View.GONE;
|
||||
}
|
||||
else {
|
||||
_subGroupsExplore.visibility = View.GONE;
|
||||
_subGroups.view.visibility = View.VISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
fun setToggles(vararg buttons: Toggle) {
|
||||
_tagsContainer.removeAllViews();
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.futo.platformplayer.views.subscriptions
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.futo.platformplayer.*
|
||||
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.constructs.TaskHandler
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
||||
class SubscriptionExploreButton : ConstraintLayout {
|
||||
val onClick = Event0();
|
||||
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_subscription_group_bar_explore, this);
|
||||
|
||||
val dp10 = 10.dp(resources);
|
||||
findViewById<ShapeableImageView>(R.id.image)
|
||||
.apply {
|
||||
adjustViewBounds = true
|
||||
scaleType = ImageView.ScaleType.CENTER_CROP;
|
||||
shapeAppearanceModel = ShapeAppearanceModel.builder().setAllCorners(CornerFamily.ROUNDED, dp10.toFloat()).build()
|
||||
}
|
||||
|
||||
findViewById<ConstraintLayout>(R.id.root).setOnClickListener {
|
||||
onClick.emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -11,6 +14,9 @@ import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.PlayerControlView
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
@@ -39,6 +45,14 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
|
||||
|
||||
//Events
|
||||
private val _evMuteChanged = mutableListOf<(FutoThumbnailPlayer, Boolean)->Unit>();
|
||||
private val _loadArtwork = object: CustomTarget<Bitmap>() {
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
setArtwork(BitmapDrawable(resources, resource));
|
||||
}
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
setArtwork(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@@ -113,11 +127,38 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
|
||||
fun setPreview(video: IPlatformVideoDetails) {
|
||||
val videoSource = VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
|
||||
val audioSource = VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
setSource(videoSource, audioSource,true, false);
|
||||
if (video.live != null) {
|
||||
setSource(video.live, null,true, false);
|
||||
} else {
|
||||
val videoSource = VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
|
||||
val audioSource = VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
if (videoSource == null && audioSource != null) {
|
||||
val thumbnail = video.thumbnails.getHQThumbnail();
|
||||
if (!thumbnail.isNullOrBlank()) {
|
||||
Glide.with(videoView).asBitmap().load(thumbnail).into(_loadArtwork);
|
||||
} else {
|
||||
Glide.with(videoView).clear(_loadArtwork);
|
||||
setArtwork(null);
|
||||
}
|
||||
} else {
|
||||
Glide.with(videoView).clear(_loadArtwork);
|
||||
}
|
||||
|
||||
setSource(videoSource, audioSource,true, false);
|
||||
}
|
||||
}
|
||||
override fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun setArtwork(drawable: Drawable?) {
|
||||
if (drawable != null) {
|
||||
videoView.defaultArtwork = drawable;
|
||||
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL;
|
||||
} else {
|
||||
videoView.defaultArtwork = null;
|
||||
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioManager
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
@@ -235,16 +238,41 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
gestureControl.setupTouchArea(_layoutControls, background);
|
||||
gestureControl.onSeek.subscribe { seekFromCurrent(it); };
|
||||
gestureControl.onSoundAdjusted.subscribe { setVolume(it) };
|
||||
gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) };
|
||||
gestureControl.onBrightnessAdjusted.subscribe {
|
||||
if (it == 1.0f) {
|
||||
_overlay_brightness.visibility = View.GONE;
|
||||
gestureControl.onSoundAdjusted.subscribe {
|
||||
if (Settings.instance.gestureControls.useSystemVolume) {
|
||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (it * maxVolume).toInt(), 0)
|
||||
} else {
|
||||
_overlay_brightness.visibility = View.VISIBLE;
|
||||
_overlay_brightness.setBackgroundColor(Color.valueOf(0.0f, 0.0f, 0.0f, (1.0f - it)).toArgb());
|
||||
setVolume(it)
|
||||
}
|
||||
};
|
||||
gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) };
|
||||
gestureControl.onBrightnessAdjusted.subscribe {
|
||||
if (context is Activity && Settings.instance.gestureControls.useSystemBrightness) {
|
||||
val window = context.window
|
||||
val layout: WindowManager.LayoutParams = window.attributes
|
||||
layout.screenBrightness = it
|
||||
window.attributes = layout
|
||||
} else {
|
||||
if (it == 1.0f) {
|
||||
_overlay_brightness.visibility = View.GONE;
|
||||
} else {
|
||||
_overlay_brightness.visibility = View.VISIBLE;
|
||||
_overlay_brightness.setBackgroundColor(Color.valueOf(0.0f, 0.0f, 0.0f, (1.0f - it)).toArgb());
|
||||
}
|
||||
}
|
||||
};
|
||||
gestureControl.onPan.subscribe { x, y ->
|
||||
_videoView.translationX = x
|
||||
_videoView.translationY = y
|
||||
}
|
||||
gestureControl.onZoom.subscribe {
|
||||
_videoView.scaleX = it
|
||||
_videoView.scaleY = it
|
||||
}
|
||||
|
||||
gestureControl.setZoomPanEnabled(_videoView.videoSurfaceView!!)
|
||||
|
||||
if(!isInEditMode) {
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
@@ -531,7 +559,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
|
||||
_videoControls_fullscreen.show();
|
||||
videoControls.hide();
|
||||
videoControls.hideImmediately();
|
||||
}
|
||||
else {
|
||||
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
|
||||
@@ -543,7 +571,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
|
||||
videoControls.show();
|
||||
_videoControls_fullscreen.hide();
|
||||
_videoControls_fullscreen.hideImmediately();
|
||||
}
|
||||
|
||||
fitOrFill(fullScreen);
|
||||
@@ -574,6 +602,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
gestureControl.resetZoomPan()
|
||||
_lastSourceFit = null;
|
||||
if(isFullScreen)
|
||||
fillHeight();
|
||||
@@ -603,6 +632,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(playing: Boolean) {
|
||||
super.onIsPlayingChanged(playing)
|
||||
updatePlayPause();
|
||||
}
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
Logger.v(TAG, "onPlaybackStateChanged $playbackState");
|
||||
updatePlayPause()
|
||||
@@ -658,7 +691,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
val viewWidth = Math.min(metrics.widthPixels, metrics.heightPixels); //TODO: Get parent width. was this.width
|
||||
val deviceHeight = Math.max(metrics.widthPixels, metrics.heightPixels);
|
||||
val maxHeight = deviceHeight * 0.6;
|
||||
val maxHeight = deviceHeight * 0.4;
|
||||
|
||||
val determinedHeight = if(w > h)
|
||||
((h * (viewWidth.toDouble() / w)).toInt())
|
||||
@@ -730,8 +763,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun setGestureSoundFactor(soundFactor: Float) {
|
||||
gestureControl.setSoundFactor(soundFactor);
|
||||
}
|
||||
|
||||
override fun onSurfaceSizeChanged(width: Int, height: Int) {
|
||||
gestureControl.resetZoomPan()
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,21 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
private var _toResume = false;
|
||||
|
||||
private val _playerEventListener = object: Player.Listener {
|
||||
override fun onPlaybackSuppressionReasonChanged(playbackSuppressionReason: Int) {
|
||||
super.onPlaybackSuppressionReasonChanged(playbackSuppressionReason)
|
||||
}
|
||||
|
||||
override fun onSurfaceSizeChanged(width: Int, height: Int) {
|
||||
super.onSurfaceSizeChanged(width, height)
|
||||
this@FutoVideoPlayerBase.onSurfaceSizeChanged(width, height);
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
super.onIsPlayingChanged(isPlaying);
|
||||
this@FutoVideoPlayerBase.onIsPlayingChanged(isPlaying);
|
||||
updatePlaying();
|
||||
}
|
||||
|
||||
//TODO: Figure out why this is deprecated, and what the alternative is.
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
@@ -582,6 +597,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
exoPlayer?.setVolume(volume);
|
||||
}
|
||||
|
||||
protected open fun onSurfaceSizeChanged(width: Int, height: Int) {
|
||||
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
protected open fun onPlayerError(error: PlaybackException) {
|
||||
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
|
||||
@@ -616,6 +635,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
protected open fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource? = null, resume: Boolean = true) { }
|
||||
|
||||
protected open fun onIsPlayingChanged(playing: Boolean) {
|
||||
|
||||
}
|
||||
protected open fun onPlaybackStateChanged(playbackState: Int) {
|
||||
if (_shouldPlaybackRestartOnConnectivity && playbackState == ExoPlayer.STATE_READY) {
|
||||
Logger.i(TAG, "_shouldPlaybackRestartOnConnectivity=false");
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:radius="10dp"
|
||||
android:topRightRadius="10dp"
|
||||
android:bottomRightRadius="10dp"
|
||||
android:bottomLeftRadius="10dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#6F6F6F" />
|
||||
</shape>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:radius="10dp"
|
||||
android:topRightRadius="10dp"
|
||||
android:bottomRightRadius="10dp"
|
||||
android:bottomLeftRadius="10dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#6F6F6F" />
|
||||
<solid android:color="#99000000" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#99000000" />
|
||||
<corners android:radius="6dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#2A2A2A" />
|
||||
<corners android:radius="25dp" />
|
||||
<size android:height="20dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke android:color="#992D63ED" android:width="5dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user