mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-26 01:35:20 +02:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44eacc2a47 | |||
| 8135d61398 | |||
| 66208f8265 | |||
| f52251e23a | |||
| dbea93efe5 | |||
| 3bf0740bd1 | |||
| fa7f1b11f3 | |||
| ff914bbdf4 | |||
| b822078d4b | |||
| 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 | |||
| d7f4dd65e8 | |||
| 599b119e62 | |||
| 41176464db | |||
| dd0ad19fb9 | |||
| 430625d2fb | |||
| 796cd1a776 | |||
| baa26af0c0 | |||
| ea0c27936e | |||
| 4aade35d19 | |||
| 251a5701af | |||
| 2da3116111 |
+29
-13
@@ -1,13 +1,14 @@
|
|||||||
package com.futo.platformplayer
|
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 junit.framework.TestCase.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class EncryptionProviderTests {
|
class GEncryptionProviderTests {
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptDecrypt() {
|
fun testEncryptDecryptV1() {
|
||||||
val encryptionProvider = EncryptionProvider.instance
|
val encryptionProvider = GEncryptionProviderV1.instance
|
||||||
val plaintext = "This is a test string."
|
val plaintext = "This is a test string."
|
||||||
|
|
||||||
// Encrypt the plaintext
|
// Encrypt the plaintext
|
||||||
@@ -22,8 +23,8 @@ class EncryptionProviderTests {
|
|||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptDecryptBytes() {
|
fun testEncryptDecryptBytesV1() {
|
||||||
val encryptionProvider = EncryptionProvider.instance
|
val encryptionProvider = GEncryptionProviderV1.instance
|
||||||
val bytes = "This is a test string.".toByteArray();
|
val bytes = "This is a test string.".toByteArray();
|
||||||
|
|
||||||
// Encrypt the plaintext
|
// Encrypt the plaintext
|
||||||
@@ -36,21 +37,36 @@ class EncryptionProviderTests {
|
|||||||
assertArrayEquals(bytes, decrypted);
|
assertArrayEquals(bytes, decrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptDecryptBytesPassword() {
|
fun testEncryptDecryptV0() {
|
||||||
val encryptionProvider = EncryptionProvider.instance
|
val encryptionProvider = GEncryptionProviderV0.instance
|
||||||
val bytes = "This is a test string.".toByteArray();
|
val plaintext = "This is a test string."
|
||||||
val password = "1234".padStart(32, '9');
|
|
||||||
|
|
||||||
// Encrypt the plaintext
|
// Encrypt the plaintext
|
||||||
val ciphertext = encryptionProvider.encrypt(bytes, password)
|
val ciphertext = encryptionProvider.encrypt(plaintext)
|
||||||
|
|
||||||
// Decrypt the ciphertext
|
// 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
|
// The decrypted string should be equal to the original plaintext
|
||||||
assertArrayEquals(bytes, decrypted);
|
assertArrayEquals(bytes, decrypted);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,26 @@
|
|||||||
<data android:host="*" />
|
<data android:host="*" />
|
||||||
<data android:scheme="file" />
|
<data android:scheme="file" />
|
||||||
|
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:scheme="content" />
|
||||||
|
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:scheme="file" />
|
||||||
|
|
||||||
<data android:mimeType="application/zip" />
|
<data android:mimeType="application/zip" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
|
|||||||
@@ -159,13 +159,27 @@ class FilterCapability {
|
|||||||
|
|
||||||
|
|
||||||
class PlatformAuthorLink {
|
class PlatformAuthorLink {
|
||||||
constructor(id, name, url, thumbnail, subscribers) {
|
constructor(id, name, url, thumbnail, subscribers, membershipUrl) {
|
||||||
this.id = id ?? PlatformID(); //PlatformID
|
this.id = id ?? PlatformID(); //PlatformID
|
||||||
this.name = name ?? ""; //string
|
this.name = name ?? ""; //string
|
||||||
this.url = url ?? ""; //string
|
this.url = url ?? ""; //string
|
||||||
this.thumbnail = thumbnail; //string
|
this.thumbnail = thumbnail; //string
|
||||||
if(subscribers)
|
if(subscribers)
|
||||||
this.subscribers = 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 {
|
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.*
|
import com.futo.platformplayer.activities.*
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
@@ -30,6 +28,7 @@ import kotlinx.serialization.*
|
|||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||||
@@ -46,7 +45,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
R.string.manage_polycentric_identity, FieldForm.BUTTON,
|
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)
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
@@ -61,7 +60,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
R.string.show_faq, FieldForm.BUTTON,
|
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)
|
@FormFieldButton(R.drawable.ic_quiz)
|
||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
@@ -74,7 +73,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
@FormField(
|
@FormField(
|
||||||
R.string.show_issues, FieldForm.BUTTON,
|
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)
|
@FormFieldButton(R.drawable.ic_data_alert)
|
||||||
fun openIssues() {
|
fun openIssues() {
|
||||||
@@ -109,7 +108,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
R.string.manage_tabs, FieldForm.BUTTON,
|
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)
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
@@ -122,11 +121,39 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.language, "group", -1, 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)
|
@FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 1)
|
||||||
var home = HomeSettings();
|
var home = HomeSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class HomeSettings {
|
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)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var homeFeedStyle: Int = 1;
|
var homeFeedStyle: Int = 1;
|
||||||
|
|
||||||
@@ -136,21 +163,39 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else
|
else
|
||||||
return FeedStyle.THUMBNAIL;
|
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.clear_hidden, FieldForm.BUTTON, R.string.clear_hidden_description, 7)
|
||||||
|
@FormFieldButton(R.drawable.ic_visibility_off)
|
||||||
|
fun clearHidden() {
|
||||||
|
StateMeta.instance.removeAllHiddenCreators();
|
||||||
|
StateMeta.instance.removeAllHiddenVideos();
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
UIDialogs.toast(it, "Creators and videos should show up again");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.search, "group", -1, 2)
|
@FormField(R.string.search, "group", -1, 2)
|
||||||
var search = SearchSettings();
|
var search = SearchSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class SearchSettings {
|
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)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var searchHistory: Boolean = true;
|
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)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var searchFeedStyle: Int = 1;
|
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 {
|
fun getSearchFeedStyle(): FeedStyle {
|
||||||
if(searchFeedStyle == 0)
|
if(searchFeedStyle == 0)
|
||||||
@@ -164,7 +209,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var subscriptions = SubscriptionsSettings();
|
var subscriptions = SubscriptionsSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class SubscriptionsSettings {
|
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)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var subscriptionsFeedStyle: Int = 1;
|
var subscriptionsFeedStyle: Int = 1;
|
||||||
|
|
||||||
@@ -175,10 +220,16 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return FeedStyle.THUMBNAIL;
|
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)
|
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 6)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var fetchOnAppBoot: Boolean = true;
|
var fetchOnAppBoot: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 6)
|
||||||
|
var fetchOnTabOpen: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 7)
|
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 7)
|
||||||
@DropdownFieldOptionsId(R.array.background_interval)
|
@DropdownFieldOptionsId(R.array.background_interval)
|
||||||
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
||||||
@@ -208,6 +259,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 10)
|
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 10)
|
||||||
var allowPlaytimeTracking: Boolean = true;
|
var allowPlaytimeTracking: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 11)
|
||||||
|
var alwaysReloadFromCache: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 4)
|
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 4)
|
||||||
@@ -215,10 +270,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
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)
|
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
||||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||||
@@ -277,10 +332,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
||||||
var resumeAfterPreview: Int = 1;
|
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{
|
fun shouldResumePreview(previewedPosition: Long): Boolean{
|
||||||
if(resumeAfterPreview == 2)
|
if(resumeAfterPreview == 2)
|
||||||
return true;
|
return true;
|
||||||
@@ -288,6 +339,14 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return true;
|
return true;
|
||||||
return false;
|
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)
|
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 5)
|
||||||
@@ -603,6 +662,23 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun export() {
|
fun export() {
|
||||||
StateBackup.startExternalBackup();
|
StateBackup.startExternalBackup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, 4)
|
||||||
|
fun import() {
|
||||||
|
val act = SettingsActivity.getActivity() ?: return;
|
||||||
|
StateApp.instance.requestFileReadAccess(act, null) {
|
||||||
|
if(it != null && it.exists()) {
|
||||||
|
val name = it.name;
|
||||||
|
val contents = it.readBytes(act);
|
||||||
|
if(contents != null) {
|
||||||
|
if(name != null && name.endsWith(".zip", true))
|
||||||
|
StateBackup.importZipBytes(act, act.lifecycleScope, contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.payment, FieldForm.GROUP, -1, 14)
|
@FormField(R.string.payment, FieldForm.GROUP, -1, 14)
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
||||||
|
|
||||||
|
if(subscription.doNotifications)
|
||||||
|
menu.selectOption(null, "notifications", true, true);
|
||||||
if(subscription.doFetchLive)
|
if(subscription.doFetchLive)
|
||||||
menu.selectOption(null, "fetchLive", true, true);
|
menu.selectOption(null, "fetchLive", true, true);
|
||||||
if(subscription.doFetchStreams)
|
if(subscription.doFetchStreams)
|
||||||
@@ -387,8 +389,13 @@ class UISlideOverlays {
|
|||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download),
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), {
|
||||||
{ showDownloadVideoOverlay(video, container, true); }, false))
|
showDownloadVideoOverlay(video, container, true);
|
||||||
|
}, false),
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
||||||
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
|
}))
|
||||||
+ actions)
|
+ actions)
|
||||||
));
|
));
|
||||||
items.add(
|
items.add(
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import android.webkit.ConsoleMessage
|
|||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import android.webkit.WebChromeClient
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
@@ -23,6 +25,8 @@ import kotlinx.serialization.json.Json
|
|||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
private lateinit var _webView: WebView;
|
private lateinit var _webView: WebView;
|
||||||
|
private lateinit var _textUrl: TextView;
|
||||||
|
private lateinit var _buttonClose: ImageButton;
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -30,6 +34,13 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_login);
|
setContentView(R.layout.activity_login);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
_textUrl = findViewById(R.id.text_url);
|
||||||
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
_buttonClose.setOnClickListener {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_webView = findViewById(R.id.web_view);
|
_webView = findViewById(R.id.web_view);
|
||||||
_webView.settings.javaScriptEnabled = true;
|
_webView.settings.javaScriptEnabled = true;
|
||||||
CookieManager.getInstance().setAcceptCookie(true);
|
CookieManager.getInstance().setAcceptCookie(true);
|
||||||
@@ -60,6 +71,8 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
};
|
};
|
||||||
var isFirstLoad = true;
|
var isFirstLoad = true;
|
||||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||||
|
_textUrl.setText(url ?: "");
|
||||||
|
|
||||||
if(!isFirstLoad)
|
if(!isFirstLoad)
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
isFirstLoad = false;
|
isFirstLoad = false;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.content.pm.ActivityInfo
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceManager
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
@@ -497,6 +503,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
};
|
};
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
else if(targetData.startsWith("grayjay://video/")) {
|
||||||
|
val videoUrl = targetData.substring("grayjay://video/".length);
|
||||||
|
navigate(_fragVideoDetail, videoUrl);
|
||||||
|
}
|
||||||
|
else if(targetData.startsWith("grayjay://channel/")) {
|
||||||
|
val channelUrl = targetData.substring("grayjay://channel/".length);
|
||||||
|
navigate(_fragMainChannel, channelUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"content" -> {
|
"content" -> {
|
||||||
if(!handleContent(targetData, intent.type)) {
|
if(!handleContent(targetData, intent.type)) {
|
||||||
@@ -583,6 +597,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
StateBackup.importZipBytes(this, lifecycleScope, data);
|
StateBackup.importZipBytes(this, lifecycleScope, data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
||||||
|
return handleUnknownText(String(data));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fun handleFile(file: String): Boolean {
|
fun handleFile(file: String): Boolean {
|
||||||
@@ -600,6 +617,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(file.lowercase().endsWith(".txt")) {
|
||||||
|
return handleUnknownText(String(readSharedFile(file)));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fun handleReconstruction(recon: String) {
|
fun handleReconstruction(recon: String) {
|
||||||
@@ -625,6 +645,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleUnknownText(text: String): Boolean {
|
||||||
|
try {
|
||||||
|
if(text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
|
||||||
|
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
|
||||||
|
navigate(_fragImportSubscriptions, lines);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, ex.message, ex);
|
||||||
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
fun handleUnknownJson(name: String?, json: String): Boolean {
|
fun handleUnknownJson(name: String?, json: String): Boolean {
|
||||||
|
|
||||||
val context = this;
|
val context = this;
|
||||||
@@ -745,6 +779,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
StateSaved.instance.setVideoToOpenBlocking(null);
|
StateSaved.instance.setVideoToOpenBlocking(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> isFragmentActive(): Boolean {
|
||||||
|
return fragCurrent is T;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate takes a MainFragment, and makes them the current main visible view
|
* Navigate takes a MainFragment, and makes them the current main visible view
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import com.futo.polycentric.core.Store
|
|||||||
import com.futo.polycentric.core.Synchronization
|
import com.futo.polycentric.core.Synchronization
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
import com.futo.polycentric.core.toURLInfoDataLink
|
import com.futo.polycentric.core.toURLInfoDataLink
|
||||||
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -222,7 +223,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80);
|
val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80);
|
||||||
|
|
||||||
Glide.with(_imagePolycentric)
|
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)
|
.placeholder(R.drawable.placeholder_profile)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(_imagePolycentric)
|
.into(_imagePolycentric)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -13,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.Loader
|
import com.futo.platformplayer.views.Loader
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||||
@@ -28,6 +30,10 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
|
|
||||||
private var _isFinished = false;
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_settings);
|
setContentView(R.layout.activity_settings);
|
||||||
@@ -43,6 +49,11 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||||
_form.setObjectValues();
|
_form.setObjectValues();
|
||||||
Settings.instance.save();
|
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 {
|
_buttonBack.setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
* A link to a channel, often with its own name and thumbnail
|
* A link to a channel, often with its own name and thumbnail
|
||||||
*/
|
*/
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class PlatformAuthorLink {
|
open class PlatformAuthorLink {
|
||||||
val id: PlatformID;
|
val id: PlatformID;
|
||||||
val name: String;
|
val name: String;
|
||||||
val url: String;
|
val url: String;
|
||||||
@@ -28,6 +28,9 @@ class PlatformAuthorLink {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
||||||
|
if(value.has("membershipUrl"))
|
||||||
|
return PlatformAuthorMembershipLink.fromV8(config, value);
|
||||||
|
|
||||||
val context = "AuthorLink"
|
val context = "AuthorLink"
|
||||||
return PlatformAuthorLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
return PlatformAuthorLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||||
value.getOrThrow(config ,"name", context),
|
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? {
|
fun getLQThumbnail() : String? {
|
||||||
return sources.firstOrNull()?.url;
|
return sources.firstOrNull()?.url;
|
||||||
}
|
}
|
||||||
|
fun getMinimumThumbnail(quality: Int): String? {
|
||||||
|
return sources.firstOrNull { it.quality >= quality }?.url ?: getHQThumbnail();
|
||||||
|
}
|
||||||
|
|
||||||
fun hasMultiple() = sources.size > 1;
|
fun hasMultiple() = sources.size > 1;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
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.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
|
||||||
data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
|
data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "(headers: '$headers', cookieString: '$cookieMap')";
|
return "(headers: '$headers', cookieString: '$cookieMap')";
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toEncrypted(): String{
|
fun toEncrypted(): String{
|
||||||
return EncryptionProvider.instance.encrypt(serialize());
|
return SourceEncrypted.fromDecrypted { serialize() }.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun serialize(): String {
|
private fun serialize(): String {
|
||||||
@@ -25,20 +22,10 @@ data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? =
|
|||||||
val TAG = "SourceAuth";
|
val TAG = "SourceAuth";
|
||||||
|
|
||||||
fun fromEncrypted(encrypted: String?): SourceAuth? {
|
fun fromEncrypted(encrypted: String?): SourceAuth? {
|
||||||
if(encrypted == null)
|
return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(str: String): SourceAuth {
|
private fun deserialize(str: String): SourceAuth {
|
||||||
val data = Json.decodeFromString<SerializedAuth>(str);
|
val data = Json.decodeFromString<SerializedAuth>(str);
|
||||||
return SourceAuth(data.cookieMap, data.headers);
|
return SourceAuth(data.cookieMap, data.headers);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-15
@@ -1,7 +1,5 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
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.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -13,7 +11,7 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toEncrypted(): String{
|
fun toEncrypted(): String{
|
||||||
return EncryptionProvider.instance.encrypt(serialize());
|
return SourceEncrypted.fromDecrypted { serialize() }.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun serialize(): String {
|
private fun serialize(): String {
|
||||||
@@ -21,20 +19,10 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "SourceAuth";
|
val TAG = "SourceCaptchaData";
|
||||||
|
|
||||||
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
|
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
|
||||||
if(encrypted == null)
|
return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(str: String): SourceCaptchaData {
|
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 description: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
val default: String? = null,
|
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
|
@kotlinx.serialization.Transient
|
||||||
val variableOrName: String get() = variable ?: name;
|
val variableOrName: String get() = variable ?: name;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class ChannelContentCache {
|
|||||||
val items = validStores.flatMap { it.getItems() }
|
val items = validStores.flatMap { it.getItems() }
|
||||||
.sortedByDescending { it.datetime };
|
.sortedByDescending { it.datetime };
|
||||||
|
|
||||||
return DedupContentPager(PlatformContentPager(items, Math.min(150, items.size)), StatePlatform.instance.getEnabledClients().map { it.id });
|
return DedupContentPager(PlatformContentPager(items, Math.min(30, items.size)), StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uncacheContent(content: SerializedPlatformContent) {
|
fun uncacheContent(content: SerializedPlatformContent) {
|
||||||
|
|||||||
@@ -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.Cipher
|
||||||
import javax.crypto.KeyGenerator
|
import javax.crypto.KeyGenerator
|
||||||
import javax.crypto.spec.GCMParameterSpec
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class EncryptionProvider {
|
class GEncryptionProviderV0 {
|
||||||
private val _keyStore: KeyStore;
|
private val _keyStore: KeyStore;
|
||||||
private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null);
|
private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null);
|
||||||
|
|
||||||
@@ -25,45 +24,43 @@ class EncryptionProvider {
|
|||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
.setRandomizedEncryptionRequired(false)
|
.setRandomizedEncryptionRequired(false)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
keyGenerator.generateKey();
|
keyGenerator.generateKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encrypt(decrypted: String, password: String? = null): String {
|
fun encrypt(decrypted: String): String {
|
||||||
val encodedBytes = encrypt(decrypted.toByteArray(), password);
|
val encodedBytes = encrypt(decrypted.toByteArray());
|
||||||
val encrypted = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
val encrypted = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
||||||
return encrypted;
|
return encrypted;
|
||||||
}
|
}
|
||||||
fun encrypt(decrypted: ByteArray, password: String? = null): ByteArray {
|
fun encrypt(decrypted: ByteArray): ByteArray {
|
||||||
val c: Cipher = Cipher.getInstance(AES_MODE);
|
val c: Cipher = Cipher.getInstance(AES_MODE);
|
||||||
val keyToUse = if(password == null) secretKey else SecretKeySpec(password.toByteArray(), "AES");
|
c.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||||
c.init(Cipher.ENCRYPT_MODE, keyToUse, GCMParameterSpec(128, FIXED_IV));
|
|
||||||
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
||||||
return encodedBytes;
|
return encodedBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decrypt(encrypted: String, password: String? = null): String {
|
fun decrypt(encrypted: String): String {
|
||||||
val c = Cipher.getInstance(AES_MODE);
|
val c = Cipher.getInstance(AES_MODE);
|
||||||
val keyToUse = if(password == null) secretKey else SecretKeySpec(password.toByteArray(), "AES");
|
c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||||
c.init(Cipher.DECRYPT_MODE, keyToUse, GCMParameterSpec(128, FIXED_IV));
|
|
||||||
val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT)));
|
val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT)));
|
||||||
return decrypted;
|
return decrypted;
|
||||||
}
|
}
|
||||||
fun decrypt(encrypted: ByteArray, password: String? = null): ByteArray {
|
fun decrypt(encrypted: ByteArray): ByteArray {
|
||||||
val c = Cipher.getInstance(AES_MODE);
|
val c = Cipher.getInstance(AES_MODE);
|
||||||
val keyToUse = if(password == null) secretKey else SecretKeySpec(password.toByteArray(), "AES");
|
c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||||
c.init(Cipher.DECRYPT_MODE, keyToUse, GCMParameterSpec(128, FIXED_IV));
|
|
||||||
return c.doFinal(encrypted);
|
return c.doFinal(encrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101);
|
||||||
private const val AndroidKeyStore = "AndroidKeyStore";
|
private const val AndroidKeyStore = "AndroidKeyStore";
|
||||||
private const val KEY_ALIAS = "FUTOMedia_Key";
|
private const val KEY_ALIAS = "FUTOMedia_Key";
|
||||||
private const val AES_MODE = "AES/GCM/NoPadding";
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-33
@@ -1,21 +1,20 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
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 {
|
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||||
private var _buttonStore: BigButton? = null;
|
private var _supportView: SupportView? = null
|
||||||
|
private var _textMonetization: TextView? = null
|
||||||
|
|
||||||
private var _lastChannel: IPlatformChannel? = null;
|
private var _lastChannel: IPlatformChannel? = null;
|
||||||
private var _lastPolycentricProfile: PolycentricProfile? = null;
|
private var _lastPolycentricProfile: PolycentricProfile? = null;
|
||||||
@@ -24,51 +23,39 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
|||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val view = inflater.inflate(R.layout.fragment_channel_monetization, container, false);
|
val view = inflater.inflate(R.layout.fragment_channel_monetization, container, false);
|
||||||
_buttonStore = view.findViewById(R.id.button_store);
|
_supportView = view.findViewById(R.id.support);
|
||||||
|
_textMonetization = view.findViewById(R.id.text_monetization);
|
||||||
_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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_lastChannel?.also {
|
_lastChannel?.also {
|
||||||
setChannel(it);
|
setChannel(it);
|
||||||
};
|
};
|
||||||
|
|
||||||
_lastPolycentricProfile?.also {
|
_supportView?.visibility = View.GONE;
|
||||||
setPolycentricProfile(it, animate = false);
|
_textMonetization?.visibility = View.GONE;
|
||||||
}
|
setPolycentricProfile(_lastPolycentricProfile, animate = false);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
_buttonStore = null;
|
_supportView = null;
|
||||||
|
_textMonetization = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setChannel(channel: IPlatformChannel) {
|
override fun setChannel(channel: IPlatformChannel) {
|
||||||
_lastChannel = channel;
|
_lastChannel = channel;
|
||||||
_buttonStore?.visibility = View.GONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
||||||
_lastPolycentricProfile = polycentricProfile;
|
_lastPolycentricProfile = polycentricProfile
|
||||||
|
if (polycentricProfile != null) {
|
||||||
if (polycentricProfile == null) {
|
_supportView?.setPolycentricProfile(polycentricProfile, animate)
|
||||||
return;
|
_supportView?.visibility = View.VISIBLE
|
||||||
}
|
_textMonetization?.visibility = View.GONE
|
||||||
|
} else {
|
||||||
if (polycentricProfile.systemState.store.isNotEmpty()) {
|
_supportView?.setPolycentricProfile(null, animate)
|
||||||
_buttonStore?.visibility = View.VISIBLE;
|
_supportView?.visibility = View.GONE
|
||||||
|
_textMonetization?.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -225,7 +225,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
if (faqIndex != -1) {
|
if (faqIndex != -1) {
|
||||||
val button = buttons[faqIndex]
|
val button = buttons[faqIndex]
|
||||||
buttons.removeAt(faqIndex)
|
buttons.removeAt(faqIndex)
|
||||||
buttons.add(1, button)
|
buttons.add(if (buttons.size == 1) 1 else 0, button)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (data in buttons) {
|
for (data in buttons) {
|
||||||
@@ -252,8 +252,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
||||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||||
if (_buttonsVisible - 2 >= defs.size) {
|
if (_buttonsVisible - 1 >= defs.size) {
|
||||||
updateBottomMenuButtons(defs.slice(IntRange(0, defs.size - 1)).toMutableList(), false);
|
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||||
} else {
|
} else {
|
||||||
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
|
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
|
||||||
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
|
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
|
||||||
|
|||||||
+11
-7
@@ -386,14 +386,18 @@ class ChannelFragment : MainFragment() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if (plugin != null && plugin.capabilities.hasSearchChannelContents) {
|
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
||||||
buttons.add(Pair(R.drawable.ic_search) {
|
withContext(Dispatchers.Main) {
|
||||||
_fragment.navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO, channel.url));
|
if (plugin != null && plugin.capabilities.hasSearchChannelContents) {
|
||||||
});
|
buttons.add(Pair(R.drawable.ic_search) {
|
||||||
}
|
_fragment.navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO, channel.url));
|
||||||
|
});
|
||||||
|
|
||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons);
|
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_buttonSubscribe.setSubscribeChannel(channel);
|
_buttonSubscribe.setSubscribeChannel(channel);
|
||||||
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
||||||
|
|||||||
+3
-1
@@ -63,7 +63,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||||
_view?.setPreviewsEnabled(previewsEnabled);
|
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.search.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
@@ -93,6 +93,8 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
Logger.w(TAG, "Failed to load results.", it);
|
Logger.w(TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanup() {
|
override fun cleanup() {
|
||||||
|
|||||||
+4
-2
@@ -78,7 +78,7 @@ class HomeFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||||
_view?.setPreviewsEnabled(previewsEnabled);
|
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
@@ -122,6 +122,8 @@ class HomeFragment : MainFragment() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
@@ -150,7 +152,7 @@ class HomeFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun filterResults(contents: List<IPlatformContent>): List<IPlatformContent> {
|
override fun filterResults(contents: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
return contents.filter { it !is IPlatformVideo || !StateMeta.instance.isVideoHidden(it.url) };
|
return contents.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadResults() {
|
private fun loadResults() {
|
||||||
|
|||||||
+18
-2
@@ -63,6 +63,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
private var _textSelectDeselectAll: TextView;
|
private var _textSelectDeselectAll: TextView;
|
||||||
private var _textNothingToImport: TextView;
|
private var _textNothingToImport: TextView;
|
||||||
private var _textCounter: TextView;
|
private var _textCounter: TextView;
|
||||||
|
private var _textLoadMore: TextView;
|
||||||
private var _adapterView: AnyAdapterView<SelectableIPlatformChannel, ImportSubscriptionViewHolder>;
|
private var _adapterView: AnyAdapterView<SelectableIPlatformChannel, ImportSubscriptionViewHolder>;
|
||||||
private var _links: List<String> = listOf();
|
private var _links: List<String> = listOf();
|
||||||
private val _items: ArrayList<SelectableIPlatformChannel> = arrayListOf();
|
private val _items: ArrayList<SelectableIPlatformChannel> = arrayListOf();
|
||||||
@@ -79,6 +80,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
_textNothingToImport = findViewById(R.id.nothing_to_import);
|
_textNothingToImport = findViewById(R.id.nothing_to_import);
|
||||||
_textSelectDeselectAll = findViewById(R.id.text_select_deselect_all);
|
_textSelectDeselectAll = findViewById(R.id.text_select_deselect_all);
|
||||||
_textCounter = findViewById(R.id.text_select_counter);
|
_textCounter = findViewById(R.id.text_select_counter);
|
||||||
|
_textLoadMore = findViewById(R.id.text_load_more);
|
||||||
_spinner = findViewById(R.id.channel_loader);
|
_spinner = findViewById(R.id.channel_loader);
|
||||||
|
|
||||||
_adapterView = findViewById<RecyclerView>(R.id.recycler_import).asAny( _items) {
|
_adapterView = findViewById<RecyclerView>(R.id.recycler_import).asAny( _items) {
|
||||||
@@ -120,6 +122,19 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
|
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
|
||||||
loadNext();
|
loadNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_textLoadMore.setOnClickListener {
|
||||||
|
if (!_limitToastShown) {
|
||||||
|
return@setOnClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
_textLoadMore.visibility = View.GONE;
|
||||||
|
_limitToastShown = false;
|
||||||
|
_counter = 0;
|
||||||
|
load();
|
||||||
|
};
|
||||||
|
|
||||||
|
_textLoadMore.visibility = View.GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
@@ -165,7 +180,8 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
if (_counter >= MAXIMUM_BATCH_SIZE) {
|
if (_counter >= MAXIMUM_BATCH_SIZE) {
|
||||||
if (!_limitToastShown) {
|
if (!_limitToastShown) {
|
||||||
_limitToastShown = true;
|
_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);
|
setLoading(false);
|
||||||
@@ -210,7 +226,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "ImportSubscriptionsFragment";
|
val TAG = "ImportSubscriptionsFragment";
|
||||||
private const val MAXIMUM_BATCH_SIZE = 90;
|
private const val MAXIMUM_BATCH_SIZE = 100;
|
||||||
fun newInstance() = ImportSubscriptionsFragment().apply {}
|
fun newInstance() = ImportSubscriptionsFragment().apply {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+22
-4
@@ -81,7 +81,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||||
_view?.setPreviewsEnabled(previewsEnabled);
|
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.subscriptions.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
@@ -108,6 +108,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
initializeToolbarContent();
|
initializeToolbarContent();
|
||||||
|
|
||||||
|
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
@@ -119,7 +121,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
|
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
|
||||||
recyclerData.lastLoad = OffsetDateTime.now();
|
recyclerData.lastLoad = OffsetDateTime.now();
|
||||||
|
|
||||||
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5)
|
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
|
||||||
loadResults(false);
|
loadResults(false);
|
||||||
else if(recyclerData.results.size == 0)
|
else if(recyclerData.results.size == 0)
|
||||||
loadCache();
|
loadCache();
|
||||||
@@ -191,7 +193,15 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
return@TaskHandler resp;
|
return@TaskHandler resp;
|
||||||
})
|
})
|
||||||
.success { loadedResult(it); }
|
.success {
|
||||||
|
if(!Settings.instance.subscriptions.alwaysReloadFromCache)
|
||||||
|
loadedResult(it);
|
||||||
|
else {
|
||||||
|
finishRefreshLayoutLoader();
|
||||||
|
setLoading(false);
|
||||||
|
loadCache();
|
||||||
|
}
|
||||||
|
} //TODO: Remove
|
||||||
.exception<RateLimitException> {
|
.exception<RateLimitException> {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
@@ -254,7 +264,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
else null;
|
else null;
|
||||||
_filterSettings.save();
|
_filterSettings.save();
|
||||||
};
|
};
|
||||||
loadResults(false)
|
if(Settings.instance.subscriptions.fetchOnTabOpen) //TODO: Do this different, temporary workaround
|
||||||
|
loadResults(false);
|
||||||
|
else
|
||||||
|
loadCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
@@ -282,6 +295,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
loadResults(true);
|
loadResults(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun loadCache() {
|
private fun loadCache() {
|
||||||
Logger.i(TAG, "Subscriptions load cache");
|
Logger.i(TAG, "Subscriptions load cache");
|
||||||
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
||||||
@@ -301,6 +315,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
_taskGetPager.run(withRefetch);
|
_taskGetPager.run(withRefetch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
||||||
|
super.onRestoreCachedData(cachedData);
|
||||||
|
setTextCentered(if (cachedData.results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||||
|
}
|
||||||
private fun loadedResult(pager: IPager<IPlatformContent>) {
|
private fun loadedResult(pager: IPager<IPlatformContent>) {
|
||||||
Logger.i(TAG, "Subscriptions new pager loaded (${pager.getResults().size})");
|
Logger.i(TAG, "Subscriptions new pager loaded (${pager.getResults().size})");
|
||||||
|
|
||||||
|
|||||||
+38
-24
@@ -36,6 +36,8 @@ import com.futo.platformplayer.api.media.LiveChatManager
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
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.chapters.ChapterType
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
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.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
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.platforms.js.models.JSVideoDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
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.states.*
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
|
import com.futo.platformplayer.views.MonetizationView
|
||||||
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
||||||
import com.futo.platformplayer.views.casting.CastView
|
import com.futo.platformplayer.views.casting.CastView
|
||||||
import com.futo.platformplayer.views.comments.AddCommentView
|
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.LiveChatOverlay
|
||||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
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.overlays.slideup.*
|
||||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
import com.futo.platformplayer.views.pills.RoundButton
|
import com.futo.platformplayer.views.pills.RoundButton
|
||||||
@@ -191,6 +196,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _container_content_replies: RepliesOverlay;
|
private val _container_content_replies: RepliesOverlay;
|
||||||
private val _container_content_description: DescriptionOverlay;
|
private val _container_content_description: DescriptionOverlay;
|
||||||
private val _container_content_liveChat: LiveChatOverlay;
|
private val _container_content_liveChat: LiveChatOverlay;
|
||||||
|
private val _container_content_support: SupportOverlay;
|
||||||
|
|
||||||
private var _container_content_current: View;
|
private var _container_content_current: View;
|
||||||
|
|
||||||
@@ -200,9 +206,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _imageDislikeIcon: ImageView;
|
private val _imageDislikeIcon: ImageView;
|
||||||
private val _imageLikeIcon: ImageView;
|
private val _imageLikeIcon: ImageView;
|
||||||
|
|
||||||
private val _buttonSupport: LinearLayout;
|
private val _monetization: MonetizationView;
|
||||||
private val _buttonStore: LinearLayout;
|
|
||||||
private val _layoutMonetization: LinearLayout;
|
|
||||||
|
|
||||||
private val _buttonMore: RoundButton;
|
private val _buttonMore: RoundButton;
|
||||||
|
|
||||||
@@ -292,6 +296,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
||||||
_container_content_description = findViewById(R.id.videodetail_container_description);
|
_container_content_description = findViewById(R.id.videodetail_container_description);
|
||||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||||
|
_container_content_support = findViewById(R.id.videodetail_container_support)
|
||||||
|
|
||||||
_textComments = findViewById(R.id.text_comments);
|
_textComments = findViewById(R.id.text_comments);
|
||||||
_addCommentView = findViewById(R.id.add_comment_view);
|
_addCommentView = findViewById(R.id.add_comment_view);
|
||||||
@@ -310,11 +315,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_imageLikeIcon = findViewById(R.id.image_like_icon);
|
_imageLikeIcon = findViewById(R.id.image_like_icon);
|
||||||
_imageDislikeIcon = findViewById(R.id.image_dislike_icon);
|
_imageDislikeIcon = findViewById(R.id.image_dislike_icon);
|
||||||
|
|
||||||
_buttonSupport = findViewById(R.id.button_support);
|
_monetization = findViewById(R.id.monetization);
|
||||||
_buttonStore = findViewById(R.id.button_store);
|
|
||||||
_layoutMonetization = findViewById(R.id.layout_monetization);
|
|
||||||
|
|
||||||
_layoutMonetization.visibility = View.GONE;
|
|
||||||
_player.attachPlayer();
|
_player.attachPlayer();
|
||||||
|
|
||||||
|
|
||||||
@@ -327,16 +328,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonSupport.setOnClickListener {
|
_monetization.onSupportTap.subscribe {
|
||||||
val author = video?.author ?: _searchVideo?.author;
|
_container_content_support.setPolycentricProfile(_polycentricProfile?.profile, false);
|
||||||
author?.let { fragment.navigate<ChannelFragment>(it).selectTab(2); };
|
switchContentView(_container_content_support);
|
||||||
fragment.lifecycleScope.launch {
|
|
||||||
delay(100);
|
|
||||||
fragment.minimizeVideoDetail();
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonStore.setOnClickListener {
|
_monetization.onStoreTap.subscribe {
|
||||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
_polycentricProfile?.profile?.systemState?.store?.let {
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(it);
|
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) {
|
StateApp.instance.preventPictureInPicture.subscribe(this) {
|
||||||
Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true");
|
Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true");
|
||||||
preventPictureInPicture = true;
|
preventPictureInPicture = true;
|
||||||
@@ -494,8 +498,14 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
updatePillButtonVisibilities();
|
updatePillButtonVisibilities();
|
||||||
|
|
||||||
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
||||||
if (StateCasting.instance.activeDevice != null) {
|
val activeDevice = StateCasting.instance.activeDevice;
|
||||||
|
if (activeDevice != null) {
|
||||||
handlePlayChanged(it);
|
handlePlayChanged(it);
|
||||||
|
|
||||||
|
val v = video;
|
||||||
|
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
||||||
|
nextVideo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -539,6 +549,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_queue.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_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
|
||||||
_description_viewMore.setOnClickListener {
|
_description_viewMore.setOnClickListener {
|
||||||
switchContentView(_container_content_description);
|
switchContentView(_container_content_description);
|
||||||
@@ -806,7 +817,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
when (Settings.instance.playback.backgroundPlay) {
|
when (Settings.instance.playback.backgroundPlay) {
|
||||||
0 -> handlePause();
|
0 -> handlePause();
|
||||||
1 -> {
|
1 -> {
|
||||||
if(!(video?.isLive ?: false))
|
if(!(video?.isLive ?: false) && Settings.instance.playback.backgroundSwitchToAudio)
|
||||||
_player.switchToAudioMode();
|
_player.switchToAudioMode();
|
||||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||||
}
|
}
|
||||||
@@ -841,6 +852,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_replies.cleanup();
|
_container_content_replies.cleanup();
|
||||||
_container_content_queue.cleanup();
|
_container_content_queue.cleanup();
|
||||||
_container_content_description.cleanup();
|
_container_content_description.cleanup();
|
||||||
|
_container_content_support.cleanup();
|
||||||
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||||
@@ -1042,6 +1054,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to get chapters", ex);
|
Logger.e(TAG, "Failed to get chapters", ex);
|
||||||
|
_player.setChapters(null);
|
||||||
|
|
||||||
/*withContext(Dispatchers.Main) {
|
/*withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
||||||
@@ -1088,6 +1101,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_2).toInt(), 0, 0);
|
(_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_title.text = video.name;
|
||||||
_minimize_meta.text = video.author.name;
|
_minimize_meta.text = video.author.name;
|
||||||
|
|
||||||
@@ -1803,6 +1821,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_isCasting = isCasting;
|
_isCasting = isCasting;
|
||||||
|
|
||||||
if(isCasting) {
|
if(isCasting) {
|
||||||
|
setFullscreen(false);
|
||||||
_player.stop();
|
_player.stop();
|
||||||
_player.hideControls(false);
|
_player.hideControls(false);
|
||||||
_cast.visibility = View.VISIBLE;
|
_cast.visibility = View.VISIBLE;
|
||||||
@@ -2090,12 +2109,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile != null) {
|
_monetization.setPolycentricProfile(cachedPolycentricProfile, animate);
|
||||||
_channelName.text = cachedPolycentricProfile.profile.systemState.username;
|
|
||||||
_layoutMonetization.visibility = View.VISIBLE;
|
|
||||||
} else {
|
|
||||||
_layoutMonetization.visibility = View.GONE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.futo.platformplayer.images;
|
package com.futo.platformplayer.images;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.bumptech.glide.Priority;
|
import com.bumptech.glide.Priority;
|
||||||
|
|||||||
@@ -39,7 +39,12 @@ class PolycentricCache {
|
|||||||
ContentType.USERNAME.value,
|
ContentType.USERNAME.value,
|
||||||
ContentType.DESCRIPTION.value,
|
ContentType.DESCRIPTION.value,
|
||||||
ContentType.STORE.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) };
|
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.media.AudioFocusRequest
|
|||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
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}");
|
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;
|
_notif_last_bitmap = bitmap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,25 @@ class StateApp {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestFileReadAccess(activity: IWithResultLauncher, path: Uri?, handle: (DocumentFile?)->Unit) {
|
||||||
|
if(activity is Context) {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
|
if(path != null)
|
||||||
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
|
||||||
|
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
|
activity.launchForResult(intent, 98) {
|
||||||
|
if(it.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = it.data?.data;
|
||||||
|
if(uri != null)
|
||||||
|
handle(DocumentFile.fromSingleUri(activity, uri));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit)
|
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit)
|
||||||
{
|
{
|
||||||
if(activity is Context)
|
if(activity is Context)
|
||||||
@@ -335,6 +354,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mainAppStarting(context: Context) {
|
fun mainAppStarting(context: Context) {
|
||||||
|
Logger.i(TAG, "MainApp Starting");
|
||||||
initializeFiles(true);
|
initializeFiles(true);
|
||||||
|
|
||||||
val logFile = File(context.filesDir, "log.txt");
|
val logFile = File(context.filesDir, "log.txt");
|
||||||
@@ -353,14 +373,18 @@ class StateApp {
|
|||||||
|
|
||||||
Logger.setLogConsumers(listOf(AndroidLogConsumer()));
|
Logger.setLogConsumers(listOf(AndroidLogConsumer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePayment.instance.initialize();
|
StatePayment.instance.initialize();
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]");
|
||||||
StatePolycentric.instance.load(context);
|
StatePolycentric.instance.load(context);
|
||||||
|
Logger.i(TAG, "MainApp Starting: Initializing [Saved]");
|
||||||
StateSaved.instance.load();
|
StateSaved.instance.load();
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]");
|
||||||
displayMetrics = context.resources.displayMetrics;
|
displayMetrics = context.resources.displayMetrics;
|
||||||
ensureConnectivityManager(context);
|
ensureConnectivityManager(context);
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Starting: Initializing [Telemetry]");
|
||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
StateTelemetry.instance.initialize();
|
StateTelemetry.instance.initialize();
|
||||||
StateTelemetry.instance.upload();
|
StateTelemetry.instance.upload();
|
||||||
@@ -381,11 +405,12 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun mainAppStarted(context: Context) {
|
fun mainAppStarted(context: Context) {
|
||||||
Logger.i(TAG, "App started");
|
Logger.i(TAG, "MainApp Started");
|
||||||
|
|
||||||
//Start loading cache
|
//Start loading cache
|
||||||
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
Logger.i(TAG, "MainApp Started: Initializing [ChannelContentCache]");
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
ChannelContentCache.instance;
|
ChannelContentCache.instance;
|
||||||
}
|
}
|
||||||
@@ -400,10 +425,12 @@ class StateApp {
|
|||||||
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
||||||
StateDeveloper.instance.runServer();
|
StateDeveloper.instance.runServer();
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Check [Migration (Subscriptions)]");
|
||||||
if(StateSubscriptions.instance.shouldMigrate())
|
if(StateSubscriptions.instance.shouldMigrate())
|
||||||
StateSubscriptions.instance.tryMigrateIfNecessary();
|
StateSubscriptions.instance.tryMigrateIfNecessary();
|
||||||
|
|
||||||
if(Settings.instance.downloads.shouldDownload()) {
|
if(Settings.instance.downloads.shouldDownload()) {
|
||||||
|
Logger.i(TAG, "MainApp Started: Check [Downloads]");
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
|
|
||||||
StateDownloads.instance.getDownloadPlaylists();
|
StateDownloads.instance.getDownloadPlaylists();
|
||||||
@@ -411,8 +438,10 @@ class StateApp {
|
|||||||
DownloadService.getOrCreateService(context);
|
DownloadService.getOrCreateService(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Check [Exports]");
|
||||||
StateDownloads.instance.checkForExportTodos();
|
StateDownloads.instance.checkForExportTodos();
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
|
||||||
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
|
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
|
||||||
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
|
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
|
||||||
val backgroundDownload = Settings.instance.autoUpdate.backgroundDownload == 1;
|
val backgroundDownload = Settings.instance.autoUpdate.backgroundDownload == 1;
|
||||||
@@ -436,6 +465,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
||||||
_receiverBecomingNoisy?.let {
|
_receiverBecomingNoisy?.let {
|
||||||
_receiverBecomingNoisy = null;
|
_receiverBecomingNoisy = null;
|
||||||
context.unregisterReceiver(it);
|
context.unregisterReceiver(it);
|
||||||
@@ -444,6 +474,7 @@ class StateApp {
|
|||||||
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||||
|
|
||||||
//Migration
|
//Migration
|
||||||
|
Logger.i(TAG, "MainApp Started: Check [Migrations]");
|
||||||
migrateStores(context, listOf(
|
migrateStores(context, listOf(
|
||||||
StateSubscriptions.instance.toMigrateCheck(),
|
StateSubscriptions.instance.toMigrateCheck(),
|
||||||
StatePlaylists.instance.toMigrateCheck()
|
StatePlaylists.instance.toMigrateCheck()
|
||||||
@@ -451,6 +482,7 @@ class StateApp {
|
|||||||
|
|
||||||
if(Settings.instance.subscriptions.fetchOnAppBoot) {
|
if(Settings.instance.subscriptions.fetchOnAppBoot) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
|
Logger.i(TAG, "MainApp Started: Fetch [Subscriptions]");
|
||||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
||||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
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 };
|
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
||||||
@@ -465,9 +497,11 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [BackgroundWork]");
|
||||||
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
|
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
|
||||||
scheduleBackgroundWork(context, interval != 0, interval);
|
scheduleBackgroundWork(context, interval != 0, interval);
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [AutoBackup]");
|
||||||
if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) {
|
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", {
|
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)) {
|
if(context is IWithResultLauncher && !Settings.instance.storage.isStorageMainValid(context)) {
|
||||||
@@ -495,6 +529,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [Announcements]");
|
||||||
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateAnnouncement.instance.loadAnnouncements();
|
StateAnnouncement.instance.loadAnnouncements();
|
||||||
@@ -513,7 +548,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateAnnouncement.instance.registerDidYouKnow();
|
StateAnnouncement.instance.registerDidYouKnow();
|
||||||
|
Logger.i(TAG, "MainApp Started: Finished");
|
||||||
}
|
}
|
||||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||||
if(!Settings.instance.didFirstStart) {
|
if(!Settings.instance.didFirstStart) {
|
||||||
@@ -712,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 {
|
companion object {
|
||||||
private val TAG = "StateApp";
|
private val TAG = "StateApp";
|
||||||
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
|
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
|
||||||
|
|||||||
@@ -1,28 +1,19 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
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.app.ShareCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.copyTo
|
import com.futo.platformplayer.copyTo
|
||||||
import com.futo.platformplayer.copyToOutputStream
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
|
||||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
|
||||||
import com.futo.platformplayer.getInputStream
|
|
||||||
import com.futo.platformplayer.getNowDiffHours
|
import com.futo.platformplayer.getNowDiffHours
|
||||||
import com.futo.platformplayer.getOutputStream
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.readBytes
|
import com.futo.platformplayer.readBytes
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -38,9 +29,8 @@ import kotlinx.serialization.json.Json
|
|||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.InputStream
|
import java.lang.Exception
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
@@ -82,7 +72,7 @@ class StateBackup {
|
|||||||
val pbytes = password.toByteArray();
|
val pbytes = password.toByteArray();
|
||||||
if(pbytes.size < 4 || pbytes.size > 32)
|
if(pbytes.size < 4 || pbytes.size > 32)
|
||||||
throw IllegalStateException("Automatic backup passwords should atleast be 4 character and smaller than 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 {
|
fun hasAutomaticBackup(): Boolean {
|
||||||
val context = StateApp.instance.contextOrNull ?: return false;
|
val context = StateApp.instance.contextOrNull ?: return false;
|
||||||
@@ -106,8 +96,8 @@ class StateBackup {
|
|||||||
val data = export();
|
val data = export();
|
||||||
val zip = data.asZip();
|
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)) {
|
if(!Settings.instance.storage.isStorageMainValid(context)) {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
UIDialogs.toast("Missing permissions for auto-backup, please set the external general directory in settings");
|
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");
|
throw IllegalStateException("Backup file does not exist");
|
||||||
|
|
||||||
val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]");
|
val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]");
|
||||||
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
importEncryptedZipBytes(context, scope, backupBytesEncrypted, password);
|
||||||
importZipBytes(context, scope, backupBytes);
|
|
||||||
Logger.i(TAG, "Finished AutoBackup restore");
|
Logger.i(TAG, "Finished AutoBackup restore");
|
||||||
}
|
}
|
||||||
catch (exSec: FileNotFoundException) {
|
catch (exSec: FileNotFoundException) {
|
||||||
@@ -179,13 +168,30 @@ class StateBackup {
|
|||||||
throw ex;
|
throw ex;
|
||||||
|
|
||||||
val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]");
|
val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]");
|
||||||
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
importEncryptedZipBytes(context, scope, backupBytesEncrypted, password);
|
||||||
importZipBytes(context, scope, backupBytes);
|
|
||||||
Logger.i(TAG, "Finished AutoBackup restore");
|
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() {
|
fun startExternalBackup() {
|
||||||
val data = export();
|
val data = export();
|
||||||
val now = OffsetDateTime.now();
|
val now = OffsetDateTime.now();
|
||||||
|
|||||||
@@ -5,15 +5,39 @@ import com.futo.platformplayer.stores.StringHashSetStorage
|
|||||||
|
|
||||||
class StateMeta {
|
class StateMeta {
|
||||||
val hiddenVideos = FragmentedStorage.get<StringHashSetStorage>("hiddenVideos");
|
val hiddenVideos = FragmentedStorage.get<StringHashSetStorage>("hiddenVideos");
|
||||||
|
val hiddenCreators = FragmentedStorage.get<StringHashSetStorage>("hiddenCreators");
|
||||||
|
|
||||||
fun isVideoHidden(videoUrl: String) : Boolean {
|
fun isVideoHidden(videoUrl: String) : Boolean {
|
||||||
return hiddenVideos.contains(videoUrl);
|
return hiddenVideos.contains(videoUrl);
|
||||||
}
|
}
|
||||||
fun addHiddenVideo(videoUrl: String) {
|
fun addHiddenVideo(videoUrl: String) {
|
||||||
hiddenVideos.addDistinct(videoUrl);
|
hiddenVideos.addDistinct(videoUrl);
|
||||||
|
hiddenVideos.save();
|
||||||
}
|
}
|
||||||
fun removeHiddenVideo(videoUrl: String) {
|
fun removeHiddenVideo(videoUrl: String) {
|
||||||
hiddenVideos.remove(videoUrl);
|
hiddenVideos.remove(videoUrl);
|
||||||
|
hiddenVideos.save();
|
||||||
|
}
|
||||||
|
fun removeAllHiddenVideos() {
|
||||||
|
hiddenVideos.removeAll();
|
||||||
|
hiddenVideos.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun isCreatorHidden(creatorUrl: String): Boolean {
|
||||||
|
return hiddenCreators.contains(creatorUrl);
|
||||||
|
}
|
||||||
|
fun addHiddenCreator(creatorUrl: String) {
|
||||||
|
hiddenCreators.addDistinct(creatorUrl);
|
||||||
|
hiddenCreators.save();
|
||||||
|
}
|
||||||
|
fun removeHiddenCreator(creatorUrl: String) {
|
||||||
|
hiddenCreators.remove(creatorUrl);
|
||||||
|
hiddenCreators.save();
|
||||||
|
}
|
||||||
|
fun removeAllHiddenCreators() {
|
||||||
|
hiddenCreators.removeAll();
|
||||||
|
hiddenCreators.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ class StringHashSetStorage : FragmentedStorageFileJson() {
|
|||||||
values.remove(obj);
|
values.remove(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun removeAll() {
|
||||||
|
synchronized(values) {
|
||||||
|
values.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
fun set(vararg objs: String) {
|
fun set(vararg objs: String) {
|
||||||
synchronized(values) {
|
synchronized(values) {
|
||||||
values.clear();
|
values.clear();
|
||||||
|
|||||||
+7
-3
@@ -42,9 +42,13 @@ class SmartSubscriptionAlgorithm(
|
|||||||
if(sub.shouldFetchPosts()) ResultCapabilities.TYPE_POSTS else null,
|
if(sub.shouldFetchPosts()) ResultCapabilities.TYPE_POSTS else null,
|
||||||
if(sub.shouldFetchLiveStreams()) ResultCapabilities.TYPE_LIVE else null
|
if(sub.shouldFetchLiveStreams()) ResultCapabilities.TYPE_LIVE else null
|
||||||
).filterNotNull().filter { capabilities.hasType(it) };
|
).filterNotNull().filter { capabilities.hasType(it) };
|
||||||
return@flatMap types.map {
|
|
||||||
SubscriptionTask(client, sub, url, it);
|
if(!types.isEmpty())
|
||||||
};
|
return@flatMap types.map {
|
||||||
|
SubscriptionTask(client, sub, url, it);
|
||||||
|
};
|
||||||
|
else
|
||||||
|
listOf(SubscriptionTask(client, sub, url, ResultCapabilities.TYPE_VIDEOS, true))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-2
@@ -2,6 +2,7 @@ package com.futo.platformplayer.subscription
|
|||||||
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
@@ -19,6 +20,7 @@ import com.futo.platformplayer.findNonRuntimeException
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -55,7 +57,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
for(clientTasks in tasksGrouped) {
|
for(clientTasks in tasksGrouped) {
|
||||||
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
|
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
|
||||||
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
||||||
if(clientCacheCount > 0) {
|
if(clientCacheCount > 0 && clientTaskCount > 0 && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
|
||||||
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +79,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
if(result != null) {
|
if(result != null) {
|
||||||
if(result.pager != null)
|
if(result.pager != null)
|
||||||
taskResults.add(result);
|
taskResults.add(result);
|
||||||
else if(result.exception != null) {
|
if(result.exception != null) {
|
||||||
val ex = result.exception;
|
val ex = result.exception;
|
||||||
if(ex != null) {
|
if(ex != null) {
|
||||||
val nonRuntimeEx = findNonRuntimeException(ex);
|
val nonRuntimeEx = findNonRuntimeException(ex);
|
||||||
@@ -196,6 +198,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
||||||
pager = ChannelContentCache.instance.getChannelCachePager(task.sub.channel.url);
|
pager = ChannelContentCache.instance.getChannelCachePager(task.sub.channel.url);
|
||||||
taskEx = ex;
|
taskEx = ex;
|
||||||
|
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@submit SubscriptionTaskResult(task, null, 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) {
|
fun bind(p: Playlist) {
|
||||||
if (p.videos.isNotEmpty()) {
|
if (p.videos.isNotEmpty()) {
|
||||||
Glide.with(_imageThumbnail)
|
Glide.with(_imageThumbnail)
|
||||||
.load(p.videos[0].thumbnails.getLQThumbnail())
|
.load(p.videos[0].thumbnails.getMinimumThumbnail(380))
|
||||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(_imageThumbnail);
|
.into(_imageThumbnail);
|
||||||
|
|||||||
+3
-1
@@ -11,6 +11,8 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
|||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import com.bumptech.glide.Glide
|
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.R
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
@@ -76,7 +78,7 @@ class VideoListEditorViewHolder : ViewHolder {
|
|||||||
|
|
||||||
fun bind(v: IPlatformVideo, canEdit: Boolean) {
|
fun bind(v: IPlatformVideo, canEdit: Boolean) {
|
||||||
Glide.with(_imageThumbnail)
|
Glide.with(_imageThumbnail)
|
||||||
.load(v.thumbnails.getLQThumbnail())
|
.load(v.thumbnails.getHQThumbnail())
|
||||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(_imageThumbnail);
|
.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;
|
_textSecondary.text = attrTextSecondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSecondaryText(text: String?) {
|
||||||
|
_textSecondary.text = text
|
||||||
|
}
|
||||||
|
|
||||||
fun withPrimaryText(text: String): BigButton {
|
fun withPrimaryText(text: String): BigButton {
|
||||||
_textPrimary.text = text;
|
_textPrimary.text = text;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.core.view.updateLayoutParams
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
@@ -37,7 +38,7 @@ class ButtonField : BigButton, IField {
|
|||||||
//private val _title : TextView;
|
//private val _title : TextView;
|
||||||
//private val _subtitle : 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){
|
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs){
|
||||||
//inflate(context, R.layout.field_button, this);
|
//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 {
|
fun fromMethod(obj : Any, method: Method) : ButtonField {
|
||||||
this._method = method;
|
this._method = method;
|
||||||
this._obj = obj;
|
this._obj = obj;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.view.View
|
|||||||
import android.widget.*
|
import android.widget.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
|
||||||
class DropdownField : TableRow, IField {
|
class DropdownField : TableRow, IField {
|
||||||
@@ -35,7 +37,7 @@ class DropdownField : TableRow, IField {
|
|||||||
|
|
||||||
override var reference: Any? = null;
|
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){
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs){
|
||||||
inflate(context, R.layout.field_dropdown, this);
|
inflate(context, R.layout.field_dropdown, this);
|
||||||
@@ -50,13 +52,21 @@ class DropdownField : TableRow, IField {
|
|||||||
_isInitFire = false;
|
_isInitFire = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Logger.i("DropdownField", "Changed: ${_selected} -> ${pos}");
|
||||||
|
val old = _selected;
|
||||||
_selected = pos;
|
_selected = pos;
|
||||||
onChanged.emit(this@DropdownField, pos);
|
onChanged.emit(this@DropdownField, pos, old);
|
||||||
}
|
}
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
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 {
|
fun asBoolean(name: String, description: String?, obj: Boolean) : DropdownField {
|
||||||
_options = resources.getStringArray(R.array.enabled_disabled_array);
|
_options = resources.getStringArray(R.array.enabled_disabled_array);
|
||||||
_spinner.adapter = ArrayAdapter<String>(context, R.layout.spinner_item_simple, _options).also {
|
_spinner.adapter = ArrayAdapter<String>(context, R.layout.spinner_item_simple, _options).also {
|
||||||
@@ -77,6 +87,23 @@ class DropdownField : TableRow, IField {
|
|||||||
return this;
|
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 {
|
override fun fromField(obj: Any, field: Field, formField: FormField?) : DropdownField {
|
||||||
this._field = field;
|
this._field = field;
|
||||||
this._obj = obj;
|
this._obj = obj;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.views.fields
|
package com.futo.platformplayer.views.fields
|
||||||
|
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
|
||||||
|
|
||||||
@@ -13,11 +14,12 @@ interface IField {
|
|||||||
val obj : Any?;
|
val obj : Any?;
|
||||||
val field : Field?;
|
val field : Field?;
|
||||||
|
|
||||||
val onChanged : Event2<IField, Any>;
|
val onChanged : Event3<IField, Any, Any>;
|
||||||
|
|
||||||
var reference: Any?;
|
var reference: Any?;
|
||||||
|
|
||||||
|
|
||||||
fun fromField(obj : Any, field : Field, formField: FormField? = null) : IField;
|
fun fromField(obj : Any, field : Field, formField: FormField? = null) : IField;
|
||||||
fun setField();
|
fun setField();
|
||||||
|
|
||||||
|
fun setValue(value: Any);
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -49,7 +50,7 @@ class FieldForm : LinearLayout {
|
|||||||
throw java.lang.IllegalStateException("Only views can be IFields");
|
throw java.lang.IllegalStateException("Only views can be IFields");
|
||||||
|
|
||||||
_root.addView(field as View);
|
_root.addView(field as View);
|
||||||
field.onChanged.subscribe { a1, a2 ->
|
field.onChanged.subscribe { a1, a2, oldValue ->
|
||||||
onChanged.emit(a1, a2);
|
onChanged.emit(a1, a2);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -67,7 +68,7 @@ class FieldForm : LinearLayout {
|
|||||||
throw java.lang.IllegalStateException("Only views can be IFields");
|
throw java.lang.IllegalStateException("Only views can be IFields");
|
||||||
|
|
||||||
_root.addView(field as View);
|
_root.addView(field as View);
|
||||||
field.onChanged.subscribe { a1, a2 ->
|
field.onChanged.subscribe { a1, a2, oldValue ->
|
||||||
onChanged.emit(a1, a2);
|
onChanged.emit(a1, a2);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -82,25 +83,59 @@ class FieldForm : LinearLayout {
|
|||||||
|
|
||||||
if(groupTitle == null) {
|
if(groupTitle == null) {
|
||||||
for(field in newFields) {
|
for(field in newFields) {
|
||||||
if(field !is View)
|
if(field.second !is View)
|
||||||
throw java.lang.IllegalStateException("Only views can be IFields");
|
throw java.lang.IllegalStateException("Only views can be IFields");
|
||||||
field.onChanged.subscribe { field, value ->
|
finalizePluginSettingField(field.first, field.second, newFields);
|
||||||
onChanged.emit(field, value);
|
|
||||||
}
|
|
||||||
_root.addView(field as View);
|
_root.addView(field as View);
|
||||||
}
|
}
|
||||||
_fields = newFields;
|
_fields = newFields.map { it.second };
|
||||||
} else {
|
} else {
|
||||||
for(field in newFields) {
|
for(field in newFields) {
|
||||||
field.onChanged.subscribe { field, value ->
|
finalizePluginSettingField(field.first, field.second, newFields);
|
||||||
onChanged.emit(field, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val group = GroupField(context, groupTitle, groupDescription)
|
val group = GroupField(context, groupTitle, groupDescription)
|
||||||
.withFields(newFields);
|
.withFields(newFields.map { it.second });
|
||||||
_root.addView(group as View);
|
_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(){
|
fun setObjectValues(){
|
||||||
val fields = _fields;
|
val fields = _fields;
|
||||||
@@ -133,26 +168,42 @@ class FieldForm : LinearLayout {
|
|||||||
private val _json = Json {};
|
private val _json = Json {};
|
||||||
|
|
||||||
|
|
||||||
fun getFieldsFromPluginSettings(context: Context, settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>): List<IField> {
|
fun getFieldsFromPluginSettings(context: Context, settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>): List<Pair<SourcePluginConfig.Setting, IField>> {
|
||||||
val fields = mutableListOf<IField>()
|
val fields = mutableListOf<Pair<SourcePluginConfig.Setting, IField>>()
|
||||||
|
|
||||||
for(setting in settings) {
|
for(setting in settings) {
|
||||||
|
val value = if(values.containsKey(setting.variableOrName)) values[setting.variableOrName] else setting.default;
|
||||||
|
|
||||||
val field = when(setting.type.lowercase()) {
|
val field = when(setting.type.lowercase()) {
|
||||||
|
"header" -> {
|
||||||
|
val groupField = GroupField(context, setting.name, setting.description);
|
||||||
|
groupField;
|
||||||
|
}
|
||||||
"boolean" -> {
|
"boolean" -> {
|
||||||
val value = if(values.containsKey(setting.variableOrName)) values[setting.variableOrName] else setting.default;
|
|
||||||
val field = ToggleField(context).withValue(setting.name,
|
val field = ToggleField(context).withValue(setting.name,
|
||||||
setting.description,
|
setting.description,
|
||||||
value == "true" || value == "1" || value == "True");
|
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);
|
values[setting.variableOrName] = _json.encodeToString (value == 1 || value == true);
|
||||||
}
|
}
|
||||||
field;
|
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;
|
else -> null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(field != null)
|
if(field != null)
|
||||||
fields.add(field);
|
fields.add(Pair(setting, field));
|
||||||
}
|
}
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
|
||||||
class GroupField : LinearLayout, IField {
|
class GroupField : LinearLayout, IField {
|
||||||
@@ -27,7 +28,7 @@ class GroupField : LinearLayout, IField {
|
|||||||
return _field;
|
return _field;
|
||||||
};
|
};
|
||||||
|
|
||||||
override val onChanged = Event2<IField, Any>();
|
override val onChanged = Event3<IField, Any, Any>();
|
||||||
|
|
||||||
private val _title : TextView;
|
private val _title : TextView;
|
||||||
private val _subtitle : TextView;
|
private val _subtitle : TextView;
|
||||||
@@ -138,4 +139,6 @@ class GroupField : LinearLayout, IField {
|
|||||||
field.setField();
|
field.setField();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setValue(value: Any) {}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import android.util.AttributeSet
|
|||||||
import android.widget.*
|
import android.widget.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ class ReadOnlyTextField : TableRow, IField {
|
|||||||
private val _title : TextView;
|
private val _title : TextView;
|
||||||
private val _value : TextView;
|
private val _value : TextView;
|
||||||
|
|
||||||
override val onChanged = Event2<IField, Any>();
|
override val onChanged = Event3<IField, Any, Any>();
|
||||||
|
|
||||||
override var reference: Any? = null;
|
override var reference: Any? = null;
|
||||||
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs){
|
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs){
|
||||||
@@ -36,6 +37,8 @@ class ReadOnlyTextField : TableRow, IField {
|
|||||||
_value = findViewById(R.id.field_value);
|
_value = findViewById(R.id.field_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setValue(value: Any) {}
|
||||||
|
|
||||||
override fun fromField(obj : Any, field : Field, formField: FormField?) : ReadOnlyTextField {
|
override fun fromField(obj : Any, field : Field, formField: FormField?) : ReadOnlyTextField {
|
||||||
this._field = field;
|
this._field = field;
|
||||||
this._obj = obj;
|
this._obj = obj;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.view.View
|
|||||||
import android.widget.*
|
import android.widget.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event2
|
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 com.futo.platformplayer.views.others.Toggle
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
|
||||||
@@ -28,10 +30,11 @@ class ToggleField : TableRow, IField {
|
|||||||
private val _title : TextView;
|
private val _title : TextView;
|
||||||
private val _description : TextView;
|
private val _description : TextView;
|
||||||
private val _toggle : Toggle;
|
private val _toggle : Toggle;
|
||||||
|
private var _lastValue: Boolean = false;
|
||||||
|
|
||||||
override var reference: Any? = null;
|
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){
|
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs){
|
||||||
inflate(context, R.layout.field_toggle, this);
|
inflate(context, R.layout.field_toggle, this);
|
||||||
@@ -40,10 +43,18 @@ class ToggleField : TableRow, IField {
|
|||||||
_description = findViewById(R.id.field_description);
|
_description = findViewById(R.id.field_description);
|
||||||
|
|
||||||
_toggle.onValueChanged.subscribe {
|
_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 {
|
fun withValue(title: String, description: String?, value: Boolean): ToggleField {
|
||||||
|
|
||||||
_title.text = title;
|
_title.text = title;
|
||||||
@@ -54,6 +65,7 @@ class ToggleField : TableRow, IField {
|
|||||||
_description.visibility = View.GONE;
|
_description.visibility = View.GONE;
|
||||||
|
|
||||||
_toggle.setValue(value, true);
|
_toggle.setValue(value, true);
|
||||||
|
_lastValue = value;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -78,14 +90,16 @@ class ToggleField : TableRow, IField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val value = field.get(obj);
|
val value = field.get(obj);
|
||||||
if(value is Boolean)
|
val toggleValue = if(value is Boolean)
|
||||||
_toggle.setValue(value, true);
|
value;
|
||||||
else if(value is Number)
|
else if(value is Number)
|
||||||
_toggle.setValue((value as Number).toInt() > 0, true);
|
(value as Number).toInt() > 0;
|
||||||
else if(value == null)
|
else if(value == null)
|
||||||
_toggle.setValue(false, true);
|
false;
|
||||||
else
|
else
|
||||||
_toggle.setValue(false, true);
|
false;
|
||||||
|
_toggle.setValue(toggleValue, true);
|
||||||
|
_lastValue = toggleValue;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Toggle : AppCompatImageView {
|
|||||||
scaleType = ScaleType.FIT_CENTER;
|
scaleType = ScaleType.FIT_CENTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setValue(v: Boolean, animated: Boolean = true) {
|
fun setValue(v: Boolean, animated: Boolean = true, withEvent: Boolean = false) {
|
||||||
if (value == v) {
|
if (value == v) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -44,5 +44,8 @@ class Toggle : AppCompatImageView {
|
|||||||
} else {
|
} else {
|
||||||
setImageResource(if (v) R.drawable.toggle_enabled else R.drawable.toggle_disabled);
|
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> {
|
.exception<Throwable> {
|
||||||
Logger.e(TAG, "Failed to load comments.", it);
|
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);
|
setLoading(false);
|
||||||
} else TaskHandler(IPlatformVideoDetails::class.java, StateApp.instance.scopeGetter);
|
} else TaskHandler(IPlatformVideoDetails::class.java, StateApp.instance.scopeGetter);
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
private val _control_videosettings_fullscreen: ImageButton;
|
private val _control_videosettings_fullscreen: ImageButton;
|
||||||
private val _control_minimize_fullscreen: ImageButton;
|
private val _control_minimize_fullscreen: ImageButton;
|
||||||
private val _control_rotate_lock_fullscreen: ImageButton;
|
private val _control_rotate_lock_fullscreen: ImageButton;
|
||||||
|
private val _control_cast_fullscreen: ImageButton;
|
||||||
private val _control_play_fullscreen: ImageButton;
|
private val _control_play_fullscreen: ImageButton;
|
||||||
private val _time_bar_fullscreen: TimeBar;
|
private val _time_bar_fullscreen: TimeBar;
|
||||||
private val _overlay_brightness: FrameLayout;
|
private val _overlay_brightness: FrameLayout;
|
||||||
@@ -127,10 +128,15 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
_control_minimize_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_minimize);
|
_control_minimize_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_minimize);
|
||||||
_control_videosettings_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_settings);
|
_control_videosettings_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_settings);
|
||||||
_control_rotate_lock_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_rotate_lock);
|
_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_play_fullscreen = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play);
|
||||||
_control_chapter_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_chapter_current);
|
_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);
|
_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);
|
_overlay_brightness = findViewById(R.id.overlay_brightness);
|
||||||
|
|
||||||
_title_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_title);
|
_title_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_title);
|
||||||
@@ -229,6 +235,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
StatePlayer.instance.rotationLock = !StatePlayer.instance.rotationLock;
|
StatePlayer.instance.rotationLock = !StatePlayer.instance.rotationLock;
|
||||||
updateRotateLock();
|
updateRotateLock();
|
||||||
};
|
};
|
||||||
|
_control_cast_fullscreen.setOnClickListener {
|
||||||
|
UIDialogs.showCastingDialog(context);
|
||||||
|
};
|
||||||
|
|
||||||
var lastPos = 0L;
|
var lastPos = 0L;
|
||||||
videoControls.setProgressUpdateListener { position, bufferedPosition ->
|
videoControls.setProgressUpdateListener { position, bufferedPosition ->
|
||||||
@@ -270,7 +279,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
if (drawable != null) {
|
if (drawable != null) {
|
||||||
_videoView.defaultArtwork = drawable;
|
_videoView.defaultArtwork = drawable;
|
||||||
_videoView.useArtwork = true;
|
_videoView.useArtwork = true;
|
||||||
fitHeight();
|
fitOrFill(isFullScreen);
|
||||||
} else {
|
} else {
|
||||||
_videoView.defaultArtwork = null;
|
_videoView.defaultArtwork = null;
|
||||||
_videoView.useArtwork = false;
|
_videoView.useArtwork = false;
|
||||||
@@ -311,7 +320,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
gestureControl.hideControls();
|
gestureControl.hideControls();
|
||||||
//videoControlsBar.visibility = View.GONE;
|
//videoControlsBar.visibility = View.GONE;
|
||||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||||
fillHeight();
|
|
||||||
_videoControls_fullscreen.show();
|
_videoControls_fullscreen.show();
|
||||||
videoControls.hide();
|
videoControls.hide();
|
||||||
}
|
}
|
||||||
@@ -323,16 +332,25 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
gestureControl.hideControls();
|
gestureControl.hideControls();
|
||||||
//videoControlsBar.visibility = View.VISIBLE;
|
//videoControlsBar.visibility = View.VISIBLE;
|
||||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||||
fitHeight();
|
|
||||||
videoControls.show();
|
videoControls.show();
|
||||||
_videoControls_fullscreen.hide();
|
_videoControls_fullscreen.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fitOrFill(fullScreen);
|
||||||
gestureControl.setFullscreen(fullScreen);
|
gestureControl.setFullscreen(fullScreen);
|
||||||
onToggleFullScreen.emit(fullScreen);
|
onToggleFullScreen.emit(fullScreen);
|
||||||
isFullScreen = fullScreen;
|
isFullScreen = fullScreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fitOrFill(fullScreen: Boolean) {
|
||||||
|
if (fullScreen) {
|
||||||
|
fillHeight();
|
||||||
|
} else {
|
||||||
|
fitHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun lockControlsAlpha(locked : Boolean) {
|
fun lockControlsAlpha(locked : Boolean) {
|
||||||
if(locked && _isControlsLocked != locked) {
|
if(locked && _isControlsLocked != 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="#000000" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<solid android:color="#3A1448" />
|
<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" />
|
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||||
</shape>
|
</shape>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<solid android:color="#144826" />
|
<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" />
|
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||||
</shape>
|
</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:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960">
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
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"/>
|
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:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960">
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M120,800L120,720L600,720L600,800L120,800ZM640,520Q557,520 498.5,461.5Q440,403 440,320Q440,237 498.5,178.5Q557,120 640,120Q723,120 781.5,178.5Q840,237 840,320Q840,403 781.5,461.5Q723,520 640,520ZM120,480L120,400L372,400Q379,422 388,442Q397,462 410,480L120,480ZM120,640L120,560L496,560Q519,574 545,583.5Q571,593 600,597L600,640L120,640ZM620,360L660,360L660,200L620,200L620,360ZM640,440Q648,440 654,434Q660,428 660,420Q660,412 654,406Q648,400 640,400Q632,400 626,406Q620,412 620,420Q620,428 626,434Q632,440 640,440Z"/>
|
android:pathData="M120,800L120,720L600,720L600,800L120,800ZM640,520Q557,520 498.5,461.5Q440,403 440,320Q440,237 498.5,178.5Q557,120 640,120Q723,120 781.5,178.5Q840,237 840,320Q840,403 781.5,461.5Q723,520 640,520ZM120,480L120,400L372,400Q379,422 388,442Q397,462 410,480L120,480ZM120,640L120,560L496,560Q519,574 545,583.5Q571,593 600,597L600,640L120,640ZM620,360L660,360L660,200L620,200L620,360ZM640,440Q648,440 654,434Q660,428 660,420Q660,412 654,406Q648,400 640,400Q632,400 626,406Q620,412 620,420Q620,428 626,434Q632,440 640,440Z"/>
|
||||||
|
|||||||
@@ -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="M420,600L540,600L517,471Q537,461 548.5,442Q560,423 560,400Q560,367 536.5,343.5Q513,320 480,320Q447,320 423.5,343.5Q400,367 400,400Q400,423 411.5,442Q423,461 443,471L420,600ZM480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M120,720L120,640L600,640L600,720L120,720ZM120,520L120,440L840,440L840,520L120,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
|
||||||
|
</vector>
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960">
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
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"/>
|
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:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960">
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
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"/>
|
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"/>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960">
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M560,600Q577,600 589.5,587.5Q602,575 602,558Q602,541 589.5,528.5Q577,516 560,516Q543,516 530.5,528.5Q518,541 518,558Q518,575 530.5,587.5Q543,600 560,600ZM530,472L590,472Q590,443 596,429.5Q602,416 624,394Q654,364 664,345.5Q674,327 674,302Q674,257 642.5,228.5Q611,200 560,200Q519,200 488.5,223Q458,246 446,284L500,306Q509,281 524.5,268.5Q540,256 560,256Q584,256 599,269.5Q614,283 614,306Q614,320 606,332.5Q598,345 578,364Q545,393 537.5,409.5Q530,426 530,472ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z"/>
|
android:pathData="M560,600Q577,600 589.5,587.5Q602,575 602,558Q602,541 589.5,528.5Q577,516 560,516Q543,516 530.5,528.5Q518,541 518,558Q518,575 530.5,587.5Q543,600 560,600ZM530,472L590,472Q590,443 596,429.5Q602,416 624,394Q654,364 664,345.5Q674,327 674,302Q674,257 642.5,228.5Q611,200 560,200Q519,200 488.5,223Q458,246 446,284L500,306Q509,281 524.5,268.5Q540,256 560,256Q584,256 599,269.5Q614,283 614,306Q614,320 606,332.5Q598,345 578,364Q545,393 537.5,409.5Q530,426 530,472ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z"/>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960">
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M160,720L800,720Q800,720 800,720Q800,720 800,720L800,400L520,400L520,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L160,720Z"/>
|
android:pathData="M160,720L800,720Q800,720 800,720Q800,720 800,720L800,400L520,400L520,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L160,720Z"/>
|
||||||
|
|||||||
@@ -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="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M640,480L640,400L720,400L720,480L640,480ZM640,560L560,560L560,480L640,480L640,560ZM640,640L640,560L720,560L720,640L640,640ZM447,320L367,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L560,720L560,640L640,640L640,720L800,720Q800,720 800,720Q800,720 800,720L800,320Q800,320 800,320Q800,320 800,320L640,320L640,400L560,400L560,320L447,320ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L400,160L480,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L160,720Q160,720 160,720Q160,720 160,720L160,320Q160,320 160,320Q160,320 160,320L160,320L160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720Z"/>
|
||||||
|
</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"?>
|
<?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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:background="@color/black">
|
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
|
<WebView
|
||||||
android:id="@+id/web_view"
|
android:id="@+id/web_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="0dp"
|
||||||
</FrameLayout>
|
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>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?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"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@color/gray_1d">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingTop="40dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/update_spinner"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_move_up" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_progress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text=""
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_dialog"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/there_is_an_update_available_do_you_wish_to_update"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:layout_marginTop="28dp"
|
||||||
|
android:layout_marginBottom="28dp">
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?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"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@color/gray_1d">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingTop="40dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/update_spinner"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_move_up" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_progress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text=""
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_dialog"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/import_options"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:layout_marginTop="28dp"
|
||||||
|
android:layout_marginBottom="28dp">
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:buttonIcon="@drawable/ic_zip"
|
||||||
|
app:buttonText="Import Grayjay export (.zip)"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
app:buttonBackground="@drawable/background_big_button_black"
|
||||||
|
app:buttonSubText="Pick a Grayjay export zip file" />
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:buttonIcon="@drawable/ic_encrypted"
|
||||||
|
android:alpha="0.5"
|
||||||
|
app:buttonBackground="@drawable/background_big_button_black"
|
||||||
|
app:buttonText="Import Grayjay Auto-Backup (.ezip)"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
app:buttonSubText="Pick a Grayjay auto-backup encrypted zip file" />
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
app:buttonIcon="@drawable/ic_lines"
|
||||||
|
android:alpha="0.5"
|
||||||
|
app:buttonBackground="@drawable/background_big_button_black"
|
||||||
|
app:buttonText="Import Line Text file (.txt)"
|
||||||
|
app:buttonSubText="Pick a text file with one entry per line" />
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
app:buttonIcon="@drawable/ic_play"
|
||||||
|
app:buttonBackground="@drawable/background_big_button_black"
|
||||||
|
app:buttonText="Import NewPipe Subscriptions (.json)"
|
||||||
|
app:buttonSubText="Pick a NewPipe subscriptions json file" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_cancel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/close"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:background="@color/transparent" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,92 +1,23 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_margin="18dp">
|
|
||||||
|
|
||||||
<com.futo.platformplayer.views.buttons.BigButton
|
<com.futo.platformplayer.views.SupportView
|
||||||
android:id="@+id/button_store"
|
android:id="@+id/support"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_monetization"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:buttonIcon="@drawable/ic_store"
|
android:gravity="center_horizontal"
|
||||||
app:buttonText="@string/store"
|
android:layout_gravity="center"
|
||||||
app:buttonSubText="@string/visit_my_store" />
|
android:text="@string/this_creator_has_not_setup_any_monetization_features"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
<LinearLayout
|
android:textColor="#ACACAC"
|
||||||
android:layout_width="match_parent"
|
android:textSize="12dp"
|
||||||
android:layout_height="wrap_content"
|
android:visibility="gone" />
|
||||||
android:orientation="vertical"
|
</FrameLayout>
|
||||||
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>
|
|
||||||
@@ -40,6 +40,19 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1" />
|
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
|
<TextView
|
||||||
android:id="@+id/text_select_counter"
|
android:id="@+id/text_select_counter"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -445,65 +445,10 @@
|
|||||||
android:text="@string/click_to_read_more"/>
|
android:text="@string/click_to_read_more"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<com.futo.platformplayer.views.MonetizationView
|
||||||
android:id="@+id/layout_monetization"
|
android:id="@+id/monetization"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
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>
|
|
||||||
|
|
||||||
<com.futo.platformplayer.views.videometa.UpNextView
|
<com.futo.platformplayer.views.videometa.UpNextView
|
||||||
android:id="@+id/up_next"
|
android:id="@+id/up_next"
|
||||||
@@ -602,6 +547,12 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="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>
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/videodetail_loading_overlay"
|
android:id="@+id/videodetail_loading_overlay"
|
||||||
|
|||||||
@@ -13,12 +13,13 @@
|
|||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/image_video_thumbnail"
|
android:id="@+id/image_video_thumbnail"
|
||||||
android:layout_height="60dp"
|
android:layout_height="50dp"
|
||||||
android:layout_width="60dp"
|
android:layout_width="50dp"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
app:shapeAppearanceOverlay="@style/roundedCorners_4dp"
|
app:shapeAppearanceOverlay="@style/roundedCorners_4dp"
|
||||||
app:srcCompat="@drawable/placeholder_video_thumbnail"
|
app:srcCompat="@drawable/placeholder_video_thumbnail"
|
||||||
android:background="@drawable/video_thumbnail_outline"
|
android:background="@drawable/video_thumbnail_outline"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent" />
|
app:layout_constraintLeft_toLeftOf="parent" />
|
||||||
|
|
||||||
@@ -48,9 +49,10 @@
|
|||||||
android:fontFamily="@font/inter_extra_light"
|
android:fontFamily="@font/inter_extra_light"
|
||||||
tools:text="3 videos"
|
tools:text="3 videos"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_name"
|
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
|
||||||
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:layout_marginStart="10dp" />
|
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"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="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
|
<ImageButton
|
||||||
android:id="@+id/exo_rotate_lock"
|
android:id="@+id/exo_rotate_lock"
|
||||||
android:layout_width="50dp"
|
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">
|
<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>
|
||||||
<string-array name="feed_style">
|
<string-array name="feed_style">
|
||||||
<item>معاينة</item>
|
<item>معاينة</item>
|
||||||
@@ -697,7 +701,7 @@
|
|||||||
<item>استئناف بعد 10 ثوان</item>
|
<item>استئناف بعد 10 ثوان</item>
|
||||||
<item>استئناف دائم</item>
|
<item>استئناف دائم</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="languages">
|
<string-array name="audio_languages">
|
||||||
<item>الإنجليزية</item>
|
<item>الإنجليزية</item>
|
||||||
<item>الإسبانية</item>
|
<item>الإسبانية</item>
|
||||||
<item>الفرنسية</item>
|
<item>الفرنسية</item>
|
||||||
|
|||||||
@@ -682,6 +682,10 @@
|
|||||||
<string-array name="subscriptions_sortby_array">
|
<string-array name="subscriptions_sortby_array">
|
||||||
<item>Name aufsteigend</item>
|
<item>Name aufsteigend</item>
|
||||||
<item>Name absteigend</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>
|
||||||
<string-array name="feed_style">
|
<string-array name="feed_style">
|
||||||
<item>Vorschau</item>
|
<item>Vorschau</item>
|
||||||
@@ -697,7 +701,7 @@
|
|||||||
<item>Nach 10 Sekunden fortsetzen</item>
|
<item>Nach 10 Sekunden fortsetzen</item>
|
||||||
<item>Immer fortsetzen</item>
|
<item>Immer fortsetzen</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="languages">
|
<string-array name="audio_languages">
|
||||||
<item>Englisch</item>
|
<item>Englisch</item>
|
||||||
<item>Spanisch</item>
|
<item>Spanisch</item>
|
||||||
<item>Französisch</item>
|
<item>Französisch</item>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user