Subscription persistence fixes, home toggle fixes, subs exchange gzip, etc

This commit is contained in:
Kelvin K 2025-04-07 23:31:00 +02:00
parent b14518edb1
commit 7b355139fb
8 changed files with 191 additions and 80 deletions

View File

@ -27,14 +27,18 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.others.PlatformLinkMovementMethod import com.futo.platformplayer.others.PlatformLinkMovementMethod
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.time.OffsetDateTime
import java.util.* import java.util.*
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "; private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String { fun getRandomString(sizeOfRandomString: Int): String {
@ -279,3 +283,34 @@ fun ByteBuffer.toUtf8String(): String {
get(remainingBytes) get(remainingBytes)
return String(remainingBytes, Charsets.UTF_8) return String(remainingBytes, Charsets.UTF_8)
} }
fun ByteArray.toGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val gzipTimeStart = OffsetDateTime.now();
val outputStream = ByteArrayOutputStream()
GZIPOutputStream(outputStream).use { gzip ->
gzip.write(this)
}
val result = outputStream.toByteArray();
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
return result;
}
fun ByteArray.fromGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val inputStream = ByteArrayInputStream(this)
val outputStream = ByteArrayOutputStream()
GZIPInputStream(inputStream).use { gzip ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (gzip.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
return outputStream.toByteArray()
}

View File

@ -250,39 +250,53 @@ class HomeFragment : MainFragment() {
layoutParams = layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
} }
fragment._togglePluginsDisabled.clear();
synchronized(_filterLock) { synchronized(_filterLock) {
val buttonsPlugins = (if (_togglesConfig.contains("plugins")) var buttonsPlugins: List<ToggleBar.Toggle> = listOf()
buttonsPlugins = (if (_togglesConfig.contains("plugins"))
(StatePlatform.instance.getEnabledClients() (StatePlatform.instance.getEnabledClients()
.filter { it is JSClient && it.enableInHome } .filter { it is JSClient && it.enableInHome }
.map { plugin -> .map { plugin ->
ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) plugin.name else "", plugin.icon, !fragment._togglePluginsDisabled.contains(plugin.id), { ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) plugin.name else "", plugin.icon, !fragment._togglePluginsDisabled.contains(plugin.id), { view, active ->
if (it) { var dontSwap = false;
if (active) {
if (fragment._togglePluginsDisabled.contains(plugin.id)) if (fragment._togglePluginsDisabled.contains(plugin.id))
fragment._togglePluginsDisabled.remove(plugin.id); fragment._togglePluginsDisabled.remove(plugin.id);
} else { } else {
if (!fragment._togglePluginsDisabled.contains(plugin.id)) if (!fragment._togglePluginsDisabled.contains(plugin.id)) {
fragment._togglePluginsDisabled.add(plugin.id); val enabledClients = StatePlatform.instance.getEnabledClients();
val availableAfterDisable = enabledClients.count { !fragment._togglePluginsDisabled.contains(it.id) && it.id != plugin.id };
if(availableAfterDisable > 0)
fragment._togglePluginsDisabled.add(plugin.id);
else {
UIDialogs.appToast("Home needs atleast 1 plugin active");
dontSwap = true;
}
}
}
if(!dontSwap)
reloadForFilters();
else {
view.setToggle(!active);
} }
reloadForFilters();
}).withTag("plugins") }).withTag("plugins")
}) })
else listOf()) else listOf())
val buttons = (listOf<ToggleBar.Toggle?>( val buttons = (listOf<ToggleBar.Toggle?>(
(if (_togglesConfig.contains("today")) (if (_togglesConfig.contains("today"))
ToggleBar.Toggle("Today", fragment._toggleRecent) { ToggleBar.Toggle("Today", fragment._toggleRecent) { view, active ->
fragment._toggleRecent = it; reloadForFilters() fragment._toggleRecent = active; reloadForFilters()
} }
.withTag("today") else null), .withTag("today") else null),
(if (_togglesConfig.contains("watched")) (if (_togglesConfig.contains("watched"))
ToggleBar.Toggle("Unwatched", fragment._toggleWatched) { ToggleBar.Toggle("Unwatched", fragment._toggleWatched) { view, active ->
fragment._toggleWatched = it; reloadForFilters() fragment._toggleWatched = active; reloadForFilters()
} }
.withTag("watched") else null), .withTag("watched") else null),
).filterNotNull() + buttonsPlugins) ).filterNotNull() + buttonsPlugins)
.sortedBy { _togglesConfig.indexOf(it.tag ?: "") } ?: listOf() .sortedBy { _togglesConfig.indexOf(it.tag ?: "") } ?: listOf()
val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, { val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, { view, active ->
showOrderOverlay(_overlayContainer, showOrderOverlay(_overlayContainer,
"Visible home filters", "Visible home filters",
listOf( listOf(

View File

@ -18,6 +18,7 @@ import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.exceptions.RateLimitException import com.futo.platformplayer.exceptions.RateLimitException
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment.SubscriptionsFeedView.FeedFilterSettings
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
@ -56,6 +57,9 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _group: SubscriptionGroup? = null; private var _group: SubscriptionGroup? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null; private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
private val _filterLock = Object();
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
override fun onShownWithView(parameter: Any?, isBack: Boolean) { override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack); super.onShownWithView(parameter, isBack);
_view?.onShown(); _view?.onShown();
@ -184,8 +188,6 @@ class SubscriptionsFeedFragment : MainFragment() {
return Json.encodeToString(this); return Json.encodeToString(this);
} }
} }
private val _filterLock = Object();
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
private var _bypassRateLimit = false; private var _bypassRateLimit = false;
private val _lastExceptions: List<Throwable>? = null; private val _lastExceptions: List<Throwable>? = null;
@ -284,13 +286,18 @@ class SubscriptionsFeedFragment : MainFragment() {
fragment.navigate<SubscriptionGroupFragment>(g); fragment.navigate<SubscriptionGroupFragment>(g);
}; };
synchronized(_filterLock) { synchronized(fragment._filterLock) {
_subscriptionBar?.setToggles( _subscriptionBar?.setToggles(
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); }, SubscriptionBar.Toggle(context.getString(R.string.videos), fragment._filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { view, active ->
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); }, toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), active); },
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); }, SubscriptionBar.Toggle(context.getString(R.string.posts), fragment._filterSettings.allowContentTypes.contains(ContentType.POST)) { view, active ->
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); }, toggleFilterContentType(ContentType.POST, active); },
SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); } SubscriptionBar.Toggle(context.getString(R.string.live), fragment._filterSettings.allowLive) { view, active ->
fragment._filterSettings.allowLive = active; fragment._filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.planned), fragment._filterSettings.allowPlanned) { view, active ->
fragment._filterSettings.allowPlanned = active; fragment._filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.watched), fragment._filterSettings.allowWatched) { view, active ->
fragment._filterSettings.allowWatched = active; fragment._filterSettings.save(); loadResults(false); }
); );
} }
@ -301,13 +308,13 @@ class SubscriptionsFeedFragment : MainFragment() {
toggleFilterContentType(contentType, isTrue); toggleFilterContentType(contentType, isTrue);
} }
private fun toggleFilterContentType(contentType: ContentType, isTrue: Boolean) { private fun toggleFilterContentType(contentType: ContentType, isTrue: Boolean) {
synchronized(_filterLock) { synchronized(fragment._filterLock) {
if(!isTrue) { if(!isTrue) {
_filterSettings.allowContentTypes.remove(contentType); fragment._filterSettings.allowContentTypes.remove(contentType);
} else if(!_filterSettings.allowContentTypes.contains(contentType)) { } else if(!fragment._filterSettings.allowContentTypes.contains(contentType)) {
_filterSettings.allowContentTypes.add(contentType) fragment._filterSettings.allowContentTypes.add(contentType)
} }
_filterSettings.save(); fragment._filterSettings.save();
}; };
if(Settings.instance.subscriptions.fetchOnTabOpen) { //TODO: Do this different, temporary workaround if(Settings.instance.subscriptions.fetchOnTabOpen) { //TODO: Do this different, temporary workaround
loadResults(false); loadResults(false);
@ -320,9 +327,9 @@ class SubscriptionsFeedFragment : MainFragment() {
val nowSoon = OffsetDateTime.now().plusMinutes(5); val nowSoon = OffsetDateTime.now().plusMinutes(5);
val filterGroup = subGroup; val filterGroup = subGroup;
return results.filter { return results.filter {
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType); val allowedContentType = fragment._filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration)) if(it is IPlatformVideo && it.duration > 0 && !fragment._filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
return@filter false; return@filter false;
//TODO: Check against a sub cache //TODO: Check against a sub cache
@ -331,11 +338,11 @@ class SubscriptionsFeedFragment : MainFragment() {
if(it.datetime?.isAfter(nowSoon) == true) { if(it.datetime?.isAfter(nowSoon) == true) {
if(!_filterSettings.allowPlanned) if(!fragment._filterSettings.allowPlanned)
return@filter false; return@filter false;
} }
if(_filterSettings.allowLive) { //If allowLive, always show live if(fragment._filterSettings.allowLive) { //If allowLive, always show live
if(it is IPlatformVideo && it.isLive) if(it is IPlatformVideo && it.isLive)
return@filter true; return@filter true;
} }

View File

@ -15,12 +15,14 @@ import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.debug.Stopwatch
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.findNonRuntimeException import com.futo.platformplayer.findNonRuntimeException
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
import com.futo.platformplayer.getNowDiffMiliseconds
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
@ -32,6 +34,8 @@ import com.futo.platformplayer.subsexchange.ChannelRequest
import com.futo.platformplayer.subsexchange.ChannelResolve import com.futo.platformplayer.subsexchange.ChannelResolve
import com.futo.platformplayer.subsexchange.ExchangeContract import com.futo.platformplayer.subsexchange.ExchangeContract
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
@ -149,42 +153,56 @@ abstract class SubscriptionsTaskFetchAlgorithm(
//Resolve Subscription Exchange //Resolve Subscription Exchange
if(contract != null) { if(contract != null) {
try { fun resolve() {
resolveTime = measureTimeMillis { try {
val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract!!.required.contains(it.task.url) }.map { resolveTime = measureTimeMillis {
ChannelResolve( val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract!!.required.contains(it.task.url) }.map {
it.task.url, ChannelResolve(
it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) } it.task.url,
) it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) }
}.toTypedArray() )
val resolve = subsExchangeClient?.resolveContract( }.toTypedArray()
contract!!,
*resolves val resolveRequestStart = OffsetDateTime.now();
);
if (resolve != null) { val resolve = subsExchangeClient?.resolveContract(
resolveCount = resolves.size; contract!!,
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}") *resolves
for(result in resolve){ );
val task = providedTasks?.find { it.url == result.channelUrl };
if(task != null) { Logger.i(TAG, "Subscription Exchange contract resolved request in ${resolveRequestStart.getNowDiffMiliseconds()}ms");
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
providedTasks?.remove(task); if (resolve != null) {
resolveCount = resolves.size;
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}")
for(result in resolve){
val task = providedTasks?.find { it.url == result.channelUrl };
if(task != null) {
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
providedTasks?.remove(task);
}
}
}
if (providedTasks != null) {
for(task in providedTasks!!) {
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
} }
} }
} }
if (providedTasks != null) { Logger.i(TAG, "Subscription Exchange contract resolved in ${resolveTime}ms");
for(task in providedTasks!!) {
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
}
}
}
Logger.i(TAG, "Subscription Exchange contract resolved in ${resolveTime}ms");
}
catch(ex: Throwable) {
//TODO: fetch remainder after all?
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex);
}
} }
catch(ex: Throwable) { if(providedTasks?.size ?: 0 == 0)
//TODO: fetch remainder after all? scope.launch(Dispatchers.IO) {
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex); resolve();
} }
else
resolve();
} }
} }

