mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-19 14:32:34 +02:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 290d2ceb50 | |||
| 8ec9025990 | |||
| c4cf856dcd | |||
| 38bb4e25d3 | |||
| 0de996d91c | |||
| 1f38c9b27d | |||
| 234f31b02d | |||
| 00e40e8cd6 | |||
| 0bc6a43dc1 | |||
| e7e0157fbc | |||
| 4cae1a41a5 | |||
| 4fa61e7f52 | |||
| f02ac796f5 | |||
| 22146a6bdc | |||
| 5285eae01d | |||
| c47ca369e4 | |||
| f0b1f62bb1 | |||
| f7aa6d006e | |||
| 6b67cd549f | |||
| fc6bf85822 | |||
| fbd9345cf8 | |||
| 63137b4c4d | |||
| e28dc7a3a6 | |||
| 6e14acc685 | |||
| ba64153f1d | |||
| 72c04e7556 | |||
| 54f37ee5b2 | |||
| 4fbb325313 | |||
| e1d3b95f73 | |||
| 8f7b4b8257 | |||
| 9d906025ea |
+29
-13
@@ -1,13 +1,14 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
||||
import com.futo.platformplayer.encryption.GEncryptionProviderV0
|
||||
import com.futo.platformplayer.encryption.GEncryptionProviderV1
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class EncryptionProviderTests {
|
||||
class GEncryptionProviderTests {
|
||||
@Test
|
||||
fun testEncryptDecrypt() {
|
||||
val encryptionProvider = EncryptionProvider.instance
|
||||
fun testEncryptDecryptV1() {
|
||||
val encryptionProvider = GEncryptionProviderV1.instance
|
||||
val plaintext = "This is a test string."
|
||||
|
||||
// Encrypt the plaintext
|
||||
@@ -22,8 +23,8 @@ class EncryptionProviderTests {
|
||||
|
||||
|
||||
@Test
|
||||
fun testEncryptDecryptBytes() {
|
||||
val encryptionProvider = EncryptionProvider.instance
|
||||
fun testEncryptDecryptBytesV1() {
|
||||
val encryptionProvider = GEncryptionProviderV1.instance
|
||||
val bytes = "This is a test string.".toByteArray();
|
||||
|
||||
// Encrypt the plaintext
|
||||
@@ -36,21 +37,36 @@ class EncryptionProviderTests {
|
||||
assertArrayEquals(bytes, decrypted);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testEncryptDecryptBytesPassword() {
|
||||
val encryptionProvider = EncryptionProvider.instance
|
||||
val bytes = "This is a test string.".toByteArray();
|
||||
val password = "1234".padStart(32, '9');
|
||||
fun testEncryptDecryptV0() {
|
||||
val encryptionProvider = GEncryptionProviderV0.instance
|
||||
val plaintext = "This is a test string."
|
||||
|
||||
// Encrypt the plaintext
|
||||
val ciphertext = encryptionProvider.encrypt(bytes, password)
|
||||
val ciphertext = encryptionProvider.encrypt(plaintext)
|
||||
|
||||
// Decrypt the ciphertext
|
||||
val decrypted = encryptionProvider.decrypt(ciphertext, password)
|
||||
val decrypted = encryptionProvider.decrypt(ciphertext)
|
||||
|
||||
// The decrypted string should be equal to the original plaintext
|
||||
assertEquals(plaintext, decrypted)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testEncryptDecryptBytesV0() {
|
||||
val encryptionProvider = GEncryptionProviderV0.instance
|
||||
val bytes = "This is a test string.".toByteArray();
|
||||
|
||||
// Encrypt the plaintext
|
||||
val ciphertext = encryptionProvider.encrypt(bytes)
|
||||
|
||||
// Decrypt the ciphertext
|
||||
val decrypted = encryptionProvider.decrypt(ciphertext)
|
||||
|
||||
// The decrypted string should be equal to the original plaintext
|
||||
assertArrayEquals(bytes, decrypted);
|
||||
|
||||
}
|
||||
|
||||
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
|
||||
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV1
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class GPasswordEncryptionProviderTests {
|
||||
@Test
|
||||
fun testEncryptDecryptBytesPasswordV1() {
|
||||
val encryptionProvider = GPasswordEncryptionProviderV1();
|
||||
val bytes = "This is a test string.".toByteArray();
|
||||
|
||||
// Encrypt the plaintext
|
||||
val ciphertext = encryptionProvider.encrypt(bytes, "1234")
|
||||
|
||||
// Decrypt the ciphertext
|
||||
val decrypted = encryptionProvider.decrypt(ciphertext, "1234")
|
||||
|
||||
// The decrypted string should be equal to the original plaintext
|
||||
assertArrayEquals(bytes, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncryptDecryptBytesPasswordV0() {
|
||||
val encryptionProvider = GPasswordEncryptionProviderV0("1234".padStart(32, '9'));
|
||||
val bytes = "This is a test string.".toByteArray();
|
||||
|
||||
// Encrypt the plaintext
|
||||
val ciphertext = encryptionProvider.encrypt(bytes)
|
||||
|
||||
// Decrypt the ciphertext
|
||||
val decrypted = encryptionProvider.decrypt(ciphertext)
|
||||
|
||||
// The decrypted string should be equal to the original plaintext
|
||||
assertArrayEquals(bytes, decrypted);
|
||||
}
|
||||
|
||||
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
|
||||
assertEquals(a.size, b.size);
|
||||
for(i in 0 until a.size) {
|
||||
assertEquals(a[i], b[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,13 +159,27 @@ class FilterCapability {
|
||||
|
||||
|
||||
class PlatformAuthorLink {
|
||||
constructor(id, name, url, thumbnail, subscribers) {
|
||||
constructor(id, name, url, thumbnail, subscribers, membershipUrl) {
|
||||
this.id = id ?? PlatformID(); //PlatformID
|
||||
this.name = name ?? ""; //string
|
||||
this.url = url ?? ""; //string
|
||||
this.thumbnail = thumbnail; //string
|
||||
if(subscribers)
|
||||
this.subscribers = subscribers;
|
||||
if(membershipUrl)
|
||||
this.membershipUrl = membershipUrl ?? null; //string (for backcompat)
|
||||
}
|
||||
}
|
||||
class PlatformAuthorMembershipLink {
|
||||
constructor(id, name, url, thumbnail, subscribers, membershipUrl) {
|
||||
this.id = id ?? PlatformID(); //PlatformID
|
||||
this.name = name ?? ""; //string
|
||||
this.url = url ?? ""; //string
|
||||
this.thumbnail = thumbnail; //string
|
||||
if(subscribers)
|
||||
this.subscribers = subscribers;
|
||||
if(membershipUrl)
|
||||
this.membershipUrl = membershipUrl ?? null; //string
|
||||
}
|
||||
}
|
||||
class PlatformContent {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class HorizontalSpaceItemDecoration(private val startSpace: Int, private val betweenSpace: Int, private val endSpace: Int) : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
outRect.left = betweenSpace
|
||||
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
if (position == 0) {
|
||||
outRect.left = startSpace
|
||||
}
|
||||
|
||||
else if (position == state.itemCount - 1) {
|
||||
outRect.right = endSpace
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.webkit.CookieManager
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.activities.*
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
@@ -30,6 +28,7 @@ import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||
@@ -46,7 +45,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(
|
||||
R.string.manage_polycentric_identity, FieldForm.BUTTON,
|
||||
R.string.manage_your_polycentric_identity, -4
|
||||
R.string.manage_your_polycentric_identity, -5
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_person)
|
||||
fun managePolycentricIdentity() {
|
||||
@@ -61,7 +60,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(
|
||||
R.string.show_faq, FieldForm.BUTTON,
|
||||
R.string.get_answers_to_common_questions, -3
|
||||
R.string.get_answers_to_common_questions, -4
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_quiz)
|
||||
fun openFAQ() {
|
||||
@@ -74,7 +73,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
@FormField(
|
||||
R.string.show_issues, FieldForm.BUTTON,
|
||||
R.string.a_list_of_user_reported_and_self_reported_issues, -2
|
||||
R.string.a_list_of_user_reported_and_self_reported_issues, -3
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_data_alert)
|
||||
fun openIssues() {
|
||||
@@ -109,7 +108,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(
|
||||
R.string.manage_tabs, FieldForm.BUTTON,
|
||||
R.string.change_tabs_visible_on_the_home_screen, -1
|
||||
R.string.change_tabs_visible_on_the_home_screen, -2
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_tabs)
|
||||
fun manageTabs() {
|
||||
@@ -122,11 +121,39 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 0)
|
||||
var language = LanguageSettings();
|
||||
@Serializable
|
||||
class LanguageSettings {
|
||||
@FormField(R.string.app_language, FieldForm.DROPDOWN, R.string.may_require_restart, 5, "app_language")
|
||||
@DropdownFieldOptionsId(R.array.app_languages)
|
||||
var appLanguage: Int = 0;
|
||||
|
||||
fun getAppLanguageLocaleString(): String? {
|
||||
return when(appLanguage) {
|
||||
0 -> null
|
||||
1 -> "en";
|
||||
2 -> "de";
|
||||
3 -> "es";
|
||||
4 -> "pt";
|
||||
5 -> "fr"
|
||||
6 -> "ja";
|
||||
7 -> "ko";
|
||||
8 -> "zh";
|
||||
9 -> "ru";
|
||||
10 -> "ar";
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 1)
|
||||
var home = HomeSettings();
|
||||
@Serializable
|
||||
class HomeSettings {
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 5)
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
|
||||
@DropdownFieldOptionsId(R.array.feed_style)
|
||||
var homeFeedStyle: Int = 1;
|
||||
|
||||
@@ -136,21 +163,28 @@ class Settings : FragmentedStorageFileJson() {
|
||||
else
|
||||
return FeedStyle.THUMBNAIL;
|
||||
}
|
||||
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||
var previewFeedItems: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.search, "group", -1, 2)
|
||||
var search = SearchSettings();
|
||||
@Serializable
|
||||
class SearchSettings {
|
||||
@FormField(R.string.search_history, FieldForm.TOGGLE, -1, 4)
|
||||
@FormField(R.string.search_history, FieldForm.TOGGLE, R.string.may_require_restart, 3)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var searchHistory: Boolean = true;
|
||||
|
||||
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 5)
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 4)
|
||||
@DropdownFieldOptionsId(R.array.feed_style)
|
||||
var searchFeedStyle: Int = 1;
|
||||
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||
var previewFeedItems: Boolean = true;
|
||||
|
||||
|
||||
|
||||
fun getSearchFeedStyle(): FeedStyle {
|
||||
if(searchFeedStyle == 0)
|
||||
@@ -164,7 +198,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var subscriptions = SubscriptionsSettings();
|
||||
@Serializable
|
||||
class SubscriptionsSettings {
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 5)
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 4)
|
||||
@DropdownFieldOptionsId(R.array.feed_style)
|
||||
var subscriptionsFeedStyle: Int = 1;
|
||||
|
||||
@@ -175,6 +209,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||
return FeedStyle.THUMBNAIL;
|
||||
}
|
||||
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||
var previewFeedItems: Boolean = true;
|
||||
|
||||
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 6)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var fetchOnAppBoot: Boolean = true;
|
||||
@@ -208,6 +245,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 10)
|
||||
var allowPlaytimeTracking: Boolean = true;
|
||||
|
||||
}
|
||||
|
||||
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 4)
|
||||
@@ -215,10 +253,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Serializable
|
||||
class PlaybackSettings {
|
||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
||||
@DropdownFieldOptionsId(R.array.languages)
|
||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||
var primaryLanguage: Int = 0;
|
||||
|
||||
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.languages)[primaryLanguage];
|
||||
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||
|
||||
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||
@@ -277,10 +315,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
||||
var resumeAfterPreview: Int = 1;
|
||||
|
||||
|
||||
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 8)
|
||||
var useLiveChatWindow: Boolean = true;
|
||||
|
||||
fun shouldResumePreview(previewedPosition: Long): Boolean{
|
||||
if(resumeAfterPreview == 2)
|
||||
return true;
|
||||
@@ -288,6 +322,14 @@ class Settings : FragmentedStorageFileJson() {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 8)
|
||||
var useLiveChatWindow: Boolean = true;
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 8)
|
||||
var backgroundSwitchToAudio: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 5)
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.webkit.ConsoleMessage
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
@@ -23,6 +25,8 @@ import kotlinx.serialization.json.Json
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
private lateinit var _webView: WebView;
|
||||
private lateinit var _textUrl: TextView;
|
||||
private lateinit var _buttonClose: ImageButton;
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -30,6 +34,13 @@ class LoginActivity : AppCompatActivity() {
|
||||
setContentView(R.layout.activity_login);
|
||||
setNavigationBarColorAndIcons();
|
||||
|
||||
_textUrl = findViewById(R.id.text_url);
|
||||
_buttonClose = findViewById(R.id.button_close);
|
||||
_buttonClose.setOnClickListener {
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
_webView = findViewById(R.id.web_view);
|
||||
_webView.settings.javaScriptEnabled = true;
|
||||
CookieManager.getInstance().setAcceptCookie(true);
|
||||
@@ -60,6 +71,8 @@ class LoginActivity : AppCompatActivity() {
|
||||
};
|
||||
var isFirstLoad = true;
|
||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||
_textUrl.setText(url ?: "");
|
||||
|
||||
if(!isFirstLoad)
|
||||
return@subscribe;
|
||||
isFirstLoad = false;
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
@@ -154,6 +155,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
Logger.i(TAG, "MainActivity.attachBaseContext")
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||
StateApp.instance.mainAppStarting(this);
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.Synchronization
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.toURLInfoDataLink
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -222,7 +223,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80);
|
||||
|
||||
Glide.with(_imagePolycentric)
|
||||
.load(avatar?.toURLInfoDataLink(processHandle.system.toProto(), processHandle.processSecret.process.toProto(), systemState.servers.toList()))
|
||||
.load(avatar?.toURLInfoSystemLinkUrl(processHandle.system.toProto(), avatar.process, systemState.servers.toList()))
|
||||
.placeholder(R.drawable.placeholder_profile)
|
||||
.crossfade()
|
||||
.into(_imagePolycentric)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -13,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.Loader
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||
@@ -28,6 +30,10 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||
|
||||
private var _isFinished = false;
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
@@ -43,6 +49,11 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||
_form.setObjectValues();
|
||||
Settings.instance.save();
|
||||
|
||||
if(field.descriptor?.id == "app_language") {
|
||||
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
||||
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
|
||||
}
|
||||
};
|
||||
_buttonBack.setOnClickListener {
|
||||
finish();
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.futo.platformplayer.getOrThrow
|
||||
* A link to a channel, often with its own name and thumbnail
|
||||
*/
|
||||
@kotlinx.serialization.Serializable
|
||||
class PlatformAuthorLink {
|
||||
open class PlatformAuthorLink {
|
||||
val id: PlatformID;
|
||||
val name: String;
|
||||
val url: String;
|
||||
@@ -28,6 +28,9 @@ class PlatformAuthorLink {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
||||
if(value.has("membershipUrl"))
|
||||
return PlatformAuthorMembershipLink.fromV8(config, value);
|
||||
|
||||
val context = "AuthorLink"
|
||||
return PlatformAuthorLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||
value.getOrThrow(config ,"name", context),
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.futo.platformplayer.api.media.models
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
/**
|
||||
* A link to a channel, often with its own name and thumbnail
|
||||
*/
|
||||
@kotlinx.serialization.Serializable
|
||||
class PlatformAuthorMembershipLink: PlatformAuthorLink {
|
||||
val membershipUrl: String?;
|
||||
|
||||
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null, membershipUrl: String? = null): super(id, name, url, thumbnail, subscribers)
|
||||
{
|
||||
this.membershipUrl = membershipUrl;
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
|
||||
val context = "AuthorMembershipLink"
|
||||
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||
value.getOrThrow(config ,"name", context),
|
||||
value.getOrThrow(config, "url", context),
|
||||
value.getOrDefault<String>(config, "thumbnail", context, null),
|
||||
if(value.has("subscribers")) value.getOrThrow(config,"subscribers", context) else null,
|
||||
if(value.has("membershipUrl")) value.getOrThrow(config, "membershipUrl", context) else null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,10 @@ class Thumbnails {
|
||||
fun getLQThumbnail() : String? {
|
||||
return sources.firstOrNull()?.url;
|
||||
}
|
||||
fun getMinimumThumbnail(quality: Int): String? {
|
||||
return sources.firstOrNull { it.quality >= quality }?.url ?: getHQThumbnail();
|
||||
}
|
||||
|
||||
fun hasMultiple() = sources.size > 1;
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js
|
||||
|
||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
|
||||
data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
|
||||
override fun toString(): String {
|
||||
return "(headers: '$headers', cookieString: '$cookieMap')";
|
||||
}
|
||||
|
||||
fun toEncrypted(): String{
|
||||
return EncryptionProvider.instance.encrypt(serialize());
|
||||
return SourceEncrypted.fromDecrypted { serialize() }.toJson();
|
||||
}
|
||||
|
||||
private fun serialize(): String {
|
||||
@@ -25,20 +22,10 @@ data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? =
|
||||
val TAG = "SourceAuth";
|
||||
|
||||
fun fromEncrypted(encrypted: String?): SourceAuth? {
|
||||
if(encrypted == null)
|
||||
return null;
|
||||
|
||||
val decrypted = EncryptionProvider.instance.decrypt(encrypted);
|
||||
try {
|
||||
return deserialize(decrypted);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to deserialize authentication", ex);
|
||||
return null;
|
||||
}
|
||||
return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
|
||||
}
|
||||
|
||||
fun deserialize(str: String): SourceAuth {
|
||||
private fun deserialize(str: String): SourceAuth {
|
||||
val data = Json.decodeFromString<SerializedAuth>(str);
|
||||
return SourceAuth(data.cookieMap, data.headers);
|
||||
}
|
||||
|
||||
+3
-15
@@ -1,7 +1,5 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js
|
||||
|
||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -13,7 +11,7 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
|
||||
}
|
||||
|
||||
fun toEncrypted(): String{
|
||||
return EncryptionProvider.instance.encrypt(serialize());
|
||||
return SourceEncrypted.fromDecrypted { serialize() }.toJson();
|
||||
}
|
||||
|
||||
private fun serialize(): String {
|
||||
@@ -21,20 +19,10 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "SourceAuth";
|
||||
val TAG = "SourceCaptchaData";
|
||||
|
||||
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
|
||||
if(encrypted == null)
|
||||
return null;
|
||||
|
||||
val decrypted = EncryptionProvider.instance.decrypt(encrypted);
|
||||
try {
|
||||
return deserialize(decrypted);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to deserialize authentication", ex);
|
||||
return null;
|
||||
}
|
||||
return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
|
||||
}
|
||||
|
||||
fun deserialize(str: String): SourceCaptchaData {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js
|
||||
|
||||
import com.futo.platformplayer.encryption.GEncryptionProvider
|
||||
import com.futo.platformplayer.encryption.GEncryptionProviderV0
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.lang.Exception
|
||||
|
||||
@Serializable
|
||||
data class SourceEncrypted(
|
||||
val encrypted: String,
|
||||
val version: Int = GEncryptionProvider.version
|
||||
) {
|
||||
fun toJson(): String {
|
||||
return Json.encodeToString(this);
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromDecrypted(serializer: () -> String): SourceEncrypted {
|
||||
return SourceEncrypted(GEncryptionProvider.instance.encrypt(serializer()));
|
||||
}
|
||||
|
||||
fun <T> decryptEncrypted(encrypted: String?, deserializer: (decrypted: String) -> T): T? {
|
||||
if(encrypted == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
val encryptedSourceAuth = Json.decodeFromString<SourceEncrypted>(encrypted)
|
||||
if (encryptedSourceAuth.version != GEncryptionProvider.version) {
|
||||
throw Exception("Invalid encryption version.");
|
||||
}
|
||||
|
||||
val decrypted = GEncryptionProvider.instance.decrypt(encryptedSourceAuth.encrypted);
|
||||
try {
|
||||
return deserializer(decrypted);
|
||||
} catch(ex: Throwable) {
|
||||
Logger.e(SourceAuth.TAG, "Failed to deserialize SourceEncrypted<T>", ex);
|
||||
return null;
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
//Try to fall back to old mechanism, remove this eventually
|
||||
if (!encrypted.contains("version")) {
|
||||
val decrypted = GEncryptionProviderV0.instance.decrypt(encrypted);
|
||||
try {
|
||||
return deserializer(decrypted);
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(SourceAuth.TAG, "Failed to deserialize SourceEncrypted<T>", ex);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -144,7 +144,10 @@ class SourcePluginConfig(
|
||||
val description: String,
|
||||
val type: String,
|
||||
val default: String? = null,
|
||||
val variable: String? = null
|
||||
val variable: String? = null,
|
||||
val dependency: String? = null,
|
||||
val warningDialog: String? = null,
|
||||
val options: List<String>? = null
|
||||
) {
|
||||
@kotlinx.serialization.Transient
|
||||
val variableOrName: String get() = variable ?: name;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.futo.platformplayer.encryption
|
||||
|
||||
class GEncryptionProvider {
|
||||
companion object {
|
||||
val instance: GEncryptionProviderV1 = GEncryptionProviderV1.instance;
|
||||
val version = 1;
|
||||
}
|
||||
}
|
||||
+13
-16
@@ -8,9 +8,8 @@ import java.security.KeyStore
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class EncryptionProvider {
|
||||
class GEncryptionProviderV0 {
|
||||
private val _keyStore: KeyStore;
|
||||
private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null);
|
||||
|
||||
@@ -25,45 +24,43 @@ class EncryptionProvider {
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setRandomizedEncryptionRequired(false)
|
||||
.build());
|
||||
|
||||
|
||||
keyGenerator.generateKey();
|
||||
}
|
||||
}
|
||||
|
||||
fun encrypt(decrypted: String, password: String? = null): String {
|
||||
val encodedBytes = encrypt(decrypted.toByteArray(), password);
|
||||
fun encrypt(decrypted: String): String {
|
||||
val encodedBytes = encrypt(decrypted.toByteArray());
|
||||
val encrypted = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
||||
return encrypted;
|
||||
}
|
||||
fun encrypt(decrypted: ByteArray, password: String? = null): ByteArray {
|
||||
fun encrypt(decrypted: ByteArray): ByteArray {
|
||||
val c: Cipher = Cipher.getInstance(AES_MODE);
|
||||
val keyToUse = if(password == null) secretKey else SecretKeySpec(password.toByteArray(), "AES");
|
||||
c.init(Cipher.ENCRYPT_MODE, keyToUse, GCMParameterSpec(128, FIXED_IV));
|
||||
c.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
||||
return encodedBytes;
|
||||
}
|
||||
|
||||
fun decrypt(encrypted: String, password: String? = null): String {
|
||||
fun decrypt(encrypted: String): String {
|
||||
val c = Cipher.getInstance(AES_MODE);
|
||||
val keyToUse = if(password == null) secretKey else SecretKeySpec(password.toByteArray(), "AES");
|
||||
c.init(Cipher.DECRYPT_MODE, keyToUse, GCMParameterSpec(128, FIXED_IV));
|
||||
c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||
val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT)));
|
||||
return decrypted;
|
||||
}
|
||||
fun decrypt(encrypted: ByteArray, password: String? = null): ByteArray {
|
||||
fun decrypt(encrypted: ByteArray): ByteArray {
|
||||
val c = Cipher.getInstance(AES_MODE);
|
||||
val keyToUse = if(password == null) secretKey else SecretKeySpec(password.toByteArray(), "AES");
|
||||
c.init(Cipher.DECRYPT_MODE, keyToUse, GCMParameterSpec(128, FIXED_IV));
|
||||
c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||
return c.doFinal(encrypted);
|
||||
}
|
||||
|
||||
companion object {
|
||||
val instance: EncryptionProvider = EncryptionProvider();
|
||||
val instance: GEncryptionProviderV0 = GEncryptionProviderV0();
|
||||
|
||||
private val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101);
|
||||
private const val AndroidKeyStore = "AndroidKeyStore";
|
||||
private const val KEY_ALIAS = "FUTOMedia_Key";
|
||||
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||
private val TAG = "EncryptionProvider";
|
||||
private const val TAG_LENGTH = 128
|
||||
private val TAG = "GEncryptionProviderV0";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.futo.platformplayer.encryption
|
||||
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import java.security.Key
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
class GEncryptionProviderV1 {
|
||||
private val _keyStore: KeyStore;
|
||||
private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null);
|
||||
|
||||
constructor() {
|
||||
_keyStore = KeyStore.getInstance(AndroidKeyStore);
|
||||
_keyStore.load(null);
|
||||
|
||||
if (!_keyStore.containsAlias(KEY_ALIAS)) {
|
||||
val keyGenerator: KeyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore)
|
||||
keyGenerator.init(KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setRandomizedEncryptionRequired(false)
|
||||
.build());
|
||||
|
||||
keyGenerator.generateKey();
|
||||
}
|
||||
}
|
||||
|
||||
fun encrypt(decrypted: String): String {
|
||||
val encrypted = encrypt(decrypted.toByteArray());
|
||||
val encoded = Base64.encodeToString(encrypted, Base64.DEFAULT);
|
||||
return encoded;
|
||||
}
|
||||
fun encrypt(decrypted: ByteArray): ByteArray {
|
||||
val ivBytes = generateIv()
|
||||
val c: Cipher = Cipher.getInstance(AES_MODE);
|
||||
c.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, ivBytes));
|
||||
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
||||
return ivBytes + encodedBytes;
|
||||
}
|
||||
|
||||
fun decrypt(data: String): String {
|
||||
val bytes = Base64.decode(data, Base64.DEFAULT)
|
||||
return String(decrypt(bytes));
|
||||
}
|
||||
fun decrypt(bytes: ByteArray): ByteArray {
|
||||
val encrypted = bytes.sliceArray(IntRange(IV_SIZE, bytes.size - 1))
|
||||
val ivBytes = bytes.sliceArray(IntRange(0, IV_SIZE - 1))
|
||||
|
||||
val c = Cipher.getInstance(AES_MODE);
|
||||
c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, ivBytes));
|
||||
return c.doFinal(encrypted);
|
||||
}
|
||||
|
||||
private fun generateIv(): ByteArray {
|
||||
val r = SecureRandom()
|
||||
val ivBytes = ByteArray(IV_SIZE)
|
||||
r.nextBytes(ivBytes)
|
||||
return ivBytes
|
||||
}
|
||||
|
||||
companion object {
|
||||
val instance: GEncryptionProviderV1 = GEncryptionProviderV1();
|
||||
|
||||
private const val AndroidKeyStore = "AndroidKeyStore";
|
||||
private const val KEY_ALIAS = "FUTOMedia_Key";
|
||||
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||
private const val IV_SIZE = 12;
|
||||
private const val TAG_LENGTH = 128
|
||||
private val TAG = "GEncryptionProviderV1";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.futo.platformplayer.encryption
|
||||
|
||||
class GPasswordEncryptionProvider {
|
||||
companion object {
|
||||
val version = 1;
|
||||
val instance = GPasswordEncryptionProviderV1.instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.futo.platformplayer.encryption
|
||||
|
||||
import android.util.Base64
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class GPasswordEncryptionProviderV0 {
|
||||
private val _key: SecretKeySpec;
|
||||
|
||||
constructor(password: String) {
|
||||
_key = SecretKeySpec(password.toByteArray(), "AES");
|
||||
}
|
||||
|
||||
fun encrypt(decrypted: String): String {
|
||||
val encodedBytes = encrypt(decrypted.toByteArray());
|
||||
val encrypted = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
||||
return encrypted;
|
||||
}
|
||||
fun encrypt(decrypted: ByteArray): ByteArray {
|
||||
val c: Cipher = Cipher.getInstance(AES_MODE);
|
||||
c.init(Cipher.ENCRYPT_MODE, _key, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
||||
return encodedBytes;
|
||||
}
|
||||
|
||||
fun decrypt(encrypted: String): String {
|
||||
val c = Cipher.getInstance(AES_MODE);
|
||||
c.init(Cipher.DECRYPT_MODE, _key, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||
val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT)));
|
||||
return decrypted;
|
||||
}
|
||||
fun decrypt(encrypted: ByteArray): ByteArray {
|
||||
val c = Cipher.getInstance(AES_MODE);
|
||||
c.init(Cipher.DECRYPT_MODE, _key, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||
return c.doFinal(encrypted);
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101);
|
||||
private const val TAG_LENGTH = 128
|
||||
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||
private val TAG = "GPasswordEncryptionProviderV0";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.futo.platformplayer.encryption
|
||||
|
||||
import android.util.Base64
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class GPasswordEncryptionProviderV1 {
|
||||
fun encrypt(decrypted: String, password: String): String {
|
||||
val encrypted = encrypt(decrypted.toByteArray(), password);
|
||||
val encoded = Base64.encodeToString(encrypted, Base64.DEFAULT);
|
||||
return encoded;
|
||||
}
|
||||
|
||||
fun encrypt(decrypted: ByteArray, password: String): ByteArray {
|
||||
val saltBytes = generateSalt()
|
||||
val ivBytes = generateIv()
|
||||
val c: Cipher = Cipher.getInstance(AES_MODE);
|
||||
val key = deriveKeyFromPassword(password, saltBytes)
|
||||
|
||||
c.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(TAG_LENGTH, ivBytes));
|
||||
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
||||
return saltBytes + ivBytes + encodedBytes;
|
||||
}
|
||||
|
||||
fun decrypt(data: String, password: String): String {
|
||||
val bytes = Base64.decode(data, Base64.DEFAULT)
|
||||
return String(decrypt(bytes, password));
|
||||
}
|
||||
fun decrypt(bytes: ByteArray, password: String): ByteArray {
|
||||
val encrypted = bytes.sliceArray(IntRange(SALT_SIZE + IV_SIZE, bytes.size - 1))
|
||||
val ivBytes = bytes.sliceArray(IntRange(SALT_SIZE, SALT_SIZE + IV_SIZE - 1))
|
||||
val saltBytes = bytes.sliceArray(IntRange(0, SALT_SIZE - 1))
|
||||
val key = deriveKeyFromPassword(password, saltBytes)
|
||||
|
||||
val c = Cipher.getInstance(AES_MODE);
|
||||
c.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(TAG_LENGTH, ivBytes));
|
||||
return c.doFinal(encrypted);
|
||||
}
|
||||
|
||||
private fun deriveKeyFromPassword(password: String, salt: ByteArray): SecretKeySpec {
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val spec = PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH)
|
||||
val tmp = factory.generateSecret(spec)
|
||||
return SecretKeySpec(tmp.encoded, "AES")
|
||||
}
|
||||
|
||||
private fun generateSalt(): ByteArray {
|
||||
val random = SecureRandom()
|
||||
val salt = ByteArray(SALT_SIZE)
|
||||
random.nextBytes(salt)
|
||||
return salt
|
||||
}
|
||||
|
||||
private fun generateIv(): ByteArray {
|
||||
val r = SecureRandom()
|
||||
val ivBytes = ByteArray(IV_SIZE)
|
||||
r.nextBytes(ivBytes)
|
||||
return ivBytes
|
||||
}
|
||||
|
||||
companion object {
|
||||
val instance = GPasswordEncryptionProviderV1();
|
||||
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||
private const val IV_SIZE = 12
|
||||
private const val SALT_SIZE = 16
|
||||
private const val ITERATION_COUNT = 2 * 65536
|
||||
private const val KEY_LENGTH = 256
|
||||
private const val TAG_LENGTH = 128
|
||||
private val TAG = "GPasswordEncryptionProviderV1";
|
||||
}
|
||||
}
|
||||
+6
-26
@@ -11,11 +11,12 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.SupportView
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
|
||||
|
||||
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||
private var _buttonStore: BigButton? = null;
|
||||
private var _supportView: SupportView? = null
|
||||
|
||||
private var _lastChannel: IPlatformChannel? = null;
|
||||
private var _lastPolycentricProfile: PolycentricProfile? = null;
|
||||
@@ -24,20 +25,7 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_channel_monetization, container, false);
|
||||
_buttonStore = view.findViewById(R.id.button_store);
|
||||
|
||||
_buttonStore?.onClick?.subscribe {
|
||||
_lastPolycentricProfile?.systemState?.store?.let {
|
||||
try {
|
||||
val uri = Uri.parse(it);
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = uri
|
||||
startActivity(intent)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to open URI: '${it}'.", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
_supportView = view.findViewById(R.id.support);
|
||||
|
||||
_lastChannel?.also {
|
||||
setChannel(it);
|
||||
@@ -52,24 +40,16 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView();
|
||||
_buttonStore = null;
|
||||
_supportView = null;
|
||||
}
|
||||
|
||||
override fun setChannel(channel: IPlatformChannel) {
|
||||
_lastChannel = channel;
|
||||
_buttonStore?.visibility = View.GONE;
|
||||
}
|
||||
|
||||
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
||||
_lastPolycentricProfile = polycentricProfile;
|
||||
|
||||
if (polycentricProfile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (polycentricProfile.systemState.store.isNotEmpty()) {
|
||||
_buttonStore?.visibility = View.VISIBLE;
|
||||
}
|
||||
_lastPolycentricProfile = polycentricProfile
|
||||
_supportView?.setPolycentricProfile(polycentricProfile, animate)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+3
-3
@@ -225,7 +225,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
if (faqIndex != -1) {
|
||||
val button = buttons[faqIndex]
|
||||
buttons.removeAt(faqIndex)
|
||||
buttons.add(1, button)
|
||||
buttons.add(if (buttons.size == 1) 1 else 0, button)
|
||||
}
|
||||
|
||||
for (data in buttons) {
|
||||
@@ -252,8 +252,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||
if (_buttonsVisible - 2 >= defs.size) {
|
||||
updateBottomMenuButtons(defs.slice(IntRange(0, defs.size - 1)).toMutableList(), false);
|
||||
if (_buttonsVisible - 1 >= defs.size) {
|
||||
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||
} else {
|
||||
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
|
||||
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
|
||||
|
||||
+3
-1
@@ -63,7 +63,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
}
|
||||
|
||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||
_view?.setPreviewsEnabled(previewsEnabled);
|
||||
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.search.previewFeedItems);
|
||||
}
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
@@ -93,6 +93,8 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
Logger.w(TAG, "Failed to load results.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||
}
|
||||
|
||||
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
|
||||
+3
-1
@@ -78,7 +78,7 @@ class HomeFragment : MainFragment() {
|
||||
}
|
||||
|
||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||
_view?.setPreviewsEnabled(previewsEnabled);
|
||||
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems);
|
||||
}
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
@@ -122,6 +122,8 @@ class HomeFragment : MainFragment() {
|
||||
setLoading(false);
|
||||
};
|
||||
};
|
||||
|
||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||
}
|
||||
|
||||
fun onShown() {
|
||||
|
||||
+17
-1
@@ -63,6 +63,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
private var _textSelectDeselectAll: TextView;
|
||||
private var _textNothingToImport: TextView;
|
||||
private var _textCounter: TextView;
|
||||
private var _textLoadMore: TextView;
|
||||
private var _adapterView: AnyAdapterView<SelectableIPlatformChannel, ImportSubscriptionViewHolder>;
|
||||
private var _links: List<String> = listOf();
|
||||
private val _items: ArrayList<SelectableIPlatformChannel> = arrayListOf();
|
||||
@@ -79,6 +80,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
_textNothingToImport = findViewById(R.id.nothing_to_import);
|
||||
_textSelectDeselectAll = findViewById(R.id.text_select_deselect_all);
|
||||
_textCounter = findViewById(R.id.text_select_counter);
|
||||
_textLoadMore = findViewById(R.id.text_load_more);
|
||||
_spinner = findViewById(R.id.channel_loader);
|
||||
|
||||
_adapterView = findViewById<RecyclerView>(R.id.recycler_import).asAny( _items) {
|
||||
@@ -120,6 +122,19 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
|
||||
loadNext();
|
||||
};
|
||||
|
||||
_textLoadMore.setOnClickListener {
|
||||
if (!_limitToastShown) {
|
||||
return@setOnClickListener;
|
||||
}
|
||||
|
||||
_textLoadMore.visibility = View.GONE;
|
||||
_limitToastShown = false;
|
||||
_counter = 0;
|
||||
load();
|
||||
};
|
||||
|
||||
_textLoadMore.visibility = View.GONE;
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
@@ -165,7 +180,8 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
if (_counter >= MAXIMUM_BATCH_SIZE) {
|
||||
if (!_limitToastShown) {
|
||||
_limitToastShown = true;
|
||||
UIDialogs.toast(context, "Stopped after {requestCount} to avoid rate limit, re-enter to import rest".replace("{requestCount}", MAXIMUM_BATCH_SIZE.toString()));
|
||||
_textLoadMore.visibility = View.VISIBLE;
|
||||
UIDialogs.toast(context, context.getString(R.string.stopped_after_requestcount_to_avoid_rate_limit_click_load_more_to_load_more).replace("{requestCount}", MAXIMUM_BATCH_SIZE.toString()));
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
||||
+3
-1
@@ -81,7 +81,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
}
|
||||
|
||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||
_view?.setPreviewsEnabled(previewsEnabled);
|
||||
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.subscriptions.previewFeedItems);
|
||||
}
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
@@ -108,6 +108,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
};
|
||||
|
||||
initializeToolbarContent();
|
||||
|
||||
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
|
||||
}
|
||||
|
||||
fun onShown() {
|
||||
|
||||
+31
-23
@@ -36,6 +36,8 @@ import com.futo.platformplayer.api.media.LiveChatManager
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||
@@ -49,6 +51,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
@@ -70,6 +73,7 @@ import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.states.*
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
import com.futo.platformplayer.views.MonetizationView
|
||||
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
||||
import com.futo.platformplayer.views.casting.CastView
|
||||
import com.futo.platformplayer.views.comments.AddCommentView
|
||||
@@ -79,6 +83,7 @@ import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
import com.futo.platformplayer.views.overlays.SupportOverlay
|
||||
import com.futo.platformplayer.views.overlays.slideup.*
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
import com.futo.platformplayer.views.pills.RoundButton
|
||||
@@ -191,6 +196,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _container_content_replies: RepliesOverlay;
|
||||
private val _container_content_description: DescriptionOverlay;
|
||||
private val _container_content_liveChat: LiveChatOverlay;
|
||||
private val _container_content_support: SupportOverlay;
|
||||
|
||||
private var _container_content_current: View;
|
||||
|
||||
@@ -200,9 +206,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _imageDislikeIcon: ImageView;
|
||||
private val _imageLikeIcon: ImageView;
|
||||
|
||||
private val _buttonSupport: LinearLayout;
|
||||
private val _buttonStore: LinearLayout;
|
||||
private val _layoutMonetization: LinearLayout;
|
||||
private val _monetization: MonetizationView;
|
||||
|
||||
private val _buttonMore: RoundButton;
|
||||
|
||||
@@ -292,6 +296,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
||||
_container_content_description = findViewById(R.id.videodetail_container_description);
|
||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||
_container_content_support = findViewById(R.id.videodetail_container_support)
|
||||
|
||||
_textComments = findViewById(R.id.text_comments);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
@@ -310,11 +315,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_imageLikeIcon = findViewById(R.id.image_like_icon);
|
||||
_imageDislikeIcon = findViewById(R.id.image_dislike_icon);
|
||||
|
||||
_buttonSupport = findViewById(R.id.button_support);
|
||||
_buttonStore = findViewById(R.id.button_store);
|
||||
_layoutMonetization = findViewById(R.id.layout_monetization);
|
||||
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
_monetization = findViewById(R.id.monetization);
|
||||
_player.attachPlayer();
|
||||
|
||||
|
||||
@@ -327,16 +328,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
||||
};
|
||||
|
||||
_buttonSupport.setOnClickListener {
|
||||
val author = video?.author ?: _searchVideo?.author;
|
||||
author?.let { fragment.navigate<ChannelFragment>(it).selectTab(2); };
|
||||
fragment.lifecycleScope.launch {
|
||||
delay(100);
|
||||
fragment.minimizeVideoDetail();
|
||||
};
|
||||
_monetization.onSupportTap.subscribe {
|
||||
_container_content_support.setPolycentricProfile(_polycentricProfile?.profile, false);
|
||||
switchContentView(_container_content_support);
|
||||
};
|
||||
|
||||
_buttonStore.setOnClickListener {
|
||||
_monetization.onStoreTap.subscribe {
|
||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
||||
try {
|
||||
val uri = Uri.parse(it);
|
||||
@@ -349,6 +346,13 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
};
|
||||
|
||||
_player.attachPlayer();
|
||||
|
||||
_container_content_liveChat.onRaidNow.subscribe {
|
||||
StatePlayer.instance.clearQueue();
|
||||
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
||||
};
|
||||
|
||||
StateApp.instance.preventPictureInPicture.subscribe(this) {
|
||||
Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true");
|
||||
preventPictureInPicture = true;
|
||||
@@ -545,6 +549,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
|
||||
_description_viewMore.setOnClickListener {
|
||||
switchContentView(_container_content_description);
|
||||
@@ -812,7 +817,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
when (Settings.instance.playback.backgroundPlay) {
|
||||
0 -> handlePause();
|
||||
1 -> {
|
||||
if(!(video?.isLive ?: false))
|
||||
if(!(video?.isLive ?: false) && Settings.instance.playback.backgroundSwitchToAudio)
|
||||
_player.switchToAudioMode();
|
||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||
}
|
||||
@@ -847,6 +852,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_replies.cleanup();
|
||||
_container_content_queue.cleanup();
|
||||
_container_content_description.cleanup();
|
||||
_container_content_support.cleanup();
|
||||
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
@@ -1048,6 +1054,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to get chapters", ex);
|
||||
_player.setChapters(null);
|
||||
|
||||
/*withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
||||
@@ -1094,6 +1101,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_2).toInt(), 0, 0);
|
||||
}
|
||||
|
||||
video.author.let {
|
||||
if(it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty())
|
||||
_monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl);
|
||||
}
|
||||
|
||||
_minimize_title.text = video.name;
|
||||
_minimize_meta.text = video.author.name;
|
||||
|
||||
@@ -1809,6 +1821,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_isCasting = isCasting;
|
||||
|
||||
if(isCasting) {
|
||||
setFullscreen(false);
|
||||
_player.stop();
|
||||
_player.hideControls(false);
|
||||
_cast.visibility = View.VISIBLE;
|
||||
@@ -2096,12 +2109,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_channelName.text = cachedPolycentricProfile.profile.systemState.username;
|
||||
_layoutMonetization.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
}
|
||||
_monetization.setPolycentricProfile(cachedPolycentricProfile, animate);
|
||||
}
|
||||
|
||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.futo.platformplayer.images;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Priority;
|
||||
|
||||
@@ -39,7 +39,12 @@ class PolycentricCache {
|
||||
ContentType.USERNAME.value,
|
||||
ContentType.DESCRIPTION.value,
|
||||
ContentType.STORE.value,
|
||||
ContentType.SERVER.value
|
||||
ContentType.SERVER.value,
|
||||
ContentType.STORE_DATA.value,
|
||||
ContentType.PROMOTION_BANNER.value,
|
||||
ContentType.PROMOTION.value,
|
||||
ContentType.MEMBERSHIP_URLS.value,
|
||||
ContentType.DONATION_DESTINATIONS.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||
import android.media.MediaMetadata
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.SystemClock
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
@@ -278,7 +279,13 @@ class MediaPlaybackService : Service() {
|
||||
|
||||
Logger.i(TAG, "Updating notification bitmap=${if (bitmap != null) "yes" else "no."} channelId=${channel.id} icon=${icon} video=${video?.name ?: ""} playWhenReady=${playWhenReady} session.sessionToken=${session.sessionToken}");
|
||||
|
||||
startForeground(MEDIA_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// For API 29 and above
|
||||
startForeground(MEDIA_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||
} else {
|
||||
// For API 28 and below
|
||||
startForeground(MEDIA_NOTIF_ID, notif);
|
||||
}
|
||||
|
||||
_notif_last_bitmap = bitmap;
|
||||
}
|
||||
|
||||
@@ -354,6 +354,7 @@ class StateApp {
|
||||
}
|
||||
|
||||
fun mainAppStarting(context: Context) {
|
||||
Logger.i(TAG, "MainApp Starting");
|
||||
initializeFiles(true);
|
||||
|
||||
val logFile = File(context.filesDir, "log.txt");
|
||||
@@ -372,14 +373,18 @@ class StateApp {
|
||||
|
||||
Logger.setLogConsumers(listOf(AndroidLogConsumer()));
|
||||
}
|
||||
|
||||
StatePayment.instance.initialize();
|
||||
|
||||
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: Initializing [Telemetry]");
|
||||
if (!BuildConfig.DEBUG) {
|
||||
StateTelemetry.instance.initialize();
|
||||
StateTelemetry.instance.upload();
|
||||
@@ -400,11 +405,12 @@ class StateApp {
|
||||
}
|
||||
}
|
||||
fun mainAppStarted(context: Context) {
|
||||
Logger.i(TAG, "App started");
|
||||
Logger.i(TAG, "MainApp Started");
|
||||
|
||||
//Start loading cache
|
||||
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
Logger.i(TAG, "MainApp Started: Initializing [ChannelContentCache]");
|
||||
val time = measureTimeMillis {
|
||||
ChannelContentCache.instance;
|
||||
}
|
||||
@@ -419,10 +425,12 @@ class StateApp {
|
||||
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
||||
StateDeveloper.instance.runServer();
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Check [Migration (Subscriptions)]");
|
||||
if(StateSubscriptions.instance.shouldMigrate())
|
||||
StateSubscriptions.instance.tryMigrateIfNecessary();
|
||||
|
||||
if(Settings.instance.downloads.shouldDownload()) {
|
||||
Logger.i(TAG, "MainApp Started: Check [Downloads]");
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
|
||||
StateDownloads.instance.getDownloadPlaylists();
|
||||
@@ -430,8 +438,10 @@ class StateApp {
|
||||
DownloadService.getOrCreateService(context);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Check [Exports]");
|
||||
StateDownloads.instance.checkForExportTodos();
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
|
||||
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
|
||||
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
|
||||
val backgroundDownload = Settings.instance.autoUpdate.backgroundDownload == 1;
|
||||
@@ -455,6 +465,7 @@ class StateApp {
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
||||
_receiverBecomingNoisy?.let {
|
||||
_receiverBecomingNoisy = null;
|
||||
context.unregisterReceiver(it);
|
||||
@@ -463,6 +474,7 @@ class StateApp {
|
||||
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||
|
||||
//Migration
|
||||
Logger.i(TAG, "MainApp Started: Check [Migrations]");
|
||||
migrateStores(context, listOf(
|
||||
StateSubscriptions.instance.toMigrateCheck(),
|
||||
StatePlaylists.instance.toMigrateCheck()
|
||||
@@ -470,6 +482,7 @@ class StateApp {
|
||||
|
||||
if(Settings.instance.subscriptions.fetchOnAppBoot) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Logger.i(TAG, "MainApp Started: Fetch [Subscriptions]");
|
||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
||||
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
||||
@@ -484,9 +497,11 @@ class StateApp {
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Initialize [BackgroundWork]");
|
||||
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
|
||||
scheduleBackgroundWork(context, interval != 0, interval);
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Initialize [AutoBackup]");
|
||||
if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) {
|
||||
StateAnnouncement.instance.registerAnnouncement("backup", "Set Automatic Backup", "Configure daily backups of your data to restore in case of catastrophic failure.", AnnouncementType.SESSION, null, null, "Configure", {
|
||||
if(context is IWithResultLauncher && !Settings.instance.storage.isStorageMainValid(context)) {
|
||||
@@ -514,6 +529,7 @@ class StateApp {
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Initialize [Announcements]");
|
||||
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StateAnnouncement.instance.loadAnnouncements();
|
||||
@@ -532,7 +548,7 @@ class StateApp {
|
||||
}
|
||||
|
||||
StateAnnouncement.instance.registerDidYouKnow();
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Finished");
|
||||
}
|
||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||
if(!Settings.instance.didFirstStart) {
|
||||
@@ -731,6 +747,34 @@ class StateApp {
|
||||
}
|
||||
}
|
||||
|
||||
fun getLocaleContext(baseContext: Context?): Context? {
|
||||
val locale = getLocaleSetting(baseContext);
|
||||
try {
|
||||
|
||||
if (baseContext != null && locale != null) {
|
||||
val config = baseContext.resources.configuration;
|
||||
config.setLocale(locale);
|
||||
return baseContext.createConfigurationContext(config);
|
||||
}
|
||||
return baseContext;
|
||||
}
|
||||
catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to load locale", ex);
|
||||
return baseContext;
|
||||
}
|
||||
}
|
||||
fun getLocaleSetting(context: Context?): Locale? {
|
||||
return context?.getSharedPreferences("language", Context.MODE_PRIVATE)
|
||||
?.getString("language", null)
|
||||
?.let { Locale(it) };
|
||||
}
|
||||
fun setLocaleSetting(context: Context?, locale: String?) {
|
||||
context?.getSharedPreferences("language", Context.MODE_PRIVATE)
|
||||
?.edit()
|
||||
?.putString("language", locale)
|
||||
?.apply();
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "StateApp";
|
||||
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract.EXTRA_INITIAL_URI
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.copyTo
|
||||
import com.futo.platformplayer.copyToOutputStream
|
||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
||||
import com.futo.platformplayer.getInputStream
|
||||
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
|
||||
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
|
||||
import com.futo.platformplayer.getNowDiffHours
|
||||
import com.futo.platformplayer.getOutputStream
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.readBytes
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
@@ -38,9 +29,8 @@ import kotlinx.serialization.json.Json
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.lang.Exception
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
@@ -82,7 +72,7 @@ class StateBackup {
|
||||
val pbytes = password.toByteArray();
|
||||
if(pbytes.size < 4 || pbytes.size > 32)
|
||||
throw IllegalStateException("Automatic backup passwords should atleast be 4 character and smaller than 32");
|
||||
return password.padStart(32, '9');
|
||||
return password;
|
||||
}
|
||||
fun hasAutomaticBackup(): Boolean {
|
||||
val context = StateApp.instance.contextOrNull ?: return false;
|
||||
@@ -106,8 +96,8 @@ class StateBackup {
|
||||
val data = export();
|
||||
val zip = data.asZip();
|
||||
|
||||
val encryptedZip = EncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword());
|
||||
|
||||
//Prepend some magic bytes to identify everything version 1 and up
|
||||
val encryptedZip = byteArrayOf(0x11, 0x22, 0x33, 0x44, GPasswordEncryptionProvider.version.toByte()) + GPasswordEncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword());
|
||||
if(!Settings.instance.storage.isStorageMainValid(context)) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
UIDialogs.toast("Missing permissions for auto-backup, please set the external general directory in settings");
|
||||
@@ -151,8 +141,7 @@ class StateBackup {
|
||||
throw IllegalStateException("Backup file does not exist");
|
||||
|
||||
val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]");
|
||||
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
||||
importZipBytes(context, scope, backupBytes);
|
||||
importEncryptedZipBytes(context, scope, backupBytesEncrypted, password);
|
||||
Logger.i(TAG, "Finished AutoBackup restore");
|
||||
}
|
||||
catch (exSec: FileNotFoundException) {
|
||||
@@ -179,13 +168,30 @@ class StateBackup {
|
||||
throw ex;
|
||||
|
||||
val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]");
|
||||
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
||||
importZipBytes(context, scope, backupBytes);
|
||||
importEncryptedZipBytes(context, scope, backupBytesEncrypted, password);
|
||||
Logger.i(TAG, "Finished AutoBackup restore");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun importEncryptedZipBytes(context: Context, scope: CoroutineScope, backupBytesEncrypted: ByteArray, password: String) {
|
||||
val backupBytes: ByteArray;
|
||||
//Check magic bytes indicating version 1 and up
|
||||
if (backupBytesEncrypted[0] == 0x11.toByte() && backupBytesEncrypted[1] == 0x22.toByte() && backupBytesEncrypted[2] == 0x33.toByte() && backupBytesEncrypted[3] == 0x44.toByte()) {
|
||||
val version = backupBytesEncrypted[4].toInt();
|
||||
if (version != GPasswordEncryptionProvider.version) {
|
||||
throw Exception("Invalid encryption version");
|
||||
}
|
||||
|
||||
backupBytes = GPasswordEncryptionProvider.instance.decrypt(backupBytesEncrypted.sliceArray(IntRange(5, backupBytesEncrypted.size - 1)), getAutomaticBackupPassword(password))
|
||||
} else {
|
||||
//Else its a version 0
|
||||
backupBytes = GPasswordEncryptionProviderV0(getAutomaticBackupPassword(password).padStart(32, '9')).decrypt(backupBytesEncrypted);
|
||||
}
|
||||
|
||||
importZipBytes(context, scope, backupBytes);
|
||||
}
|
||||
|
||||
fun startExternalBackup() {
|
||||
val data = export();
|
||||
val now = OffsetDateTime.now();
|
||||
|
||||
+2
-1
@@ -79,7 +79,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
if(result != null) {
|
||||
if(result.pager != null)
|
||||
taskResults.add(result);
|
||||
else if(result.exception != null) {
|
||||
if(result.exception != null) {
|
||||
val ex = result.exception;
|
||||
if(ex != null) {
|
||||
val nonRuntimeEx = findNonRuntimeException(ex);
|
||||
@@ -198,6 +198,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
||||
pager = ChannelContentCache.instance.getChannelCachePager(task.sub.channel.url);
|
||||
taskEx = ex;
|
||||
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
||||
}
|
||||
}
|
||||
return@submit SubscriptionTaskResult(task, null, taskEx);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.HorizontalSpaceItemDecoration
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
data class StoreItem(
|
||||
val url: String,
|
||||
val name: String,
|
||||
val image: String
|
||||
);
|
||||
|
||||
class MonetizationView : LinearLayout {
|
||||
private val _buttonSupport: LinearLayout;
|
||||
private val _buttonStore: LinearLayout;
|
||||
private val _buttonMembership: LinearLayout;
|
||||
private val _membershipPlatform: PlatformIndicator;
|
||||
private var _membershipUrl: String? = null;
|
||||
|
||||
private val _textMerchandise: TextView;
|
||||
private val _recyclerMerchandise: RecyclerView;
|
||||
private val _loaderMerchandise: Loader;
|
||||
private val _layoutMerchandise: FrameLayout;
|
||||
private var _merchandiseAdapterView: AnyAdapterView<StoreItem, StoreItemViewHolder>? = null;
|
||||
|
||||
private val _root: LinearLayout;
|
||||
|
||||
private val _taskLoadMerchandise = TaskHandler<String, List<StoreItem>>(StateApp.instance.scopeGetter, { url ->
|
||||
val client = ManagedHttpClient();
|
||||
val result = client.get("https://storecache.grayjay.app/StoreData?url=$url")
|
||||
if (!result.isOk) {
|
||||
throw Exception("Failed to retrieve store data.");
|
||||
}
|
||||
|
||||
return@TaskHandler result.body?.let { Json.decodeFromString<List<StoreItem>>(it.string()); } ?: listOf();
|
||||
})
|
||||
.success { setMerchandise(it) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load merchandise profile.", it);
|
||||
};
|
||||
|
||||
val onSupportTap = Event0();
|
||||
val onStoreTap = Event0();
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_monetization, this);
|
||||
_buttonSupport = findViewById(R.id.button_support);
|
||||
_buttonStore = findViewById(R.id.button_store);
|
||||
_buttonMembership = findViewById(R.id.button_membership);
|
||||
_membershipPlatform = findViewById(R.id.membership_platform);
|
||||
_buttonMembership.setOnClickListener {
|
||||
_membershipUrl?.let {
|
||||
val uri = Uri.parse(it);
|
||||
val intent = Intent(Intent.ACTION_VIEW);
|
||||
intent.data = uri;
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
_textMerchandise = findViewById(R.id.text_merchandise);
|
||||
_recyclerMerchandise = findViewById(R.id.recycler_merchandise);
|
||||
_loaderMerchandise = findViewById(R.id.loader_merchandise);
|
||||
_layoutMerchandise = findViewById(R.id.layout_merchandise);
|
||||
|
||||
_root = findViewById(R.id.root);
|
||||
|
||||
_recyclerMerchandise.addItemDecoration(HorizontalSpaceItemDecoration(30, 16, 30))
|
||||
_merchandiseAdapterView = _recyclerMerchandise.asAny(orientation = RecyclerView.HORIZONTAL);
|
||||
|
||||
_buttonSupport.setOnClickListener { onSupportTap.emit(); }
|
||||
_buttonStore.setOnClickListener { onStoreTap.emit(); }
|
||||
_buttonMembership.visibility = View.GONE;
|
||||
setMerchandise(null);
|
||||
}
|
||||
|
||||
fun setPlatformMembership(pluginId: String?, url: String? = null) {
|
||||
if(pluginId.isNullOrEmpty() || url.isNullOrEmpty()) {
|
||||
_buttonMembership.visibility = GONE;
|
||||
_membershipUrl = null;
|
||||
}
|
||||
else {
|
||||
_membershipUrl = url;
|
||||
_membershipPlatform.setPlatformFromClientID(pluginId);
|
||||
_buttonMembership.visibility = VISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMerchandise(items: List<StoreItem>?) {
|
||||
_loaderMerchandise.stop();
|
||||
|
||||
if (items == null) {
|
||||
_textMerchandise.visibility = View.GONE;
|
||||
_recyclerMerchandise.visibility = View.GONE;
|
||||
_layoutMerchandise.visibility = View.GONE;
|
||||
} else {
|
||||
_textMerchandise.visibility = View.VISIBLE;
|
||||
_recyclerMerchandise.visibility = View.VISIBLE;
|
||||
_layoutMerchandise.visibility = View.VISIBLE;
|
||||
_merchandiseAdapterView?.adapter?.setData(items.shuffled());
|
||||
}
|
||||
}
|
||||
|
||||
fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
if (profile != null) {
|
||||
if (profile.systemState.store.isNotEmpty()) {
|
||||
_buttonStore.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_buttonStore.visibility = View.GONE;
|
||||
}
|
||||
|
||||
_root.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_root.visibility = View.GONE;
|
||||
}
|
||||
|
||||
setMerchandise(null);
|
||||
val storeData = profile?.systemState?.storeData;
|
||||
if (storeData != null) {
|
||||
try {
|
||||
val storeItems = Json.decodeFromString<List<StoreItem>>(storeData);
|
||||
setMerchandise(storeItems);
|
||||
} catch (_: Throwable) {
|
||||
try {
|
||||
val uri = Uri.parse(storeData);
|
||||
if (uri.isAbsolute) {
|
||||
_taskLoadMerchandise.run(storeData);
|
||||
_loaderMerchandise.start();
|
||||
} else {
|
||||
Logger.i(TAG, "Merchandise not loaded, not URL nor JSON")
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "MonetizationView";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
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.dp
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
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
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import userpackage.Protocol.ImageManifest
|
||||
|
||||
class SupportView : LinearLayout {
|
||||
private val _layoutStore: LinearLayout
|
||||
private val _buttonPromotion: BigButton
|
||||
private val _layoutMemberships: LinearLayout
|
||||
private val _layoutMembershipEntries: LinearLayout
|
||||
private val _layoutPromotions: LinearLayout
|
||||
private val _layoutPromotionEntries: LinearLayout
|
||||
private val _layoutDonation: LinearLayout
|
||||
private val _layoutDonationEntries: LinearLayout
|
||||
private val _buttonStore: BigButton
|
||||
private val _imagePromotion: ShapeableImageView
|
||||
private var _textNoSupportOptionsSet: TextView
|
||||
private var _polycentricProfile: PolycentricProfile? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_support, this);
|
||||
|
||||
_layoutStore = findViewById(R.id.layout_store)
|
||||
_buttonStore = findViewById(R.id.button_store)
|
||||
_layoutMemberships = findViewById(R.id.layout_memberships)
|
||||
_layoutMembershipEntries = findViewById(R.id.layout_membership_entries)
|
||||
_layoutPromotions = findViewById(R.id.layout_promotions)
|
||||
_layoutPromotionEntries = findViewById(R.id.layout_promotion_entries)
|
||||
_layoutDonation = findViewById(R.id.layout_donation)
|
||||
_layoutDonationEntries = findViewById(R.id.layout_donation_entries)
|
||||
_buttonPromotion = findViewById(R.id.button_promotion)
|
||||
_imagePromotion = findViewById(R.id.image_promotion)
|
||||
_textNoSupportOptionsSet = findViewById(R.id.text_no_support_options_set)
|
||||
|
||||
_buttonPromotion.onClick.subscribe { openPromotion() }
|
||||
_imagePromotion.setOnClickListener { openPromotion() }
|
||||
_buttonStore.onClick.subscribe {
|
||||
val storeUrl = _polycentricProfile?.systemState?.store ?: return@subscribe
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(storeUrl))
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPromotion() {
|
||||
val promotionUrl = _polycentricProfile?.systemState?.promotion ?: return
|
||||
val uri = Uri.parse(promotionUrl)
|
||||
if (!uri.isAbsolute && (uri.scheme == "https" || uri.scheme == "http")) {
|
||||
return
|
||||
}
|
||||
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
|
||||
private fun setMemberships(urls: List<String>) {
|
||||
_layoutMembershipEntries.removeAllViews()
|
||||
for (url in urls) {
|
||||
val button = createMembershipButton(url)
|
||||
_layoutMembershipEntries.addView(button)
|
||||
}
|
||||
_layoutMemberships.visibility = if (urls.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
private fun createMembershipButton(url: String): BigButton {
|
||||
val uri = Uri.parse(url)
|
||||
val name: String
|
||||
val iconDrawableId: Int
|
||||
|
||||
if (uri.host?.contains("patreon.com") == true) {
|
||||
name = "Patreon"
|
||||
iconDrawableId = R.drawable.patreon
|
||||
} else {
|
||||
name = uri.host.toString()
|
||||
iconDrawableId = R.drawable.ic_web_white
|
||||
}
|
||||
|
||||
return BigButton(context, name, "Become a member on $name", iconDrawableId) {
|
||||
val intent = Intent(Intent.ACTION_VIEW);
|
||||
intent.data = uri;
|
||||
context.startActivity(intent);
|
||||
}.apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
};
|
||||
}
|
||||
|
||||
private fun setDonations(destinations: List<String>) {
|
||||
_layoutDonationEntries.removeAllViews()
|
||||
for (destination in destinations) {
|
||||
val button = createDonationButton(destination)
|
||||
_layoutDonationEntries.addView(button)
|
||||
}
|
||||
_layoutDonation.visibility = if (destinations.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
private enum class CryptoType {
|
||||
BITCOIN, ETHEREUM, LITECOIN, RIPPLE, UNKNOWN
|
||||
}
|
||||
|
||||
private fun getCryptoType(address: String): CryptoType {
|
||||
val btcRegex = Regex("^(1|3)[1-9A-HJ-NP-Za-km-z]{25,34}$|^(bc1)[0-9a-zA-HJ-NP-Z]{39,59}$")
|
||||
val ethRegex = Regex("^(0x)[0-9a-fA-F]{40}$")
|
||||
val ltcRegex = Regex("^(L|M)[1-9A-HJ-NP-Za-km-z]{26,33}$|^(ltc1)[0-9a-zA-HJ-NP-Z]{39,59}$")
|
||||
val xrpRegex = Regex("^r[1-9A-HJ-NP-Za-km-z]{24,34}$")
|
||||
|
||||
return when {
|
||||
ltcRegex.matches(address) -> CryptoType.LITECOIN
|
||||
btcRegex.matches(address) -> CryptoType.BITCOIN
|
||||
ethRegex.matches(address) -> CryptoType.ETHEREUM
|
||||
xrpRegex.matches(address) -> CryptoType.RIPPLE
|
||||
else -> CryptoType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDonationButton(destination: String): BigButton {
|
||||
val uri = Uri.parse(destination)
|
||||
|
||||
var action: (() -> Unit)? = null
|
||||
val (name, iconDrawableId, cryptoType) = if (uri.scheme == "http" || uri.scheme == "https") {
|
||||
val hostName = uri.host ?: ""
|
||||
|
||||
action = {
|
||||
val intent = Intent(Intent.ACTION_VIEW);
|
||||
intent.data = uri;
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
if (hostName.contains("paypal.com")) {
|
||||
Triple("Paypal", R.drawable.paypal, null) // Replace with your actual PayPal drawable resource
|
||||
} else {
|
||||
Triple(hostName, R.drawable.ic_web_white, null) // Replace with your generic web drawable resource
|
||||
}
|
||||
} else {
|
||||
action = {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Donation Address", destination)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
when (getCryptoType(destination)) {
|
||||
CryptoType.BITCOIN -> Triple("Bitcoin", R.drawable.bitcoin, CryptoType.BITCOIN)
|
||||
CryptoType.ETHEREUM -> Triple("Ethereum", R.drawable.ethereum, CryptoType.ETHEREUM)
|
||||
CryptoType.LITECOIN -> Triple("Litecoin", R.drawable.litecoin, CryptoType.LITECOIN)
|
||||
CryptoType.RIPPLE -> Triple("Ripple", R.drawable.ripple, CryptoType.RIPPLE)
|
||||
CryptoType.UNKNOWN -> Triple("Unknown", R.drawable.ic_paid, CryptoType.UNKNOWN)
|
||||
}
|
||||
}
|
||||
|
||||
return BigButton(context, name, destination.takeIf { cryptoType != null } ?: "Donate on $name", iconDrawableId, action).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
};
|
||||
}
|
||||
|
||||
private fun setPromotions(url: String?, imageUrl: String?) {
|
||||
Logger.i(TAG, "setPromotions($url, $imageUrl)")
|
||||
|
||||
if (url != null) {
|
||||
_layoutPromotions.visibility = View.VISIBLE
|
||||
|
||||
if (imageUrl != null) {
|
||||
_buttonPromotion.visibility = View.GONE
|
||||
_imagePromotion.visibility = View.VISIBLE
|
||||
|
||||
Glide.with(_imagePromotion)
|
||||
.load(imageUrl)
|
||||
.crossfade()
|
||||
.into(_imagePromotion)
|
||||
} else {
|
||||
_buttonPromotion.setSecondaryText(url)
|
||||
_buttonPromotion.visibility = View.VISIBLE
|
||||
_imagePromotion.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
_layoutPromotions.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
||||
if (_polycentricProfile == profile) {
|
||||
return
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
setDonations(profile.systemState.donationDestinations);
|
||||
setMemberships(profile.systemState.membershipUrls);
|
||||
|
||||
val imageManifest = profile.systemState.promotionBanner?.imageManifestsList?.firstOrNull()
|
||||
if (imageManifest != null) {
|
||||
val imageUrl = imageManifest.toURLInfoSystemLinkUrl(profile.system.toProto(), imageManifest.process, profile.systemState.servers.toList());
|
||||
setPromotions(profile.systemState.promotion, imageUrl);
|
||||
} else {
|
||||
setPromotions(null, null);
|
||||
}
|
||||
|
||||
if (profile.systemState.store.isNotEmpty()) {
|
||||
_layoutStore.visibility = View.VISIBLE
|
||||
} else {
|
||||
_layoutStore.visibility = View.GONE
|
||||
}
|
||||
|
||||
_textNoSupportOptionsSet.visibility = View.GONE
|
||||
} else {
|
||||
setDonations(listOf());
|
||||
setMemberships(listOf());
|
||||
setPromotions(null, null);
|
||||
_layoutStore.visibility = View.GONE
|
||||
_textNoSupportOptionsSet.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
_polycentricProfile = profile
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "SupportView";
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class PlaylistsViewHolder : ViewHolder {
|
||||
fun bind(p: Playlist) {
|
||||
if (p.videos.isNotEmpty()) {
|
||||
Glide.with(_imageThumbnail)
|
||||
.load(p.videos[0].thumbnails.getLQThumbnail())
|
||||
.load(p.videos[0].thumbnails.getMinimumThumbnail(380))
|
||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||
.crossfade()
|
||||
.into(_imageThumbnail);
|
||||
|
||||
+3
-1
@@ -11,6 +11,8 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
@@ -76,7 +78,7 @@ class VideoListEditorViewHolder : ViewHolder {
|
||||
|
||||
fun bind(v: IPlatformVideo, canEdit: Boolean) {
|
||||
Glide.with(_imageThumbnail)
|
||||
.load(v.thumbnails.getLQThumbnail())
|
||||
.load(v.thumbnails.getHQThumbnail())
|
||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||
.crossfade()
|
||||
.into(_imageThumbnail);
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.futo.platformplayer.views.adapters.viewholders
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
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.logging.Logger
|
||||
import com.futo.platformplayer.views.StoreItem
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
|
||||
class StoreItemViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<StoreItem>(
|
||||
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_store_item, _viewGroup, false)) {
|
||||
|
||||
private val _image: ShapeableImageView;
|
||||
private val _name: TextView;
|
||||
private var _storeItem: StoreItem? = null;
|
||||
|
||||
init {
|
||||
_image = _view.findViewById(R.id.image_item);
|
||||
_name = _view.findViewById(R.id.text_item);
|
||||
_view.findViewById<LinearLayout>(R.id.root).setOnClickListener {
|
||||
val s = _storeItem ?: return@setOnClickListener;
|
||||
|
||||
try {
|
||||
val uri = Uri.parse(s.url);
|
||||
val intent = Intent(Intent.ACTION_VIEW);
|
||||
intent.data = uri;
|
||||
_view.context.startActivity(intent);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to open URI: '${it}'.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(storeItem: StoreItem) {
|
||||
Glide.with(_image)
|
||||
.load(storeItem.image)
|
||||
.crossfade()
|
||||
.into(_image);
|
||||
|
||||
_name.text = storeItem.name;
|
||||
_storeItem = storeItem;
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "StoreItemViewHolder";
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,10 @@ open class BigButton : LinearLayout {
|
||||
_textSecondary.text = attrTextSecondary;
|
||||
}
|
||||
|
||||
fun setSecondaryText(text: String?) {
|
||||
_textSecondary.text = text
|
||||
}
|
||||
|
||||
fun withPrimaryText(text: String): BigButton {
|
||||
_textPrimary.text = text;
|
||||
return this;
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import java.lang.reflect.Field
|
||||
@@ -37,7 +38,7 @@ class ButtonField : BigButton, IField {
|
||||
//private val _title : TextView;
|
||||
//private val _subtitle : TextView;
|
||||
|
||||
override val onChanged = Event2<IField, Any>();
|
||||
override val onChanged = Event3<IField, Any, Any>();
|
||||
|
||||
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs){
|
||||
//inflate(context, R.layout.field_button, this);
|
||||
@@ -59,6 +60,8 @@ class ButtonField : BigButton, IField {
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValue(value: Any) {}
|
||||
|
||||
fun fromMethod(obj : Any, method: Method) : ButtonField {
|
||||
this._method = method;
|
||||
this._obj = obj;
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.view.View
|
||||
import android.widget.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import java.lang.reflect.Field
|
||||
|
||||
class DropdownField : TableRow, IField {
|
||||
@@ -35,7 +37,7 @@ class DropdownField : TableRow, IField {
|
||||
|
||||
override var reference: Any? = null;
|
||||
|
||||
override val onChanged = Event2<IField, Any>();
|
||||
override val onChanged = Event3<IField, Any, Any>();
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs){
|
||||
inflate(context, R.layout.field_dropdown, this);
|
||||
@@ -50,13 +52,21 @@ class DropdownField : TableRow, IField {
|
||||
_isInitFire = false;
|
||||
return;
|
||||
}
|
||||
Logger.i("DropdownField", "Changed: ${_selected} -> ${pos}");
|
||||
val old = _selected;
|
||||
_selected = pos;
|
||||
onChanged.emit(this@DropdownField, pos);
|
||||
onChanged.emit(this@DropdownField, pos, old);
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
};
|
||||
}
|
||||
|
||||
override fun setValue(value: Any) {
|
||||
if(value is Int) {
|
||||
_spinner.setSelection(value);
|
||||
}
|
||||
}
|
||||
|
||||
fun asBoolean(name: String, description: String?, obj: Boolean) : DropdownField {
|
||||
_options = resources.getStringArray(R.array.enabled_disabled_array);
|
||||
_spinner.adapter = ArrayAdapter<String>(context, R.layout.spinner_item_simple, _options).also {
|
||||
@@ -77,6 +87,23 @@ class DropdownField : TableRow, IField {
|
||||
return this;
|
||||
}
|
||||
|
||||
fun withValue(title: String, description: String?, options: List<String>, value: Int): DropdownField {
|
||||
_title.text = title;
|
||||
_description.visibility = if(description.isNullOrEmpty()) View.GONE else View.VISIBLE;
|
||||
_description.text = description ?: "";
|
||||
|
||||
_options = options.toTypedArray();
|
||||
_spinner.adapter = ArrayAdapter<String>(context, R.layout.spinner_item_simple, _options).also {
|
||||
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||
};
|
||||
|
||||
_selected = value;
|
||||
_spinner.isSelected = false;
|
||||
_spinner.setSelection(_selected, true);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
override fun fromField(obj: Any, field: Field, formField: FormField?) : DropdownField {
|
||||
this._field = field;
|
||||
this._obj = obj;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.futo.platformplayer.views.fields
|
||||
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import java.lang.reflect.Field
|
||||
|
||||
|
||||
@@ -13,11 +14,12 @@ interface IField {
|
||||
val obj : Any?;
|
||||
val field : Field?;
|
||||
|
||||
val onChanged : Event2<IField, Any>;
|
||||
val onChanged : Event3<IField, Any, Any>;
|
||||
|
||||
var reference: Any?;
|
||||
|
||||
|
||||
fun fromField(obj : Any, field : Field, formField: FormField? = null) : IField;
|
||||
fun setField();
|
||||
|
||||
fun setValue(value: Any);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -49,7 +50,7 @@ class FieldForm : LinearLayout {
|
||||
throw java.lang.IllegalStateException("Only views can be IFields");
|
||||
|
||||
_root.addView(field as View);
|
||||
field.onChanged.subscribe { a1, a2 ->
|
||||
field.onChanged.subscribe { a1, a2, oldValue ->
|
||||
onChanged.emit(a1, a2);
|
||||
};
|
||||
}
|
||||
@@ -67,7 +68,7 @@ class FieldForm : LinearLayout {
|
||||
throw java.lang.IllegalStateException("Only views can be IFields");
|
||||
|
||||
_root.addView(field as View);
|
||||
field.onChanged.subscribe { a1, a2 ->
|
||||
field.onChanged.subscribe { a1, a2, oldValue ->
|
||||
onChanged.emit(a1, a2);
|
||||
};
|
||||
}
|
||||
@@ -82,25 +83,59 @@ class FieldForm : LinearLayout {
|
||||
|
||||
if(groupTitle == null) {
|
||||
for(field in newFields) {
|
||||
if(field !is View)
|
||||
if(field.second !is View)
|
||||
throw java.lang.IllegalStateException("Only views can be IFields");
|
||||
field.onChanged.subscribe { field, value ->
|
||||
onChanged.emit(field, value);
|
||||
}
|
||||
finalizePluginSettingField(field.first, field.second, newFields);
|
||||
_root.addView(field as View);
|
||||
}
|
||||
_fields = newFields;
|
||||
_fields = newFields.map { it.second };
|
||||
} else {
|
||||
for(field in newFields) {
|
||||
field.onChanged.subscribe { field, value ->
|
||||
onChanged.emit(field, value);
|
||||
}
|
||||
finalizePluginSettingField(field.first, field.second, newFields);
|
||||
}
|
||||
val group = GroupField(context, groupTitle, groupDescription)
|
||||
.withFields(newFields);
|
||||
.withFields(newFields.map { it.second });
|
||||
_root.addView(group as View);
|
||||
}
|
||||
}
|
||||
private fun finalizePluginSettingField(setting: SourcePluginConfig.Setting, field: IField, others: List<Pair<SourcePluginConfig.Setting, IField>>) {
|
||||
field.onChanged.subscribe { field, value, oldValue ->
|
||||
onChanged.emit(field, value);
|
||||
|
||||
setting.warningDialog?.let {
|
||||
if(it.isNotBlank() && isValueTrue(value))
|
||||
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, setting.warningDialog, null, null, 0,
|
||||
UIDialogs.Action("Cancel", {
|
||||
field.setValue(oldValue);
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Ok", {
|
||||
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}
|
||||
}
|
||||
if(setting.dependency != null) {
|
||||
val dependentField = others.firstOrNull { it.first.variableOrName == setting.dependency };
|
||||
if(dependentField == null || dependentField.second !is View)
|
||||
(field as View).visibility = View.GONE;
|
||||
else {
|
||||
dependentField.second.onChanged.subscribe { dependentField, value, oldValue ->
|
||||
val isValid = isValueTrue(value);
|
||||
if(isValid)
|
||||
(field as View).visibility = View.VISIBLE;
|
||||
else
|
||||
(field as View).visibility = View.GONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun isValueTrue(value: Any): Boolean {
|
||||
return when(value) {
|
||||
is Int -> value > 0;
|
||||
is Boolean -> value;
|
||||
is String -> value.toIntOrNull()?.let { it > 0 } ?: false || value.lowercase() == "true";
|
||||
else -> false
|
||||
};
|
||||
}
|
||||
|
||||
fun setObjectValues(){
|
||||
val fields = _fields;
|
||||
@@ -133,26 +168,42 @@ class FieldForm : LinearLayout {
|
||||
private val _json = Json {};
|
||||
|
||||
|
||||
fun getFieldsFromPluginSettings(context: Context, settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>): List<IField> {
|
||||
val fields = mutableListOf<IField>()
|
||||
fun getFieldsFromPluginSettings(context: Context, settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>): List<Pair<SourcePluginConfig.Setting, IField>> {
|
||||
val fields = mutableListOf<Pair<SourcePluginConfig.Setting, IField>>()
|
||||
|
||||
for(setting in settings) {
|
||||
val value = if(values.containsKey(setting.variableOrName)) values[setting.variableOrName] else setting.default;
|
||||
|
||||
val field = when(setting.type.lowercase()) {
|
||||
"header" -> {
|
||||
val groupField = GroupField(context, setting.name, setting.description);
|
||||
groupField;
|
||||
}
|
||||
"boolean" -> {
|
||||
val value = if(values.containsKey(setting.variableOrName)) values[setting.variableOrName] else setting.default;
|
||||
val field = ToggleField(context).withValue(setting.name,
|
||||
setting.description,
|
||||
value == "true" || value == "1" || value == "True");
|
||||
field.onChanged.subscribe { field, value ->
|
||||
field.onChanged.subscribe { field, value, oldValue ->
|
||||
values[setting.variableOrName] = _json.encodeToString (value == 1 || value == true);
|
||||
}
|
||||
field;
|
||||
}
|
||||
"dropdown" -> {
|
||||
if(setting.options != null && !setting.options.isEmpty()) {
|
||||
var selected = value?.toIntOrNull()?.coerceAtLeast(0) ?: 0;
|
||||
val field = DropdownField(context).withValue(setting.name, setting.description, setting.options, selected);
|
||||
field.onChanged.subscribe { field, value, oldValue ->
|
||||
values[setting.variableOrName] = value.toString();
|
||||
}
|
||||
field;
|
||||
}
|
||||
else null;
|
||||
}
|
||||
else -> null;
|
||||
}
|
||||
|
||||
if(field != null)
|
||||
fields.add(field);
|
||||
fields.add(Pair(setting, field));
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import java.lang.reflect.Field
|
||||
|
||||
class GroupField : LinearLayout, IField {
|
||||
@@ -27,7 +28,7 @@ class GroupField : LinearLayout, IField {
|
||||
return _field;
|
||||
};
|
||||
|
||||
override val onChanged = Event2<IField, Any>();
|
||||
override val onChanged = Event3<IField, Any, Any>();
|
||||
|
||||
private val _title : TextView;
|
||||
private val _subtitle : TextView;
|
||||
@@ -138,4 +139,6 @@ class GroupField : LinearLayout, IField {
|
||||
field.setField();
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValue(value: Any) {}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.util.AttributeSet
|
||||
import android.widget.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
|
||||
@@ -27,7 +28,7 @@ class ReadOnlyTextField : TableRow, IField {
|
||||
private val _title : TextView;
|
||||
private val _value : TextView;
|
||||
|
||||
override val onChanged = Event2<IField, Any>();
|
||||
override val onChanged = Event3<IField, Any, Any>();
|
||||
|
||||
override var reference: Any? = null;
|
||||
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs){
|
||||
@@ -36,6 +37,8 @@ class ReadOnlyTextField : TableRow, IField {
|
||||
_value = findViewById(R.id.field_value);
|
||||
}
|
||||
|
||||
override fun setValue(value: Any) {}
|
||||
|
||||
override fun fromField(obj : Any, field : Field, formField: FormField?) : ReadOnlyTextField {
|
||||
this._field = field;
|
||||
this._obj = obj;
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.view.View
|
||||
import android.widget.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.others.Toggle
|
||||
import java.lang.reflect.Field
|
||||
|
||||
@@ -28,10 +30,11 @@ class ToggleField : TableRow, IField {
|
||||
private val _title : TextView;
|
||||
private val _description : TextView;
|
||||
private val _toggle : Toggle;
|
||||
private var _lastValue: Boolean = false;
|
||||
|
||||
override var reference: Any? = null;
|
||||
|
||||
override val onChanged = Event2<IField, Any>();
|
||||
override val onChanged = Event3<IField, Any, Any>();
|
||||
|
||||
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs){
|
||||
inflate(context, R.layout.field_toggle, this);
|
||||
@@ -40,10 +43,18 @@ class ToggleField : TableRow, IField {
|
||||
_description = findViewById(R.id.field_description);
|
||||
|
||||
_toggle.onValueChanged.subscribe {
|
||||
onChanged.emit(this, it);
|
||||
val lastVal = _lastValue;
|
||||
Logger.i("ToggleField", "Changed: ${lastVal} -> ${it}");
|
||||
_lastValue = it;
|
||||
onChanged.emit(this, it, lastVal);
|
||||
};
|
||||
}
|
||||
|
||||
override fun setValue(value: Any) {
|
||||
if(value is Boolean)
|
||||
_toggle.setValue(value, true, true);
|
||||
}
|
||||
|
||||
fun withValue(title: String, description: String?, value: Boolean): ToggleField {
|
||||
|
||||
_title.text = title;
|
||||
@@ -54,6 +65,7 @@ class ToggleField : TableRow, IField {
|
||||
_description.visibility = View.GONE;
|
||||
|
||||
_toggle.setValue(value, true);
|
||||
_lastValue = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -78,14 +90,16 @@ class ToggleField : TableRow, IField {
|
||||
}
|
||||
|
||||
val value = field.get(obj);
|
||||
if(value is Boolean)
|
||||
_toggle.setValue(value, true);
|
||||
val toggleValue = if(value is Boolean)
|
||||
value;
|
||||
else if(value is Number)
|
||||
_toggle.setValue((value as Number).toInt() > 0, true);
|
||||
(value as Number).toInt() > 0;
|
||||
else if(value == null)
|
||||
_toggle.setValue(false, true);
|
||||
false;
|
||||
else
|
||||
_toggle.setValue(false, true);
|
||||
false;
|
||||
_toggle.setValue(toggleValue, true);
|
||||
_lastValue = toggleValue;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class Toggle : AppCompatImageView {
|
||||
scaleType = ScaleType.FIT_CENTER;
|
||||
}
|
||||
|
||||
fun setValue(v: Boolean, animated: Boolean = true) {
|
||||
fun setValue(v: Boolean, animated: Boolean = true, withEvent: Boolean = false) {
|
||||
if (value == v) {
|
||||
return;
|
||||
}
|
||||
@@ -44,5 +44,8 @@ class Toggle : AppCompatImageView {
|
||||
} else {
|
||||
setImageResource(if (v) R.drawable.toggle_enabled else R.drawable.toggle_disabled);
|
||||
}
|
||||
|
||||
if(withEvent)
|
||||
onValueChanged.emit(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.views.SupportView
|
||||
|
||||
class SupportOverlay : LinearLayout {
|
||||
val onClose = Event0();
|
||||
|
||||
private val _topbar: OverlayTopbar;
|
||||
private val _support: SupportView;
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.overlay_support, this)
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_support = findViewById(R.id.support);
|
||||
|
||||
_topbar.onClose.subscribe(this, onClose::emit);
|
||||
}
|
||||
|
||||
|
||||
fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
||||
_support.setPolycentricProfile(profile, animate)
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
_topbar.onClose.remove(this);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,8 @@ class CommentsList : ConstraintLayout {
|
||||
}
|
||||
.exception<Throwable> {
|
||||
Logger.e(TAG, "Failed to load comments.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_comments) + (it.message ?: ""), it, ::fetchComments);
|
||||
UIDialogs.toast(context, context.getString(R.string.failed_to_load_comments) + "\n" + (it.message ?: ""));
|
||||
//UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_comments) + (it.message ?: ""), it, ::fetchComments);
|
||||
setLoading(false);
|
||||
} else TaskHandler(IPlatformVideoDetails::class.java, StateApp.instance.scopeGetter);
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
private val _control_videosettings_fullscreen: ImageButton;
|
||||
private val _control_minimize_fullscreen: ImageButton;
|
||||
private val _control_rotate_lock_fullscreen: ImageButton;
|
||||
private val _control_cast_fullscreen: ImageButton;
|
||||
private val _control_play_fullscreen: ImageButton;
|
||||
private val _time_bar_fullscreen: TimeBar;
|
||||
private val _overlay_brightness: FrameLayout;
|
||||
@@ -127,10 +128,15 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_control_minimize_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_minimize);
|
||||
_control_videosettings_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_settings);
|
||||
_control_rotate_lock_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_rotate_lock);
|
||||
_control_cast_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_cast);
|
||||
_control_play_fullscreen = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play);
|
||||
_control_chapter_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_chapter_current);
|
||||
_time_bar_fullscreen = _videoControls_fullscreen.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress);
|
||||
|
||||
val castVisibility = if (Settings.instance.casting.enabled) View.VISIBLE else View.GONE
|
||||
_control_cast.visibility = castVisibility
|
||||
_control_cast_fullscreen.visibility = castVisibility
|
||||
|
||||
_overlay_brightness = findViewById(R.id.overlay_brightness);
|
||||
|
||||
_title_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_title);
|
||||
@@ -213,7 +219,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
updateRotateLock();
|
||||
};
|
||||
_control_cast.setOnClickListener {
|
||||
UIDialogs.showCastingDialog(context);
|
||||
|
||||
};
|
||||
|
||||
_control_minimize_fullscreen.setOnClickListener {
|
||||
@@ -229,6 +235,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
StatePlayer.instance.rotationLock = !StatePlayer.instance.rotationLock;
|
||||
updateRotateLock();
|
||||
};
|
||||
_control_cast_fullscreen.setOnClickListener {
|
||||
UIDialogs.showCastingDialog(context);
|
||||
};
|
||||
|
||||
var lastPos = 0L;
|
||||
videoControls.setProgressUpdateListener { position, bufferedPosition ->
|
||||
@@ -270,7 +279,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
if (drawable != null) {
|
||||
_videoView.defaultArtwork = drawable;
|
||||
_videoView.useArtwork = true;
|
||||
fitHeight();
|
||||
fitOrFill(isFullScreen);
|
||||
} else {
|
||||
_videoView.defaultArtwork = null;
|
||||
_videoView.useArtwork = false;
|
||||
@@ -311,7 +320,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
gestureControl.hideControls();
|
||||
//videoControlsBar.visibility = View.GONE;
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
fillHeight();
|
||||
|
||||
_videoControls_fullscreen.show();
|
||||
videoControls.hide();
|
||||
}
|
||||
@@ -323,16 +332,25 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
gestureControl.hideControls();
|
||||
//videoControlsBar.visibility = View.VISIBLE;
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
fitHeight();
|
||||
|
||||
videoControls.show();
|
||||
_videoControls_fullscreen.hide();
|
||||
}
|
||||
|
||||
fitOrFill(fullScreen);
|
||||
gestureControl.setFullscreen(fullScreen);
|
||||
onToggleFullScreen.emit(fullScreen);
|
||||
isFullScreen = fullScreen;
|
||||
}
|
||||
|
||||
private fun fitOrFill(fullScreen: Boolean) {
|
||||
if (fullScreen) {
|
||||
fillHeight();
|
||||
} else {
|
||||
fitHeight();
|
||||
}
|
||||
}
|
||||
|
||||
fun lockControlsAlpha(locked : Boolean) {
|
||||
if(locked && _isControlsLocked != locked) {
|
||||
_isControlsLocked = locked;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#232323" />
|
||||
<corners android:radius="5dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#3A1448" />
|
||||
<corners android:radius="14dp" />
|
||||
<corners android:radius="5dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#144826" />
|
||||
<corners android:radius="14dp" />
|
||||
<corners android:radius="5dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="273.6dp"
|
||||
android:height="360dp"
|
||||
android:viewportWidth="273.6"
|
||||
android:viewportHeight="360">
|
||||
<path
|
||||
android:pathData="M217.02,167.04c18.63,-9.48 30.29,-26.18 27.57,-54.01c-3.67,-38.02 -36.53,-50.77 -78.01,-54.4l-0.01,-52.74h-32.14l-0.01,51.35c-8.46,0 -17.08,0.17 -25.66,0.34L108.76,5.9l-32.11,-0l-0.01,52.73c-6.96,0.14 -13.79,0.28 -20.47,0.28v-0.16l-44.33,-0.02l0.01,34.28c0,0 23.73,-0.45 23.34,-0.01c13.01,0.01 17.26,7.56 18.48,14.08l0.01,60.08v84.4c-0.57,4.09 -2.98,10.63 -12.08,10.64c0.41,0.36 -23.38,-0 -23.38,-0l-6.38,38.33h41.82c7.79,0.01 15.45,0.13 22.96,0.19l0.03,53.34l32.1,0.01l-0.01,-52.78c8.83,0.18 17.36,0.26 25.68,0.25l-0.01,52.53h32.14l0.02,-53.25c54.02,-3.1 91.84,-16.7 96.54,-67.39C266.92,192.61 247.69,174.4 217.02,167.04zM109.54,95.32c18.13,0 75.13,-5.77 75.14,32.06c-0.01,36.27 -57,32.03 -75.14,32.03V95.32zM109.52,262.45l0.01,-70.67c21.78,-0.01 90.08,-6.26 90.09,35.32C199.64,266.97 131.31,262.43 109.52,262.45z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size android:height="0dp"
|
||||
android:width="4dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size android:height="0dp"
|
||||
android:width="8dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size android:height="20dp"
|
||||
android:width="0dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size android:height="8dp"
|
||||
android:width="0dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M15.927,23.959l-9.823,-5.797 9.817,13.839 9.828,-13.839 -9.828,5.797zM16.073,0l-9.819,16.297 9.819,5.807 9.823,-5.801z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@@ -2,8 +2,7 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,760Q546,760 593,713Q640,666 640,600L640,440Q640,374 593,327Q546,280 480,280Q414,280 367,327Q320,374 320,440L320,600Q320,666 367,713Q414,760 480,760ZM400,640L560,640L560,560L400,560L400,640ZM400,480L560,480L560,400L400,400L400,480ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520ZM480,840Q415,840 359.5,808Q304,776 272,720L160,720L160,640L244,640Q241,620 240.5,600Q240,580 240,560L160,560L160,480L240,480Q240,460 240.5,440Q241,420 244,400L160,400L160,320L272,320Q286,297 303.5,277Q321,257 344,242L280,176L336,120L422,206Q450,197 479,197Q508,197 536,206L624,120L680,176L614,242Q637,257 655.5,276.5Q674,296 688,320L800,320L800,400L716,400Q719,420 719.5,440Q720,460 720,480L800,480L800,560L720,560Q720,580 719.5,600Q719,620 716,640L800,640L800,720L688,720Q656,776 600.5,808Q545,840 480,840Z"/>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M380,620L660,440L380,260L380,620ZM320,840L320,760L160,760Q127,760 103.5,736.5Q80,713 80,680L80,200Q80,167 103.5,143.5Q127,120 160,120L800,120Q833,120 856.5,143.5Q880,167 880,200L880,680Q880,713 856.5,736.5Q833,760 800,760L640,760L640,840L320,840ZM160,680L800,680Q800,680 800,680Q800,680 800,680L800,200Q800,200 800,200Q800,200 800,200L160,200Q160,200 160,200Q160,200 160,200L160,680Q160,680 160,680Q160,680 160,680ZM160,680Q160,680 160,680Q160,680 160,680L160,200Q160,200 160,200Q160,200 160,200L160,200Q160,200 160,200Q160,200 160,200L160,680Q160,680 160,680Q160,680 160,680L160,680Z"/>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M160,760L160,680L240,680L240,400Q240,317 290,252.5Q340,188 420,168L420,140Q420,115 437.5,97.5Q455,80 480,80Q505,80 522.5,97.5Q540,115 540,140L540,168Q620,188 670,252.5Q720,317 720,400L720,680L800,680L800,760L160,760ZM480,460L480,460L480,460L480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,880Q447,880 423.5,856.5Q400,833 400,800L560,800Q560,833 536.5,856.5Q513,880 480,880ZM320,680L640,680L640,400Q640,334 593,287Q546,240 480,240Q414,240 367,287Q320,334 320,400L320,680Z"/>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M293,796.92L342.61,584.39L177.69,441.54L394.92,422.69L480,222.31L565.08,422.69L782.31,441.54L617.39,584.39L667,796.92L480,684.08L293,796.92Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500L480,500Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#E4A72E"
|
||||
android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500L480,500Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="82.6dp"
|
||||
android:height="82.6dp"
|
||||
android:viewportWidth="82.6"
|
||||
android:viewportHeight="82.6">
|
||||
<path
|
||||
android:pathData="M41.3,0A41.3,41.3 0,1 0,82.6 41.3h0A41.18,41.18 0,0 0,41.54 0ZM42,42.7 L37.7,57.2h23a1.16,1.16 0,0 1,1.2 1.12v0.38l-2,6.9a1.49,1.49 0,0 1,-1.5 1.1H23.2l5.9,-20.1 -6.6,2L24,44l6.6,-2 8.3,-28.2a1.51,1.51 0,0 1,1.5 -1.1h8.9a1.16,1.16 0,0 1,1.2 1.12v0.38L43.5,38l6.6,-2 -1.4,4.8Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="180dp"
|
||||
android:height="180dp"
|
||||
android:viewportWidth="180"
|
||||
android:viewportHeight="180">
|
||||
<path
|
||||
android:pathData="M108.81,26.07c-26.47,0 -48,21.53 -48,48 0,26.39 21.53,47.85 48,47.85 26.39,0 47.85,-21.47 47.85,-47.85 0,-26.47 -21.47,-48 -47.85,-48"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M23.33,153.93V26.07h23.47v127.87z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,26H7c-0.8,0 -1.6,-0.4 -2.1,-1c-0.5,-0.6 -0.7,-1.4 -0.5,-2.2L8.7,4l0,0c0.3,-1.2 1.3,-2 2.6,-2h8.6c2.3,0 4.4,1 5.9,2.8c1.4,1.8 2,4.1 1.5,6.4c-0.8,4 -4.4,6.8 -8.5,6.8h-3.2c-0.5,0 -1,0.4 -1.1,0.9L13,25.2C12.9,25.7 12.5,26 12,26z"/>
|
||||
<path
|
||||
android:pathData="M12,26H7c-0.8,0 -1.6,-0.4 -2.1,-1c-0.5,-0.6 -0.7,-1.4 -0.5,-2.2L8.7,4l0,0c0.3,-1.2 1.3,-2 2.6,-2h8.6c2.3,0 4.4,1 5.9,2.8c1.4,1.8 2,4.1 1.5,6.4c-0.8,4 -4.4,6.8 -8.5,6.8h-3.2c-0.5,0 -1,0.4 -1.1,0.9L13,25.2C12.9,25.7 12.5,26 12,26z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M29.3,11.3c0,0.1 0,0.2 0,0.3c-1,4.9 -5.4,8.4 -10.4,8.4h-2.5l-1.4,5.7C14.6,27.1 13.4,28 12,28h-2c0.1,0.4 0.2,0.7 0.5,1c0.5,0.6 1.2,0.9 2,0.9H17c0.5,0 0.9,-0.3 1,-0.7l1.4,-5.5c0.1,-0.4 0.5,-0.6 0.9,-0.6h2.9c3.7,0 7,-2.5 7.7,-6C31.3,15 30.7,12.8 29.3,11.3z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M27.401,19.531c-1.131,-0.645 -2.407,-0.837 -3.672,-0.885 -1.052,-0.031 -2.631,-0.724 -2.631,-2.645 0,-1.432 1.156,-2.588 2.647,-2.645 1.265,-0.048 2.541,-0.24 3.671,-0.891 3.193,-1.844 4.292,-5.928 2.448,-9.125 -1.859,-3.199 -5.952,-4.287 -9.156,-2.437 -2.072,1.187 -3.348,3.401 -3.339,5.787 0,1.296 0.464,2.484 1.052,3.599 0.496,0.927 0.735,2.661 -0.948,3.635 -1.265,0.724 -2.843,0.272 -3.624,-0.989 -0.661,-1.068 -1.459,-2.063 -2.589,-2.708 -3.197,-1.849 -7.291,-0.751 -9.124,2.437 -1.839,3.199 -0.745,7.281 2.452,9.125 2.068,1.187 4.609,1.187 6.677,0 1.125,-0.647 1.923,-1.641 2.584,-2.708 0.541,-0.871 1.911,-1.985 3.624,-0.991 1.267,0.719 1.657,2.319 0.948,3.641 -0.583,1.093 -1.052,2.297 -1.052,3.593 0,3.688 2.991,6.672 6.677,6.677 3.688,0 6.672,-2.989 6.677,-6.677 0.011,-2.385 -1.255,-4.599 -3.323,-5.792z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@@ -1,11 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:background="@color/black">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/header"
|
||||
android:background="#000000"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp">
|
||||
<ImageButton
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:src="@drawable/ic_close" />
|
||||
<TextView
|
||||
android:id="@+id/text_url"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_close"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<WebView
|
||||
android:id="@+id/web_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/header"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,92 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="18dp">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_store"
|
||||
<com.futo.platformplayer.views.SupportView
|
||||
android:id="@+id/support"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonIcon="@drawable/ic_store"
|
||||
app:buttonText="@string/store"
|
||||
app:buttonSubText="@string/visit_my_store" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="20dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/memberships" />
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:flexWrap="wrap"
|
||||
android:layout_marginTop="5dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:textColor="#909090"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/a_monthly_recurring_payment_with_often" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:textColor="#909090"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:text="@string/additional_perks" />
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/donation"
|
||||
android:layout_marginTop="20dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:textColor="#909090"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/a_one_time_payment_to_support_the_creator" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
@@ -40,6 +40,19 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_load_more"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textSize="15dp"
|
||||
android:text="@string/load_more"
|
||||
android:textColor="@color/colorPrimary" />
|
||||
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_select_counter"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -445,65 +445,10 @@
|
||||
android:text="@string/click_to_read_more"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_monetization"
|
||||
<com.futo.platformplayer.views.MonetizationView
|
||||
android:id="@+id/monetization"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:layout_marginTop="14dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_support"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/background_support">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:src="@drawable/ic_paid" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/support"
|
||||
android:textSize="14dp"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_marginStart="6dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_store"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/background_store">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:src="@drawable/ic_store" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
tools:text="Store"
|
||||
android:textSize="14dp"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_marginStart="6dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.futo.platformplayer.views.videometa.UpNextView
|
||||
android:id="@+id/up_next"
|
||||
@@ -602,6 +547,12 @@
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.futo.platformplayer.views.overlays.SupportOverlay
|
||||
android:id="@+id/videodetail_container_support"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
<FrameLayout
|
||||
android:id="@+id/videodetail_loading_overlay"
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image_video_thumbnail"
|
||||
android:layout_height="60dp"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_width="50dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:shapeAppearanceOverlay="@style/roundedCorners_4dp"
|
||||
app:srcCompat="@drawable/placeholder_video_thumbnail"
|
||||
android:background="@drawable/video_thumbnail_outline"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent" />
|
||||
|
||||
@@ -48,9 +49,10 @@
|
||||
android:fontFamily="@font/inter_extra_light"
|
||||
tools:text="3 videos"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginStart="10dp" />
|
||||
|
||||
<!--
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.futo.platformplayer.views.overlays.OverlayTopbar
|
||||
android:id="@+id/topbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
app:title="@string/support"
|
||||
app:metadata=""
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<com.futo.platformplayer.views.SupportView
|
||||
android:id="@+id/support"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -57,6 +57,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
<ImageButton
|
||||
android:id="@+id/exo_cast"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:clickable="true"
|
||||
android:padding="12dp"
|
||||
app:srcCompat="@drawable/ic_cast" />
|
||||
<ImageButton
|
||||
android:id="@+id/exo_rotate_lock"
|
||||
android:layout_width="50dp"
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/root">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:layout_marginTop="14dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:showDividers="middle"
|
||||
android:divider="@drawable/divider_transparent_4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_membership"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/background_membership">
|
||||
|
||||
<com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
android:id="@+id/membership_platform"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/membership"
|
||||
android:textSize="14dp"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_marginStart="6dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_support"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/background_support">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:src="@drawable/ic_paid" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/support"
|
||||
android:textSize="14dp"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_marginStart="6dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_store"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/background_store">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:src="@drawable/ic_store" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/store"
|
||||
android:textSize="14dp"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_marginStart="6dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_merchandise"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:fontFamily="@font/inter_medium"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="17dp"
|
||||
android:text="@string/merchandise" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_merchandise"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="140dp"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_merchandise"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<com.futo.platformplayer.views.Loader
|
||||
android:id="@+id/loader_merchandise"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_gravity="center"/>
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,32 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:clickable="true"
|
||||
android:id="@+id/root"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image_item"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:shapeAppearanceOverlay="@style/roundedCorners_10dp"
|
||||
android:background="#E9E9E9"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_item"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
tools:text="BEAST ORIGINALS HOODIE - BLACK"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="#888888"
|
||||
android:textSize="10dp"
|
||||
android:maxLines="2"
|
||||
android:layout_marginTop="8dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,191 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="18dp"
|
||||
android:showDividers="middle"
|
||||
android:divider="@drawable/divider_transparent_vertical_20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_no_support_options_set"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13dp"
|
||||
android:textColor="#909090"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/this_creator_has_not_set_any_support_options_on_harbor_polycentric" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_store"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/store"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13dp"
|
||||
android:textColor="#909090"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/a_store_by_the_creator" />
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_store"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonIcon="@drawable/ic_store"
|
||||
app:buttonText="@string/store"
|
||||
app:buttonSubText="@string/visit_my_store"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_memberships"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/memberships" />
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:flexWrap="wrap"
|
||||
android:layout_marginTop="5dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13dp"
|
||||
android:textColor="#909090"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/a_monthly_recurring_payment_with_often" />
|
||||
|
||||
<Space android:layout_width="4dp"
|
||||
android:layout_height="match_parent"></Space>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13dp"
|
||||
android:textColor="#909090"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:text="@string/additional_perks" />
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_membership_entries"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle"
|
||||
android:divider="@drawable/divider_transparent_vertical_8dp">
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_promotions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/promotions" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13dp"
|
||||
android:textColor="#909090"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/current_promotions_by_this_creator" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_promotion_entries"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_promotion"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonIcon="@drawable/ic_star"
|
||||
app:buttonText="Promotion"
|
||||
app:buttonSubText="URL"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image_promotion"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="100dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/thumbnail"
|
||||
app:shapeAppearanceOverlay="@style/roundedCorners_10dp"
|
||||
app:srcCompat="@drawable/placeholder_video_thumbnail" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_donation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/donation" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13dp"
|
||||
android:textColor="#909090"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/a_one_time_payment_to_support_the_creator" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_donation_entries"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle"
|
||||
android:divider="@drawable/divider_transparent_vertical_8dp">
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -682,6 +682,10 @@
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>الاسم تصاعدياً</item>
|
||||
<item>الاسم تنازلياً</item>
|
||||
<item>المشاهدات تصاعدياً</item>
|
||||
<item>المشاهدات تنازلياً</item>
|
||||
<item>زمن المشاهدة تصاعدياً</item>
|
||||
<item>زمن المشاهدة تنازلياً</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>معاينة</item>
|
||||
@@ -697,7 +701,7 @@
|
||||
<item>استئناف بعد 10 ثوان</item>
|
||||
<item>استئناف دائم</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>الإنجليزية</item>
|
||||
<item>الإسبانية</item>
|
||||
<item>الفرنسية</item>
|
||||
|
||||
@@ -682,6 +682,10 @@
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>Name aufsteigend</item>
|
||||
<item>Name absteigend</item>
|
||||
<item>Aufrufe aufsteigend</item>
|
||||
<item>Aufrufe absteigend</item>
|
||||
<item>Wiedergabezeit aufsteigend</item>
|
||||
<item>Wiedergabezeit absteigend</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>Vorschau</item>
|
||||
@@ -697,7 +701,7 @@
|
||||
<item>Nach 10 Sekunden fortsetzen</item>
|
||||
<item>Immer fortsetzen</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Englisch</item>
|
||||
<item>Spanisch</item>
|
||||
<item>Französisch</item>
|
||||
|
||||
@@ -698,6 +698,10 @@
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>Nombre Ascendente</item>
|
||||
<item>Nombre Descendente</item>
|
||||
<item>Visitas Ascendente</item>
|
||||
<item>Visitas Descendente</item>
|
||||
<item>Tiempo de Visualización Ascendente</item>
|
||||
<item>Tiempo de Visualización Descendente</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>Vista Previa</item>
|
||||
@@ -713,7 +717,7 @@
|
||||
<item>Reanudar Después de 10s</item>
|
||||
<item>Siempre Reanudar</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Inglés</item>
|
||||
<item>Español</item>
|
||||
<item>Francés</item>
|
||||
|
||||
@@ -680,8 +680,12 @@
|
||||
<item>Activé</item>
|
||||
</string-array>
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>Nom croissant</item>
|
||||
<item>Nom décroissant</item>
|
||||
<item>Nom Ascendant</item>
|
||||
<item>Nom Descendant</item>
|
||||
<item>Vues Ascendantes</item>
|
||||
<item>Vues Descendantes</item>
|
||||
<item>Temps de visionnage Ascendant</item>
|
||||
<item>Temps de visionnage Descendant</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>Aperçu</item>
|
||||
@@ -697,7 +701,7 @@
|
||||
<item>Reprendre après 10s</item>
|
||||
<item>Toujours reprendre</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Anglais</item>
|
||||
<item>Espagnol</item>
|
||||
<item>Français</item>
|
||||
|
||||
@@ -680,8 +680,12 @@
|
||||
<item>有効</item>
|
||||
</string-array>
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>名前昇順</item>
|
||||
<item>名前降順</item>
|
||||
<item>名前の昇順</item>
|
||||
<item>名前の降順</item>
|
||||
<item>視聴回数の昇順</item>
|
||||
<item>視聴回数の降順</item>
|
||||
<item>視聴時間の昇順</item>
|
||||
<item>視聴時間の降順</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>プレビュー</item>
|
||||
@@ -697,7 +701,7 @@
|
||||
<item>10秒後から再開</item>
|
||||
<item>常に再開</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>英語</item>
|
||||
<item>スペイン語</item>
|
||||
<item>フランス語</item>
|
||||
|
||||
@@ -682,6 +682,10 @@
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>이름 오름차순</item>
|
||||
<item>이름 내림차순</item>
|
||||
<item>조회수 오름차순</item>
|
||||
<item>조회수 내림차순</item>
|
||||
<item>시청시간 오름차순</item>
|
||||
<item>시청시간 내림차순</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>미리보기</item>
|
||||
@@ -697,7 +701,7 @@
|
||||
<item>10초 후에 이어서</item>
|
||||
<item>항상 이어서</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>영어</item>
|
||||
<item>스페인어</item>
|
||||
<item>프랑스어</item>
|
||||
|
||||
@@ -682,6 +682,10 @@
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>Nome Ascendente</item>
|
||||
<item>Nome Descendente</item>
|
||||
<item>Visualizações Ascendente</item>
|
||||
<item>Visualizações Descendente</item>
|
||||
<item>Tempo de Assistência Ascendente</item>
|
||||
<item>Tempo de Assistência Descendente</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>Pré-visualização</item>
|
||||
@@ -697,7 +701,7 @@
|
||||
<item>Continuar Após 10s</item>
|
||||
<item>Sempre Continuar</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Inglês</item>
|
||||
<item>Espanhol</item>
|
||||
<item>Francês</item>
|
||||
|
||||
@@ -680,8 +680,12 @@
|
||||
<item>Включено</item>
|
||||
</string-array>
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>Имя по возрастанию</item>
|
||||
<item>Имя по убыванию</item>
|
||||
<item>По имени по возрастанию</item>
|
||||
<item>По имени по убыванию</item>
|
||||
<item>По количеству просмотров по возрастанию</item>
|
||||
<item>По количеству просмотров по убыванию</item>
|
||||
<item>По времени просмотра по возрастанию</item>
|
||||
<item>По времени просмотра по убыванию</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>Предпросмотр</item>
|
||||
@@ -697,7 +701,7 @@
|
||||
<item>Продолжить после 10 секунд</item>
|
||||
<item>Всегда продолжать</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Английский</item>
|
||||
<item>Испанский</item>
|
||||
<item>Французский</item>
|
||||
|
||||
@@ -682,6 +682,10 @@
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>名称升序</item>
|
||||
<item>名称降序</item>
|
||||
<item>观看次数升序</item>
|
||||
<item>观看次数降序</item>
|
||||
<item>观看时间升序</item>
|
||||
<item>观看时间降序</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>预览</item>
|
||||
@@ -697,7 +701,7 @@
|
||||
<item>预览后10秒继续</item>
|
||||
<item>始终继续</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>英语</item>
|
||||
<item>西班牙语</item>
|
||||
<item>法语</item>
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
<string name="developer">Developer</string>
|
||||
<string name="remove_historical_suggestion">Remove historical suggestion</string>
|
||||
<string name="comments">Comments</string>
|
||||
<string name="merchandise">Merchandise</string>
|
||||
<string name="reached_the_end_of_the_playlist">Reached the end of the playlist</string>
|
||||
<string name="the_playlist_will_restart_after_the_video_is_finished">The playlist will restart after the video is finished</string>
|
||||
<string name="restart_now">Restart Now</string>
|
||||
@@ -192,15 +193,19 @@
|
||||
<string name="i_already_paid">I Already Paid</string>
|
||||
<string name="memberships">Memberships</string>
|
||||
<string name="a_monthly_recurring_payment_with_often">A monthly recurring payment with often</string>
|
||||
<string name="additional_perks">additional perks.</string>
|
||||
<string name="additional_perks">additional perks</string>
|
||||
<string name="a_one_time_payment_to_support_the_creator">A one-time payment to support the creator</string>
|
||||
<string name="a_store_by_the_creator">A store by the creator</string>
|
||||
<string name="donation">Donation</string>
|
||||
<string name="promotions">Promotions</string>
|
||||
<string name="current_promotions_by_this_creator">Current promotions by this creator</string>
|
||||
<string name="downloading">Downloading</string>
|
||||
<string name="videos">Videos</string>
|
||||
<string name="clear_history">Clear history</string>
|
||||
<string name="nothing_to_import">Nothing to import</string>
|
||||
<string name="enabling_lots_of_sources_can_reduce_the_loading_speed_of_your_application">Enabling lots of sources can reduce the loading speed of your application.</string>
|
||||
<string name="support">Support</string>
|
||||
<string name="membership">Membership</string>
|
||||
<string name="store">Store</string>
|
||||
<string name="live_chat">Live Chat</string>
|
||||
<string name="remove">Remove</string>
|
||||
@@ -307,11 +312,17 @@
|
||||
<string name="import_data_description">Select a file to import, support various files (alternative to opening directly)</string>
|
||||
<string name="external_storage">External Storage</string>
|
||||
<string name="feed_style">Feed Style</string>
|
||||
<string name="app_language">App Language</string>
|
||||
<string name="may_require_restart">May require restart</string>
|
||||
<string name="fetch_on_app_boot">Fetch on app boot</string>
|
||||
<string name="get_answers_to_common_questions">Get answers to common questions</string>
|
||||
<string name="give_feedback_on_the_application">Give feedback on the application</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="live_chat_webview">Live Chat Webview</string>
|
||||
<string name="background_switch_audio">Switch to Audio in Background</string>
|
||||
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
|
||||
<string name="preview_feed_items">Preview Feed Items</string>
|
||||
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
|
||||
<string name="log_level">Log Level</string>
|
||||
<string name="logging">Logging</string>
|
||||
<string name="manage_polycentric_identity">Manage Polycentric identity</string>
|
||||
@@ -625,6 +636,9 @@
|
||||
<string name="select_your_pins_in_order">Select your pins in order</string>
|
||||
<string name="more_options">More Options</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="this_creator_has_not_set_any_support_options_on_harbor_polycentric">This creator has not set any support options on Harbor (Polycentric)</string>
|
||||
<string name="load_more">Load More</string>
|
||||
<string name="stopped_after_requestcount_to_avoid_rate_limit_click_load_more_to_load_more">Stopped after {requestCount} to avoid rate limit, click load more to load more.</string>
|
||||
<string-array name="home_screen_array">
|
||||
<item>Recommendations</item>
|
||||
<item>Subscriptions</item>
|
||||
@@ -724,6 +738,19 @@
|
||||
<item>Preview</item>
|
||||
<item>List</item>
|
||||
</string-array>
|
||||
<string-array name="app_languages">
|
||||
<item>System</item>
|
||||
<item>English (EN)</item>
|
||||
<item>German (DE)</item>
|
||||
<item>Spanish (ES)</item>
|
||||
<item>Portuguese (PT)</item>
|
||||
<item>French (FR)</item>
|
||||
<item>Japanese (JA)</item>
|
||||
<item>Korean (KO)</item>
|
||||
<item>Chinese (ZH)</item>
|
||||
<item>Russian (RU)</item>
|
||||
<item>Arabic (AR)</item>
|
||||
</string-array>
|
||||
<string-array name="player_background_behavior">
|
||||
<item>None</item>
|
||||
<item>Keep Playing</item>
|
||||
@@ -734,7 +761,7 @@
|
||||
<item>Resume After 10s</item>
|
||||
<item>Always Resume</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>English</item>
|
||||
<item>Spanish</item>
|
||||
<item>French</item>
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">4dp</item>
|
||||
</style>
|
||||
<style name="roundedCorners_10dp" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">10dp</item>
|
||||
</style>
|
||||
<style name="roundedCorners_16dp" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">16dp</item>
|
||||
|
||||
Submodule app/src/playstore/assets/sources/peertube updated: c46006bb65...cfabdc97ab
Submodule app/src/stable/assets/sources/kick updated: 63790c2dc8...d0b7a2c1b4
Submodule app/src/stable/assets/sources/nebula updated: dcc004d722...1b08b18c74
Submodule app/src/stable/assets/sources/odysee updated: b6db44bf3b...a8bc4ff913
Submodule app/src/stable/assets/sources/patreon updated: ecf4988b3f...9e26b7032e
Submodule app/src/stable/assets/sources/peertube updated: c46006bb65...cfabdc97ab
Submodule app/src/stable/assets/sources/rumble updated: 1b70c84e30...3aa9acaefe
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user