View File

@ -1,10 +1,14 @@
import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.getNowDiffMiliseconds
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm.Companion.TAG
import com.futo.platformplayer.subsexchange.ChannelRequest import com.futo.platformplayer.subsexchange.ChannelRequest
import com.futo.platformplayer.subsexchange.ChannelResolve import com.futo.platformplayer.subsexchange.ChannelResolve
import com.futo.platformplayer.subsexchange.ChannelResult import com.futo.platformplayer.subsexchange.ChannelResult
import com.futo.platformplayer.subsexchange.ExchangeContract import com.futo.platformplayer.subsexchange.ExchangeContract
import com.futo.platformplayer.subsexchange.ExchangeContractResolve import com.futo.platformplayer.subsexchange.ExchangeContractResolve
import com.futo.platformplayer.toGzip
import com.futo.platformplayer.toHumanBytesSize
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -26,6 +30,7 @@ import java.nio.charset.StandardCharsets
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.RSAPublicKeySpec import java.security.spec.RSAPublicKeySpec
import java.time.OffsetDateTime
class SubsExchangeClient(private val server: String, private val privateKey: String, private val contractTimeout: Int = 1000) { class SubsExchangeClient(private val server: String, private val privateKey: String, private val contractTimeout: Int = 1000) {
@ -40,24 +45,27 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
// Endpoint: Contract // Endpoint: Contract
fun requestContract(vararg channels: ChannelRequest): ExchangeContract { fun requestContract(vararg channels: ChannelRequest): ExchangeContract {
val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json", contractTimeout) val data = post("/api/Channel/Contract", Json.encodeToString(channels).toByteArray(Charsets.UTF_8), "application/json", contractTimeout)
return Json.decodeFromString(data) return Json.decodeFromString(data)
} }
suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract { suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract {
val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels), "application/json") val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels).toByteArray(Charsets.UTF_8), "application/json")
return Json.decodeFromString(data) return Json.decodeFromString(data)
} }
// Endpoint: Resolve // Endpoint: Resolve
fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> { fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
val contractResolve = convertResolves(*resolves) val contractResolve = convertResolves(*resolves)
val result = post("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json") val contractResolveJson = Serializer.json.encodeToString(contractResolve);
Logger.v("SubsExchangeClient", "Resolve:" + result); val contractResolveTimeStart = OffsetDateTime.now();
val result = post("/api/Channel/Resolve?contractId=${contract.id}", contractResolveJson.toByteArray(Charsets.UTF_8), "application/json", 0, true)
val contractResolveTime = contractResolveTimeStart.getNowDiffMiliseconds();
Logger.v("SubsExchangeClient", "Subscription Exchange Resolve Request [${contractResolveTime}ms]:" + result);
return Serializer.json.decodeFromString(result) return Serializer.json.decodeFromString(result)
} }
suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> { suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
val contractResolve = convertResolves(*resolves) val contractResolve = convertResolves(*resolves)
val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json") val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve).toByteArray(Charsets.UTF_8), "application/json", true)
return Serializer.json.decodeFromString(result) return Serializer.json.decodeFromString(result)
} }
@ -74,7 +82,7 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
} }
// IO methods // IO methods
private fun post(query: String, body: String, contentType: String, timeout: Int = 0): String { private fun post(query: String, body: ByteArray, contentType: String, timeout: Int = 0, gzip: Boolean = false): String {
val url = URL("${server.trim('/')}$query") val url = URL("${server.trim('/')}$query")
with(url.openConnection() as HttpURLConnection) { with(url.openConnection() as HttpURLConnection) {
if(timeout > 0) if(timeout > 0)
@ -82,7 +90,16 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
requestMethod = "POST" requestMethod = "POST"
setRequestProperty("Content-Type", contentType) setRequestProperty("Content-Type", contentType)
doOutput = true doOutput = true
OutputStreamWriter(outputStream, StandardCharsets.UTF_8).use { it.write(body); it.flush() }
if(gzip) {
val gzipData = body.toGzip();
setRequestProperty("Content-Encoding", "gzip");
outputStream.write(gzipData);
Logger.i("SubsExchangeClient", "SubsExchange using gzip (${body.size.toHumanBytesSize()} => ${gzipData.size.toHumanBytesSize()}");
}
else
outputStream.write(body);
val status = responseCode; val status = responseCode;
Logger.i("SubsExchangeClient", "POST [${url}]: ${status}"); Logger.i("SubsExchangeClient", "POST [${url}]: ${status}");
@ -105,9 +122,9 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
} }
} }
} }
private suspend fun postAsync(query: String, body: String, contentType: String): String { private suspend fun postAsync(query: String, body: ByteArray, contentType: String, gzip: Boolean = false): String {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
post(query, body, contentType) post(query, body, contentType, 0, gzip)
} }
} }

View File

@ -53,7 +53,7 @@ class ToggleBar : LinearLayout {
this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton); this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton);
else else
this.setInfo(button.name, button.isActive, button.isButton); this.setInfo(button.name, button.isActive, button.isButton);
this.onClick.subscribe { button.action(it); }; this.onClick.subscribe({ view, enabled -> button.action(view, enabled); });
}); });
} }
} }
@ -62,27 +62,27 @@ class ToggleBar : LinearLayout {
val name: String; val name: String;
val icon: Int; val icon: Int;
val iconVariable: ImageVariable?; val iconVariable: ImageVariable?;
val action: (Boolean)->Unit; val action: (ToggleTagView, Boolean)->Unit;
val isActive: Boolean; val isActive: Boolean;
var isButton: Boolean = false var isButton: Boolean = false
private set; private set;
var tag: String? = null; var tag: String? = null;
constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (Boolean)->Unit) { constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name; this.name = name;
this.icon = 0; this.icon = 0;
this.iconVariable = icon; this.iconVariable = icon;
this.action = action; this.action = action;
this.isActive = isActive; this.isActive = isActive;
} }
constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) { constructor(name: String, icon: Int, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name; this.name = name;
this.icon = icon; this.icon = icon;
this.iconVariable = null; this.iconVariable = null;
this.action = action; this.action = action;
this.isActive = isActive; this.isActive = isActive;
} }
constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) { constructor(name: String, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name; this.name = name;
this.icon = 0; this.icon = 0;
this.iconVariable = null; this.iconVariable = null;

View File

@ -12,8 +12,10 @@ import android.widget.TextView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.images.GlideHelper import com.futo.platformplayer.images.GlideHelper
import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.views.ToggleBar
class ToggleTagView : LinearLayout { class ToggleTagView : LinearLayout {
private val _root: FrameLayout; private val _root: FrameLayout;
@ -26,7 +28,7 @@ class ToggleTagView : LinearLayout {
var isButton: Boolean = false var isButton: Boolean = false
private set; private set;
var onClick = Event1<Boolean>(); var onClick = Event2<ToggleTagView, Boolean>();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true); LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true);
@ -36,7 +38,7 @@ class ToggleTagView : LinearLayout {
_root.setOnClickListener { _root.setOnClickListener {
if(!isButton) if(!isButton)
setToggle(!isActive); setToggle(!isActive);
onClick.emit(isActive); onClick.emit(this, isActive);
} }
} }
@ -52,6 +54,24 @@ class ToggleTagView : LinearLayout {
} }
} }
fun setInfo(toggle: ToggleBar.Toggle){
_text = toggle.name;
_textTag.text = toggle.name;
setToggle(toggle.isActive);
if(toggle.iconVariable != null) {
toggle.iconVariable.setImageView(_image, R.drawable.ic_error_pred);
_image.visibility = View.GONE;
}
else if(toggle.icon > 0) {
_image.setImageResource(toggle.icon);
_image.visibility = View.GONE;
}
else
_image.visibility = View.VISIBLE;
_textTag.visibility = if(!toggle.name.isNullOrEmpty()) View.VISIBLE else View.GONE;
this.isButton = isButton;
}
fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false) { fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false) {
_text = text; _text = text;
_textTag.text = text; _textTag.text = text;

View File

@ -158,7 +158,7 @@ class SubscriptionBar : LinearLayout {
for(button in buttons) { for(button in buttons) {
_tagsContainer.addView(ToggleTagView(context).apply { _tagsContainer.addView(ToggleTagView(context).apply {
this.setInfo(button.name, button.isActive); this.setInfo(button.name, button.isActive);
this.onClick.subscribe { button.action(it); }; this.onClick.subscribe({ view, value -> button.action(view, value); });
}); });
} }
} }
@ -166,16 +166,16 @@ class SubscriptionBar : LinearLayout {
class Toggle { class Toggle {
val name: String; val name: String;
val icon: Int; val icon: Int;
val action: (Boolean)->Unit; val action: (ToggleTagView, Boolean)->Unit;
val isActive: Boolean; val isActive: Boolean;
constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) { constructor(name: String, icon: Int, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name; this.name = name;
this.icon = icon; this.icon = icon;
this.action = action; this.action = action;
this.isActive = isActive; this.isActive = isActive;
} }
constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) { constructor(name: String, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name; this.name = name;
this.icon = 0; this.icon = 0;
this.action = action; this.action = action;