mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6141c36855 | |||
| 4084ab3ed0 | |||
| 34e733823a | |||
| f1d01642cd | |||
| d5551d7118 | |||
| d079a1e8e4 | |||
| c06c00ee9b | |||
| 1d8eababc2 | |||
| 75cf1ffbdd | |||
| 5499706a9b | |||
| ba57e32920 | |||
| df96c5b51c | |||
| 75f81d20db | |||
| 3fc92e4065 | |||
| 8ffd5f411f | |||
| 918161a299 | |||
| 9f50f72eaa | |||
| 2f66f124aa | |||
| 9a11717cf4 | |||
| 0d80424799 | |||
| ed9a65b2f0 | |||
| 8a53297be2 | |||
| 20862a27c8 | |||
| 95785e6c78 | |||
| e88c649578 | |||
| 09f91e64fb | |||
| b8923e59a1 | |||
| e722c0ce9a | |||
| 56248bf4b0 | |||
| 5af4787c45 | |||
| 0990247322 | |||
| 0154525578 | |||
| 1dc6eee242 | |||
| c63a63cb33 | |||
| c1967556ac | |||
| 309a57f5a1 |
@@ -1,11 +1,13 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import userpackage.Protocol
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
@@ -47,6 +49,15 @@ fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||
}
|
||||
|
||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||
if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) {
|
||||
removeServer(PolycentricCache.STAGING_SERVER)
|
||||
}
|
||||
|
||||
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||
removeServer(PolycentricCache.SERVER)
|
||||
}
|
||||
|
||||
val exceptions = fullyBackfillServers()
|
||||
for (pair in exceptions) {
|
||||
val server = pair.key
|
||||
|
||||
@@ -739,7 +739,7 @@ class UISlideOverlays {
|
||||
}
|
||||
|
||||
|
||||
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
|
||||
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), invokeParents: Boolean = true, onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
|
||||
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
||||
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
||||
|
||||
@@ -747,7 +747,7 @@ class UISlideOverlays {
|
||||
hidden
|
||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
||||
btn.handler?.invoke(btn);
|
||||
}, true) as View }.toTypedArray(),
|
||||
}, invokeParents) as View }.toTypedArray(),
|
||||
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
|
||||
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
||||
val selected = it
|
||||
|
||||
+52
-29
@@ -8,12 +8,15 @@ import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
import com.futo.polycentric.core.KeyPair
|
||||
import com.futo.polycentric.core.Process
|
||||
import com.futo.polycentric.core.ProcessSecret
|
||||
@@ -21,6 +24,9 @@ import com.futo.polycentric.core.SignedEvent
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.base64UrlToByteArray
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import userpackage.Protocol
|
||||
import userpackage.Protocol.ExportBundle
|
||||
|
||||
@@ -29,6 +35,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonScanProfile: LinearLayout;
|
||||
private lateinit var _buttonImportProfile: LinearLayout;
|
||||
private lateinit var _editProfile: EditText;
|
||||
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||
|
||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||
@@ -52,6 +59,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
_buttonHelp = findViewById(R.id.button_help);
|
||||
_buttonScanProfile = findViewById(R.id.button_scan_profile);
|
||||
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
||||
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||
_editProfile = findViewById(R.id.edit_profile);
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish();
|
||||
@@ -94,42 +102,57 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
||||
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
||||
if (urlInfo.urlType != 3L) {
|
||||
throw Exception("Expected urlInfo struct of type ExportBundle")
|
||||
}
|
||||
_loaderOverlay.show()
|
||||
|
||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
||||
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
||||
if (urlInfo.urlType != 3L) {
|
||||
throw Exception("Expected urlInfo struct of type ExportBundle")
|
||||
}
|
||||
|
||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
||||
if (existingProcessSecret != null) {
|
||||
UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported));
|
||||
return;
|
||||
}
|
||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||
|
||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||
Store.instance.addProcessSecret(processSecret);
|
||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
||||
if (existingProcessSecret != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
|
||||
}
|
||||
return@launch;
|
||||
}
|
||||
|
||||
val processHandle = processSecret.toProcessHandle();
|
||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||
Store.instance.addProcessSecret(processSecret);
|
||||
|
||||
for (e in exportBundle.events.eventsList) {
|
||||
try {
|
||||
val se = SignedEvent.fromProto(e);
|
||||
Store.instance.putSignedEvent(se);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Ignored invalid event", e);
|
||||
val processHandle = processSecret.toProcessHandle();
|
||||
|
||||
for (e in exportBundle.events.eventsList) {
|
||||
try {
|
||||
val se = SignedEvent.fromProto(e);
|
||||
Store.instance.putSignedEvent(se);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Ignored invalid event", e);
|
||||
}
|
||||
}
|
||||
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
||||
withContext(Dispatchers.Main) {
|
||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||
finish();
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to import profile", e);
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
||||
}
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
_loaderOverlay.hide();
|
||||
}
|
||||
}
|
||||
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||
finish();
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to import profile", e);
|
||||
UIDialogs.toast(this, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+48
-20
@@ -1,6 +1,8 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -12,6 +14,7 @@ import android.webkit.MimeTypeMap
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
@@ -21,14 +24,16 @@ import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.Synchronization
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.toBase64Url
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -46,6 +51,8 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonDelete: BigButton;
|
||||
private lateinit var _username: String;
|
||||
private lateinit var _imagePolycentric: ImageView;
|
||||
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||
private lateinit var _textSystem: TextView;
|
||||
private var _avatarUri: Uri? = null;
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
@@ -63,28 +70,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
_buttonExport = findViewById(R.id.button_export);
|
||||
_buttonLogout = findViewById(R.id.button_logout);
|
||||
_buttonDelete = findViewById(R.id.button_delete);
|
||||
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||
_textSystem = findViewById(R.id.text_system)
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
saveIfRequired();
|
||||
finish();
|
||||
};
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
||||
Synchronization.fullyBackFillClient(processHandle, processHandle.system, "https://srv1-stg.polycentric.io");
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
updateUI();
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUI();
|
||||
|
||||
_imagePolycentric.setOnClickListener {
|
||||
ImagePicker.with(this)
|
||||
.cropSquare()
|
||||
@@ -120,6 +112,37 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
_textSystem.setOnLongClickListener {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip: ClipData = ClipData.newPlainText("system", _textSystem.text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
updateUI()
|
||||
|
||||
StatePolycentric.instance.processHandle?.let { processHandle ->
|
||||
_loaderOverlay.show()
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
updateUI();
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
|
||||
}
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
_loaderOverlay.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveIfRequired() {
|
||||
@@ -128,13 +151,17 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
var hasChanges = false;
|
||||
val username = _editName.text.toString();
|
||||
if (username.length < 3) {
|
||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
|
||||
}
|
||||
return@launch;
|
||||
}
|
||||
|
||||
val processHandle = StatePolycentric.instance.processHandle;
|
||||
if (processHandle == null) {
|
||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
||||
}
|
||||
return@launch;
|
||||
}
|
||||
|
||||
@@ -219,6 +246,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
private fun updateUI() {
|
||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
|
||||
_textSystem.text = processHandle.system.key.toBase64Url()
|
||||
_username = systemState.username;
|
||||
_editName.text.clear();
|
||||
_editName.text.append(_username);
|
||||
|
||||
@@ -6,11 +6,12 @@ import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
@@ -25,7 +26,11 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.polycentric.core.*
|
||||
import com.futo.polycentric.core.ClaimType
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -93,7 +98,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||
|
||||
val comment = _editComment.text.toString();
|
||||
val processHandle = StatePolycentric.instance.processHandle!!
|
||||
val eventPointer = processHandle.post(comment, null, ref)
|
||||
val eventPointer = processHandle.post(comment, ref)
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -26,8 +27,8 @@ import kotlinx.coroutines.launch
|
||||
class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
private lateinit var _imageLoader: ImageView;
|
||||
private lateinit var _buttonClose: Button;
|
||||
private lateinit var _buttonAdd: Button;
|
||||
private lateinit var _buttonScanQR: Button;
|
||||
private lateinit var _buttonAdd: ImageButton;
|
||||
private lateinit var _buttonScanQR: ImageButton;
|
||||
private lateinit var _textNoDevicesFound: TextView;
|
||||
private lateinit var _textNoDevicesRemembered: TextView;
|
||||
private lateinit var _recyclerDevices: RecyclerView;
|
||||
|
||||
+12
-5
@@ -289,10 +289,17 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
buttonDefinitions.find { d -> d.id == it.id }
|
||||
}.toMutableList()
|
||||
|
||||
if (!StatePayment.instance.hasPaid) {
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||
//Add unconfigured tabs with default values
|
||||
buttonDefinitions.forEach { buttonDefinition ->
|
||||
if (!Settings.instance.tabs.any { it.id == buttonDefinition.id }) {
|
||||
newCurrentButtonDefinitions.add(buttonDefinition)
|
||||
}
|
||||
}
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.faq, canToggle = false, { false }, {
|
||||
|
||||
if (!StatePayment.instance.hasPaid) {
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||
}
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
|
||||
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||
}))
|
||||
|
||||
@@ -349,8 +356,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
|
||||
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
|
||||
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }),
|
||||
ButtonDefinition(10, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
|
||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
|
||||
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
|
||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
||||
val c = it.context ?: return@ButtonDefinition;
|
||||
Logger.i(TAG, "settings preventPictureInPicture()");
|
||||
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
||||
|
||||
+2
-1
@@ -418,6 +418,7 @@ class ChannelFragment : MainFragment() {
|
||||
|
||||
_buttonSubscribe.setSubscribeChannel(channel);
|
||||
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
||||
_textChannel.text = channel.name;
|
||||
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else "";
|
||||
|
||||
//TODO: Find a better way to access the adapter fragments..
|
||||
@@ -465,7 +466,7 @@ class ChannelFragment : MainFragment() {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
||||
|
||||
+16
-17
@@ -314,8 +314,8 @@ class PostDetailFragment : MainFragment {
|
||||
private fun updatePolycentricRating() {
|
||||
_rating.visibility = View.GONE;
|
||||
|
||||
val value = _post?.id?.value ?: _postOverview?.id?.value ?: return;
|
||||
val ref = Models.referenceFromBuffer(value.toByteArray());
|
||||
val ref = Models.referenceFromBuffer((_post?.url ?: _postOverview?.url)?.toByteArray() ?: return)
|
||||
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
||||
val version = _version;
|
||||
|
||||
_rating.onLikeDislikeUpdated.remove(this);
|
||||
@@ -333,7 +333,8 @@ class PostDetailFragment : MainFragment {
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||
ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||
)
|
||||
),
|
||||
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||
);
|
||||
|
||||
if (version != _version) {
|
||||
@@ -342,8 +343,8 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (version != _version) {
|
||||
@@ -468,9 +469,7 @@ class PostDetailFragment : MainFragment {
|
||||
if (_postOverview == null) {
|
||||
fetchPolycentricProfile();
|
||||
updatePolycentricRating();
|
||||
|
||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
||||
_addCommentView.setContext(value.url, ref);
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
}
|
||||
|
||||
updateCommentType(true);
|
||||
@@ -489,9 +488,7 @@ class PostDetailFragment : MainFragment {
|
||||
_textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count?
|
||||
_textContent.text = value.description.fixHtmlWhitespace();
|
||||
_platformIndicator.setPlatformFromClientID(value.id.pluginId);
|
||||
|
||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
||||
_addCommentView.setContext(value.url, ref);
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
|
||||
updatePolycentricRating();
|
||||
fetchPolycentricProfile();
|
||||
@@ -636,12 +633,12 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
if (cachedPolycentricProfile?.profile == null) {
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
_creatorThumbnail.setHarborAvailable(false, animate);
|
||||
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||
return;
|
||||
}
|
||||
|
||||
_layoutMonetization.visibility = View.VISIBLE;
|
||||
_creatorThumbnail.setHarborAvailable(true, animate);
|
||||
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
|
||||
}
|
||||
|
||||
private fun fetchPost() {
|
||||
@@ -665,14 +662,16 @@ class PostDetailFragment : MainFragment {
|
||||
private fun fetchPolycentricComments() {
|
||||
Logger.i(TAG, "fetchPolycentricComments")
|
||||
val post = _post;
|
||||
val idValue = post?.id?.value
|
||||
if (idValue == null) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
|
||||
val ref = (_post?.url ?: _postOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) }
|
||||
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
||||
|
||||
if (ref == null) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
|
||||
_commentsList.clear();
|
||||
return
|
||||
}
|
||||
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post.url, Models.referenceFromBuffer(idValue.toByteArray())); };
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
|
||||
}
|
||||
|
||||
private fun updateCommentType(reloadComments: Boolean) {
|
||||
|
||||
+3
-1
@@ -39,10 +39,12 @@ class SourcesFragment : MainFragment() {
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack)
|
||||
|
||||
if(topBar is AddTopBarFragment)
|
||||
if(topBar is AddTopBarFragment) {
|
||||
(topBar as AddTopBarFragment).onAdd.clear();
|
||||
(topBar as AddTopBarFragment).onAdd.subscribe {
|
||||
startActivity(Intent(requireContext(), AddSourceOptionsActivity::class.java));
|
||||
};
|
||||
}
|
||||
|
||||
_view?.reloadSources();
|
||||
}
|
||||
|
||||
+36
-7
@@ -4,19 +4,15 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -25,14 +21,13 @@ import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.SearchView
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
||||
import com.futo.platformplayer.views.overlays.CreatorSelectOverlay
|
||||
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
|
||||
@@ -64,6 +59,11 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
return view;
|
||||
}
|
||||
|
||||
override fun onHide() {
|
||||
super.onHide();
|
||||
_view?.onHide();
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SourcesFragment";
|
||||
fun newInstance() = SubscriptionGroupFragment().apply {}
|
||||
@@ -86,7 +86,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
private val _buttonSettings: ImageButton;
|
||||
private val _buttonDelete: ImageButton;
|
||||
|
||||
private val _buttonAddCreator: Button;
|
||||
private val _buttonAddCreator: FrameLayout;
|
||||
|
||||
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
|
||||
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
|
||||
@@ -97,6 +97,8 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
|
||||
private var _group: SubscriptionGroup? = null;
|
||||
|
||||
private var _didDelete: Boolean = false;
|
||||
|
||||
constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
|
||||
inflate(context, R.layout.fragment_subscriptions_group, this);
|
||||
_fragment = fragment;
|
||||
@@ -137,6 +139,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
UIDialogs.Action("Delete", {
|
||||
_group?.let {
|
||||
it.urls.remove(channel.url);
|
||||
save();
|
||||
reloadCreators(it);
|
||||
}
|
||||
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||
@@ -178,6 +181,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
UIDialogs.Action("Cancel", {}),
|
||||
UIDialogs.Action("Delete", {
|
||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
|
||||
_didDelete = true;
|
||||
fragment.close(true);
|
||||
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||
};
|
||||
@@ -188,6 +192,12 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
filterCreators();
|
||||
}
|
||||
|
||||
_topbar.setButtons(
|
||||
Pair(R.drawable.ic_share) {
|
||||
UIDialogs.toast(context, "Coming soon");
|
||||
}
|
||||
);
|
||||
|
||||
setGroup(null);
|
||||
}
|
||||
|
||||
@@ -240,6 +250,18 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
_overlay.animate().alpha(1f).setDuration(300).start();
|
||||
overlay.onSelected.subscribe {
|
||||
_group?.let { g ->
|
||||
if(g.urls.isEmpty() && g.image == null) {
|
||||
//Obtain image
|
||||
for(sub in it) {
|
||||
val sub = StateSubscriptions.instance.getSubscription(sub);
|
||||
if(sub != null && sub.channel.thumbnail != null) {
|
||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||
g.image?.setImageView(_imageGroup);
|
||||
g.image?.setImageView(_imageGroupBackground);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(url in it) {
|
||||
if(!g.urls.contains(url))
|
||||
g.urls.add(url);
|
||||
@@ -256,6 +278,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
|
||||
|
||||
fun setGroup(group: SubscriptionGroup?) {
|
||||
_didDelete = false;
|
||||
_group = group;
|
||||
_textGroupTitle.text = group?.name;
|
||||
|
||||
@@ -272,6 +295,12 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
reloadCreators(group);
|
||||
}
|
||||
|
||||
fun onHide() {
|
||||
if(!_didDelete && _group != null && StateSubscriptionGroups.instance.getSubscriptionGroup(_group!!.id) === null) {
|
||||
UIDialogs.toast(context, "Group creation cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun reloadCreators(group: SubscriptionGroup?) {
|
||||
_enabledCreators.clear();
|
||||
|
||||
+3
-1
@@ -107,12 +107,14 @@ class SubscriptionGroupListFragment : MainFragment() {
|
||||
updateGroups();
|
||||
}
|
||||
|
||||
if(topBar is AddTopBarFragment)
|
||||
if(topBar is AddTopBarFragment) {
|
||||
(topBar as AddTopBarFragment).onAdd.clear();
|
||||
(topBar as AddTopBarFragment).onAdd.subscribe {
|
||||
_overlay?.let {
|
||||
UISlideOverlays.showCreateSubscriptionGroup(it)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGroups() {
|
||||
|
||||
+24
-8
@@ -52,6 +52,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
override val hasBottomBar: Boolean get() = true;
|
||||
|
||||
private var _view: SubscriptionsFeedView? = null;
|
||||
private var _group: SubscriptionGroup? = null;
|
||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
@@ -72,6 +73,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = SubscriptionsFeedView(this, inflater, _cachedRecyclerData);
|
||||
_view = view;
|
||||
if(_group != null)
|
||||
view.selectSubgroup(_group);
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -80,6 +83,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
val view = _view;
|
||||
if (view != null) {
|
||||
_cachedRecyclerData = view.recyclerData;
|
||||
_group = view.subGroup;
|
||||
view.cleanup();
|
||||
_view = null;
|
||||
}
|
||||
@@ -100,12 +104,12 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
|
||||
|
||||
private var _subGroup: SubscriptionGroup? = null;
|
||||
var subGroup: SubscriptionGroup? = null;
|
||||
|
||||
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||
StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
|
||||
if(_subGroup?.id == id)
|
||||
if(subGroup?.id == id)
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
setProgress(progress, total);
|
||||
@@ -140,8 +144,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
|
||||
recyclerData.lastLoad = OffsetDateTime.now();
|
||||
|
||||
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
|
||||
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen) {
|
||||
loadResults(false);
|
||||
}
|
||||
else if(recyclerData.results.size == 0) {
|
||||
loadCache();
|
||||
setLoading(false);
|
||||
@@ -197,7 +202,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
private var _bypassRateLimit = false;
|
||||
private val _lastExceptions: List<Throwable>? = null;
|
||||
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
||||
val group = _subGroup;
|
||||
val group = subGroup;
|
||||
if(!_bypassRateLimit) {
|
||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
|
||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
||||
@@ -257,6 +262,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
}
|
||||
};
|
||||
|
||||
fun selectSubgroup(g: SubscriptionGroup?) {
|
||||
if(g != null)
|
||||
_subscriptionBar?.selectGroup(g);
|
||||
}
|
||||
|
||||
private fun initializeToolbarContent() {
|
||||
_subscriptionBar = SubscriptionBar(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
@@ -266,11 +276,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
if(g is SubscriptionGroup.Add)
|
||||
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
|
||||
else {
|
||||
_subGroup = g;
|
||||
subGroup = g;
|
||||
setProgress(0, 0);
|
||||
if(Settings.instance.subscriptions.fetchOnTabOpen)
|
||||
if(Settings.instance.subscriptions.fetchOnTabOpen) {
|
||||
loadCache();
|
||||
loadResults(false);
|
||||
else loadCache();
|
||||
}
|
||||
else if(g != null && StateSubscriptions.instance.getFeed(g.id) != null) {
|
||||
loadResults(false);
|
||||
}
|
||||
else
|
||||
loadCache();
|
||||
}
|
||||
};
|
||||
_subscriptionBar?.onHoldGroup?.subscribe { g ->
|
||||
@@ -311,7 +327,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
|
||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||
val nowSoon = OffsetDateTime.now().plusMinutes(5);
|
||||
val filterGroup = _subGroup;
|
||||
val filterGroup = subGroup;
|
||||
return results.filter {
|
||||
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
||||
|
||||
|
||||
+2
-2
@@ -129,8 +129,8 @@ class TutorialFragment : MainFragment() {
|
||||
override val dash: IDashManifestSource? = null
|
||||
override val hls: IHLSManifestSource? = null
|
||||
override val subtitles: List<ISubtitleSource> = emptyList()
|
||||
override val shareUrl: String = ""
|
||||
override val url: String = ""
|
||||
override val shareUrl: String = videoUrl
|
||||
override val url: String = videoUrl
|
||||
override val datetime: OffsetDateTime? = OffsetDateTime.parse("2023-12-18T00:00:00Z")
|
||||
override val thumbnails: Thumbnails = Thumbnails(arrayOf(Thumbnail(thumbnailUrl)))
|
||||
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("tutorial", "f422ced6-b551-4b62-818e-27a4f5f4918a"), "Grayjay", "", "https://releases.grayjay.app/tutorials/author.jpeg")
|
||||
|
||||
+86
-72
@@ -437,16 +437,20 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
var buttonMore: RoundButton? = null;
|
||||
buttonMore = RoundButton(context, R.drawable.ic_menu, context.getString(R.string.more), TAG_MORE) {
|
||||
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE)) {selected ->
|
||||
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE), false) {selected ->
|
||||
_buttonPins.setButtons(*(selected + listOf(buttonMore!!)).toTypedArray());
|
||||
_buttonPinStore.set(*selected.filter { it.tagRef is String }.map{ it.tagRef as String }.toTypedArray())
|
||||
_buttonPinStore.save();
|
||||
}
|
||||
};
|
||||
};
|
||||
_buttonMore = buttonMore;
|
||||
updateMoreButtons();
|
||||
|
||||
_channelButton.setOnClickListener {
|
||||
if (video is TutorialFragment.TutorialVideo) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
(video?.author ?: _searchVideo?.author)?.let {
|
||||
fragment.navigate<ChannelFragment>(it);
|
||||
fragment.lifecycleScope.launch {
|
||||
@@ -769,6 +773,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
Logger.e(TAG, "Failed to reopen live chat", ex);
|
||||
}
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
} else null,
|
||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
||||
if(!allowBackground) {
|
||||
@@ -781,6 +786,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
allowBackground = false;
|
||||
it.text.text = resources.getString(R.string.background);
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
},
|
||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||
video?.let {
|
||||
@@ -793,11 +799,13 @@ class VideoDetailView : ConstraintLayout {
|
||||
preventPictureInPicture = true;
|
||||
shareVideo();
|
||||
};
|
||||
_slideUpOverlay?.hide();
|
||||
},
|
||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
|
||||
this.startPictureInPicture();
|
||||
fragment.forcePictureInPicture();
|
||||
//PiPActivity.startPiP(context);
|
||||
_slideUpOverlay?.hide();
|
||||
},
|
||||
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
||||
video?.let {
|
||||
@@ -805,9 +813,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
fragment.navigate<BrowserFragment>(url);
|
||||
fragment.minimizeVideoDetail();
|
||||
};
|
||||
_slideUpOverlay?.hide();
|
||||
},
|
||||
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
||||
reloadVideo();
|
||||
_slideUpOverlay?.hide();
|
||||
}).filterNotNull();
|
||||
if(!_buttonPinStore.getAllValues().any())
|
||||
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
|
||||
@@ -1134,6 +1144,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(videoDetail is VideoLocal) {
|
||||
videoLocal = videoDetail;
|
||||
video = videoDetail;
|
||||
this.video = video;
|
||||
val videoTask = StatePlatform.instance.getContentDetails(videoDetail.url);
|
||||
videoTask.invokeOnCompletion { ex ->
|
||||
if(ex != null) {
|
||||
@@ -1201,9 +1212,9 @@ class VideoDetailView : ConstraintLayout {
|
||||
};
|
||||
}
|
||||
|
||||
val ref = video.id.value?.let { Models.referenceFromBuffer(it.toByteArray()) };
|
||||
_addCommentView.setContext(video.url, ref);
|
||||
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
val extraBytesRef = video.id.value?.toByteArray()
|
||||
_addCommentView.setContext(video.url, ref)
|
||||
_player.setMetadata(video.name, video.author.name);
|
||||
|
||||
if (video !is TutorialFragment.TutorialVideo) {
|
||||
@@ -1264,57 +1275,54 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_rating.onLikeDislikeUpdated.remove(this);
|
||||
|
||||
if (ref != null) {
|
||||
_rating.visibility = View.GONE;
|
||||
_rating.visibility = View.GONE;
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
||||
arrayListOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.like.data)).build(),
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||
)
|
||||
);
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
||||
arrayListOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.like.data)).build(),
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||
),
|
||||
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||
);
|
||||
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_rating.visibility = View.VISIBLE;
|
||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(ref, Opinion.like);
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(ref, Opinion.dislike);
|
||||
} else {
|
||||
args.processHandle.opinion(ref, Opinion.neutral);
|
||||
withContext(Dispatchers.Main) {
|
||||
_rating.visibility = View.VISIBLE;
|
||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(ref, Opinion.like);
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(ref, Opinion.dislike);
|
||||
} else {
|
||||
args.processHandle.opinion(ref, Opinion.neutral);
|
||||
}
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
Logger.i(TAG, "Started backfill");
|
||||
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||
Logger.i(TAG, "Finished backfill");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to backfill servers", e)
|
||||
}
|
||||
}
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
Logger.i(TAG, "Started backfill");
|
||||
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||
Logger.i(TAG, "Finished backfill");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to backfill servers", e)
|
||||
}
|
||||
}
|
||||
|
||||
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
|
||||
_rating.visibility = View.GONE;
|
||||
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
|
||||
_rating.visibility = View.GONE;
|
||||
}
|
||||
} else {
|
||||
_rating.visibility = View.GONE;
|
||||
}
|
||||
|
||||
when (video.rating) {
|
||||
@@ -1361,28 +1369,30 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
updateQueueState();
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val historyItem = getHistoryIndex(videoDetail);
|
||||
if (video !is TutorialFragment.TutorialVideo) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val historyItem = getHistoryIndex(videoDetail);
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
||||
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
||||
_layoutResume.visibility = View.VISIBLE;
|
||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
||||
withContext(Dispatchers.Main) {
|
||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
||||
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
||||
_layoutResume.visibility = View.VISIBLE;
|
||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
||||
|
||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
delay(8000);
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set resume changes.", e);
|
||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
delay(8000);
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set resume changes.", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
}
|
||||
} else {
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1954,7 +1964,9 @@ class VideoDetailView : ConstraintLayout {
|
||||
return
|
||||
}
|
||||
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, Models.referenceFromBuffer(idValue.toByteArray())); };
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
val extraBytesRef = video.id.value?.toByteArray()
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); };
|
||||
}
|
||||
private fun fetchVideo() {
|
||||
Logger.i(TAG, "fetchVideo")
|
||||
@@ -2216,9 +2228,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
val v = video ?: return;
|
||||
val currentTime = System.currentTimeMillis();
|
||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val history = getHistoryIndex(v);
|
||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||
if (v !is TutorialFragment.TutorialVideo) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val history = getHistoryIndex(v);
|
||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||
}
|
||||
}
|
||||
_lastPositionSaveTime = currentTime;
|
||||
}
|
||||
@@ -2301,7 +2315,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
||||
|
||||
@@ -56,7 +56,7 @@ class PolycentricCache {
|
||||
|
||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
||||
{ system ->
|
||||
val signedProfileEvents = ApiMethods.getQueryLatest(
|
||||
val signedEventsList = ApiMethods.getQueryLatest(
|
||||
SERVER,
|
||||
system.toProto(),
|
||||
listOf(
|
||||
@@ -72,8 +72,9 @@ class PolycentricCache {
|
||||
ContentType.MEMBERSHIP_URLS.value,
|
||||
ContentType.DONATION_DESTINATIONS.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) }
|
||||
.groupBy { e -> e.event.contentType }
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||
|
||||
val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType }
|
||||
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
||||
|
||||
val storageSystemState = StorageTypeSystemState.create()
|
||||
@@ -151,17 +152,7 @@ class PolycentricCache {
|
||||
|
||||
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
||||
{
|
||||
val urlData = if (it.startsWith("polycentric://")) {
|
||||
it.substring("polycentric://".length)
|
||||
} else it;
|
||||
|
||||
val urlBytes = urlData.base64UrlToByteArray();
|
||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||
if (urlInfo.urlType != 4L) {
|
||||
throw Exception("Only URLInfoDataLink is supported");
|
||||
}
|
||||
|
||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||
val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported");
|
||||
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
||||
},
|
||||
{ return@BatchedTaskHandler null },
|
||||
@@ -325,9 +316,10 @@ class PolycentricCache {
|
||||
.build();
|
||||
|
||||
private const val TAG = "PolycentricCache"
|
||||
const val SERVER = "https://srv1-stg.polycentric.io"
|
||||
const val STAGING_SERVER = "https://srv1-stg.polycentric.io"
|
||||
const val SERVER = "https://srv1-prod.polycentric.io"
|
||||
private var _instance: PolycentricCache? = null;
|
||||
private val CACHE_EXPIRATION_SECONDS = 60 * 60 * 3;
|
||||
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||
|
||||
@JvmStatic
|
||||
val instance: PolycentricCache
|
||||
@@ -343,5 +335,20 @@ class PolycentricCache {
|
||||
it._scope.cancel("PolycentricCache finished");
|
||||
}
|
||||
}
|
||||
|
||||
fun getDataLinkFromUrl(it: String): Protocol.URLInfoDataLink? {
|
||||
val urlData = if (it.startsWith("polycentric://")) {
|
||||
it.substring("polycentric://".length)
|
||||
} else it;
|
||||
|
||||
val urlBytes = urlData.base64UrlToByteArray();
|
||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||
if (urlInfo.urlType != 4L) {
|
||||
return null
|
||||
}
|
||||
|
||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||
return dataLink
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,13 +49,17 @@ class StateCache {
|
||||
Logger.i(TAG, "Subscriptions CachePager get subscriptions");
|
||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
||||
val allUrls = subs.map {
|
||||
val allUrls = subs
|
||||
.map {
|
||||
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
||||
if(!otherUrls.contains(it.channel.url))
|
||||
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
|
||||
else
|
||||
return@map otherUrls;
|
||||
}.flatten().distinct();
|
||||
}
|
||||
.flatten()
|
||||
.distinct()
|
||||
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
||||
|
||||
Logger.i(TAG, "Subscriptions CachePager get pagers");
|
||||
val pagers: List<IPager<IPlatformContent>>;
|
||||
|
||||
@@ -27,7 +27,20 @@ import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringStorage
|
||||
import com.futo.polycentric.core.*
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.ClaimType
|
||||
import com.futo.polycentric.core.ContentType
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.PublicKey
|
||||
import com.futo.polycentric.core.SignedEvent
|
||||
import com.futo.polycentric.core.SqlLiteDbHelper
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.base64ToByteArray
|
||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||
import com.futo.polycentric.core.toBase64
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
@@ -38,7 +51,6 @@ import userpackage.Protocol
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.Exception
|
||||
|
||||
class StatePolycentric {
|
||||
private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean);
|
||||
@@ -128,21 +140,21 @@ class StatePolycentric {
|
||||
_likeDislikeMap[ref.toByteArray().toBase64()] = LikeDislikeEntry(System.currentTimeMillis(), hasLiked, hasDisliked);
|
||||
}
|
||||
|
||||
fun hasDisliked(ref: Protocol.Reference): Boolean {
|
||||
fun hasDisliked(data: ByteArray): Boolean {
|
||||
if (!enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
||||
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||
return entry.hasDisliked;
|
||||
}
|
||||
|
||||
fun hasLiked(ref: Protocol.Reference): Boolean {
|
||||
fun hasLiked(data: ByteArray): Boolean {
|
||||
if (!enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
||||
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||
return entry.hasLiked;
|
||||
}
|
||||
|
||||
@@ -316,7 +328,7 @@ class StatePolycentric {
|
||||
return LikesDislikesReplies(likes, dislikes, replyCount)
|
||||
}
|
||||
|
||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference): IPager<IPlatformComment> {
|
||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
||||
if (!enabled) {
|
||||
return EmptyPager()
|
||||
}
|
||||
@@ -338,7 +350,8 @@ class StatePolycentric {
|
||||
Protocol.QueryReferencesRequestCountReferences.newBuilder()
|
||||
.setFromType(ContentType.POST.value)
|
||||
.build())
|
||||
.build()
|
||||
.build(),
|
||||
extraByteReferences = extraByteReferences
|
||||
);
|
||||
|
||||
val results = mapQueryReferences(contextUrl, response);
|
||||
@@ -407,7 +420,8 @@ class StatePolycentric {
|
||||
ContentType.AVATAR.value,
|
||||
ContentType.USERNAME.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
|
||||
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
|
||||
|
||||
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
|
||||
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
|
||||
|
||||
@@ -51,10 +51,16 @@ class StateSubscriptions {
|
||||
|
||||
val global: CentralizedFeed = CentralizedFeed();
|
||||
val feeds: HashMap<String, CentralizedFeed> = hashMapOf();
|
||||
val onFeedProgress = Event3<String, Int, Int>();
|
||||
val onFeedProgress = Event3<String?, Int, Int>();
|
||||
|
||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||
|
||||
init {
|
||||
global.onUpdateProgress.subscribe { progress, total ->
|
||||
onFeedProgress.emit(null, progress, total);
|
||||
}
|
||||
}
|
||||
|
||||
fun getOldestUpdateTime(): OffsetDateTime {
|
||||
val subs = getSubscriptions();
|
||||
if(subs.size == 0)
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PointF
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import java.security.MessageDigest
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class IdenticonView(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||
var hashString: String = "default"
|
||||
set(value) {
|
||||
field = value
|
||||
hash = md5(value)
|
||||
iconGenerator = null
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private var hash = ByteArray(16)
|
||||
private var iconGenerator: IconGenerator? = null
|
||||
private val path = Path()
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
val radius = (width.coerceAtMost(height) / 2).toFloat()
|
||||
val clipPath = path.apply {
|
||||
reset()
|
||||
addCircle(width / 2f, height / 2f, radius, Path.Direction.CW)
|
||||
}
|
||||
|
||||
canvas.clipPath(clipPath)
|
||||
|
||||
if (iconGenerator == null) {
|
||||
iconGenerator = IconGenerator(min(height, width).toFloat(), hash)
|
||||
}
|
||||
|
||||
iconGenerator?.render(canvas)
|
||||
}
|
||||
|
||||
private fun md5(input: String): ByteArray {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
return md.digest(input.toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
|
||||
interface Shape {
|
||||
fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint)
|
||||
}
|
||||
|
||||
class CutCorner : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val k = size * 0.42f
|
||||
val path = Path().apply {
|
||||
moveTo(0f, 0f)
|
||||
lineTo(size, 0f)
|
||||
lineTo(size, size - k * 2)
|
||||
lineTo(size - k, size)
|
||||
lineTo(0f, size)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class SideTriangle : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val w = size / 2
|
||||
val h = size * 0.8f
|
||||
val path = Path().apply {
|
||||
moveTo(size - w, 0f)
|
||||
lineTo(size, h)
|
||||
lineTo(size - w, h)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class MiddleSquare : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val s = size / 3
|
||||
canvas.drawRect(s, s, size - s, size - s, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class CornerSquare : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val inner = size * 0.1f
|
||||
val outer = max(1f, size * 0.25f)
|
||||
canvas.drawRect(outer, outer, size - inner - outer, size - inner - outer, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class OffCenterCircle : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val m = size * 0.15f
|
||||
val s = size * 0.5f
|
||||
canvas.drawCircle(size - s - m, size - s - m, s / 2, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class NegativeTriangle : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val inner = size * 0.1f
|
||||
val outer = inner * 4
|
||||
val path = Path().apply {
|
||||
addRect(0f, 0f, size, size, Path.Direction.CW)
|
||||
moveTo(outer, outer)
|
||||
lineTo(size - inner, outer)
|
||||
lineTo(outer + (size - outer - inner) / 2, size - inner)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class CutSquare : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val path = Path().apply {
|
||||
moveTo(0f, 0f)
|
||||
lineTo(size, 0f)
|
||||
lineTo(size, size * 0.7f)
|
||||
lineTo(size * 0.4f, size * 0.4f)
|
||||
lineTo(size * 0.7f, size)
|
||||
lineTo(0f, size)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class CornerPlusTriangle : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val halfSize = size / 2
|
||||
canvas.drawRect(0f, 0f, size, halfSize, paint)
|
||||
canvas.drawRect(0f, halfSize, halfSize, size, paint)
|
||||
val path = Path().apply {
|
||||
moveTo(halfSize, halfSize)
|
||||
lineTo(size, halfSize)
|
||||
lineTo(halfSize, size)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class NegativeSquare : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val inner = size * 0.14f
|
||||
val outer = size * 0.35f
|
||||
val path = Path().apply {
|
||||
addRect(0f, 0f, size, size, Path.Direction.CW)
|
||||
addRect(outer, outer, size - outer - inner, size - outer - inner, Path.Direction.CCW)
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class NegativeCircle : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val inner = size * 0.12f
|
||||
val outer = inner * 3
|
||||
val path = Path().apply {
|
||||
addRect(0f, 0f, size, size, Path.Direction.CW)
|
||||
addCircle(outer, outer, (size - inner - outer) / 2, Path.Direction.CCW)
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class NegativeRhombus : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val m = size * 0.25f
|
||||
val path = Path().apply {
|
||||
addRect(0f, 0f, size, size, Path.Direction.CW)
|
||||
moveTo(m, size / 2)
|
||||
lineTo(size / 2, m)
|
||||
lineTo(size - m, size / 2)
|
||||
lineTo(size / 2, size - m)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class ConditionalCircle : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
if (index == 0) {
|
||||
val m = size * 0.4f
|
||||
val s = size * 1.2f
|
||||
canvas.drawCircle(m, m, s / 2, paint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HalfTriangle : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val path = Path().apply {
|
||||
moveTo(size / 2, size / 2)
|
||||
lineTo(size, size / 2)
|
||||
lineTo(size / 2, size)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class Triangle(val corner: Int = 0) : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val path = Path().apply {
|
||||
when (corner) {
|
||||
0 -> {
|
||||
moveTo(0f, 0f)
|
||||
lineTo(size, 0f)
|
||||
lineTo(0f, size)
|
||||
}
|
||||
1 -> {
|
||||
moveTo(size, 0f)
|
||||
lineTo(size, size)
|
||||
lineTo(0f, size)
|
||||
}
|
||||
2 -> {
|
||||
moveTo(0f, 0f)
|
||||
lineTo(size, 0f)
|
||||
lineTo(size, size)
|
||||
}
|
||||
3 -> {
|
||||
moveTo(0f, 0f)
|
||||
lineTo(0f, size)
|
||||
lineTo(size, size)
|
||||
}
|
||||
}
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class BottomHalfTriangle : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val path = Path().apply {
|
||||
moveTo(0f, size / 2)
|
||||
lineTo(size, size / 2)
|
||||
lineTo(size / 2, size)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class Rhombus : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val path = Path().apply {
|
||||
moveTo(size / 2, 0f)
|
||||
lineTo(size, size / 2)
|
||||
lineTo(size / 2, size)
|
||||
lineTo(0f, size / 2)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class Circle : Shape {
|
||||
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||
val m = size / 6
|
||||
canvas.drawCircle(m, m, size / 2 - m, paint)
|
||||
}
|
||||
}
|
||||
|
||||
class IconGenerator(private val size: Float, private val hash: ByteArray) {
|
||||
private val digits: ByteArray
|
||||
private var selectedColors = arrayOf<Paint>()
|
||||
|
||||
init {
|
||||
digits = ByteArray(max(12, hash.size * 2))
|
||||
var index = 0
|
||||
for (byte in hash) {
|
||||
if (index >= digits.size) {
|
||||
break
|
||||
}
|
||||
digits[index] = ((byte.toInt() shr 4) and 0x0f).toByte()
|
||||
digits[index + 1] = (byte.toInt() and 0x0f).toByte()
|
||||
index += 2
|
||||
}
|
||||
selectColors()
|
||||
}
|
||||
|
||||
private fun selectColors() {
|
||||
val value = hash.copyOfRange(hash.size - 4, hash.size).fold(0) { acc, byte ->
|
||||
(acc shl 8) or (byte.toInt() and 0xFF)
|
||||
} and 0x0FFFFFFF
|
||||
val colorTheme = ColorTheme(hue = value.toFloat() / 0x0FFFFFFF)
|
||||
|
||||
val selectedColorIndices = mutableListOf<Int>()
|
||||
for (i in 0 until 3) {
|
||||
val index = (digits[8 + i].toInt() % colorTheme.colors.size)
|
||||
selectedColorIndices.add(colorTheme.validateIndex(index, selectedColorIndices))
|
||||
}
|
||||
|
||||
selectedColors = selectedColorIndices.map { index ->
|
||||
Paint().apply {
|
||||
color = colorTheme.colors[index]
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
fun renderBitmap(): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(size.toInt(), size.toInt(), Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
render(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun render(canvas: Canvas) {
|
||||
canvas.drawColor(Color.WHITE)
|
||||
|
||||
renderShape(canvas, 0, outerShapes, 2, 3, arrayOf(
|
||||
PointF(1f, 0f),
|
||||
PointF(2f, 0f),
|
||||
PointF(2f, 3f),
|
||||
PointF(1f, 3f),
|
||||
PointF(0f, 1f),
|
||||
PointF(3f, 1f),
|
||||
PointF(3f, 2f),
|
||||
PointF(0f, 2f),
|
||||
))
|
||||
renderShape(canvas, 1, outerShapes, 4, 5, arrayOf(
|
||||
PointF(0f, 0f),
|
||||
PointF(3f, 0f),
|
||||
PointF(3f, 3f),
|
||||
PointF(0f, 3f),
|
||||
))
|
||||
renderShape(canvas, 2, centerShapes, 1, null, arrayOf(
|
||||
PointF(1f, 1f),
|
||||
PointF(2f, 1f),
|
||||
PointF(2f, 2f),
|
||||
PointF(1f, 2f),
|
||||
))
|
||||
}
|
||||
|
||||
private fun renderShape(
|
||||
canvas: Canvas,
|
||||
colorIndex: Int,
|
||||
shapes: Array<Shape>,
|
||||
index: Int,
|
||||
rotationIndex: Int?,
|
||||
positions: Array<PointF>
|
||||
) {
|
||||
val cellSize = size / 4
|
||||
var r = rotationIndex?.let { digits[it].toInt() } ?: 0
|
||||
val shape = shapes[digits[index].toInt() % shapes.size]
|
||||
|
||||
val paint = Paint().apply {
|
||||
color = selectedColors[colorIndex % selectedColors.size].color
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
for ((idx, position) in positions.withIndex()) {
|
||||
canvas.save()
|
||||
canvas.translate(position.x * cellSize, position.y * cellSize)
|
||||
canvas.translate(cellSize / 2, cellSize / 2)
|
||||
canvas.rotate((r % 4) * 90f)
|
||||
canvas.translate(-cellSize / 2, -cellSize / 2)
|
||||
|
||||
shape.draw(canvas, cellSize, idx, paint)
|
||||
canvas.restore()
|
||||
r++
|
||||
}
|
||||
}
|
||||
|
||||
class ColorTheme(val hue: Float, val saturation: Float = 0.5f) {
|
||||
val colors: List<Int>
|
||||
|
||||
init {
|
||||
colors = listOf(
|
||||
// Dark gray
|
||||
grayscaleColor(0f),
|
||||
// Mid color
|
||||
hslColor(hue, saturation, colorLightness(0.5f)),
|
||||
// Light gray
|
||||
grayscaleColor(1f),
|
||||
// Light color
|
||||
hslColor(hue, saturation, colorLightness(1f)),
|
||||
// Dark color
|
||||
hslColor(hue, saturation, colorLightness(0f))
|
||||
)
|
||||
}
|
||||
|
||||
fun validateIndex(index: Int, selected: List<Int>): Int {
|
||||
return if (isDuplicate(index, listOf(0, 4), selected) || isDuplicate(index, listOf(2, 3), selected)) {
|
||||
1
|
||||
} else {
|
||||
index
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDuplicate(index: Int, values: List<Int>, selected: List<Int>): Boolean {
|
||||
if (!values.contains(index)) return false
|
||||
return values.any { selected.contains(it) }
|
||||
}
|
||||
|
||||
private fun colorLightness(value: Float): Float = lightness(value, 0.4f, 0.8f)
|
||||
|
||||
private fun grayscaleLightness(value: Float): Float = lightness(value, 0.3f, 0.9f)
|
||||
|
||||
private fun lightness(value: Float, min: Float, max: Float): Float {
|
||||
val lightness = min + value * (max - min)
|
||||
return minOf(1f, maxOf(0f, lightness))
|
||||
}
|
||||
|
||||
private fun grayscaleColor(lightness: Float): Int {
|
||||
return Color.HSVToColor(floatArrayOf(0f, 0f, lightness))
|
||||
}
|
||||
|
||||
private fun hslColor(hue: Float, saturation: Float, lightness: Float): Int {
|
||||
return Color.HSVToColor(floatArrayOf(hue, saturation, lightness))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val centerShapes = arrayOf(
|
||||
CutCorner(),
|
||||
SideTriangle(),
|
||||
MiddleSquare(),
|
||||
CornerSquare(),
|
||||
OffCenterCircle(),
|
||||
NegativeTriangle(),
|
||||
CutSquare(),
|
||||
HalfTriangle(),
|
||||
CornerPlusTriangle(),
|
||||
CutSquare(),
|
||||
NegativeCircle(),
|
||||
HalfTriangle(),
|
||||
NegativeRhombus(),
|
||||
ConditionalCircle()
|
||||
)
|
||||
|
||||
val outerShapes = arrayOf(
|
||||
Triangle(),
|
||||
BottomHalfTriangle(),
|
||||
Rhombus(),
|
||||
Circle(),
|
||||
)
|
||||
|
||||
private const val TAG = "IdenticonView"
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,21 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.pills.PillButton
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
@@ -104,7 +110,8 @@ class CommentViewHolder : ViewHolder {
|
||||
|
||||
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
||||
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
|
||||
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
|
||||
_textAuthor.text = comment.author.name;
|
||||
|
||||
val date = comment.date;
|
||||
@@ -161,8 +168,8 @@ class CommentViewHolder : ViewHolder {
|
||||
_pillRatingLikesDislikes.visibility = View.VISIBLE;
|
||||
|
||||
if (comment is PolycentricPlatformComment) {
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference);
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference.toByteArray());
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference.toByteArray());
|
||||
_pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked);
|
||||
} else {
|
||||
_pillRatingLikesDislikes.setRating(comment.rating);
|
||||
|
||||
+4
-3
@@ -126,7 +126,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
_taskGetLiveComment.cancel()
|
||||
|
||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
||||
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
|
||||
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
|
||||
_textAuthor.text = comment.author.name;
|
||||
|
||||
val date = comment.date;
|
||||
@@ -168,8 +169,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
if (likesDislikesReplies != null) {
|
||||
Log.i(TAG, "updateLikesDislikesReplies set")
|
||||
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(c.reference);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference);
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(c.reference.toByteArray());
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference.toByteArray());
|
||||
_pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
|
||||
|
||||
_buttonReplies.setLoading(false)
|
||||
|
||||
@@ -7,7 +7,7 @@ import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
@@ -18,8 +18,8 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
|
||||
|
||||
@@ -149,7 +149,8 @@ open class PlaylistView : LinearLayout {
|
||||
_neopassAnimator?.cancel();
|
||||
_neopassAnimator = null;
|
||||
|
||||
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
|
||||
val firstClaim = claims?.ownedClaims?.firstOrNull();
|
||||
val harborAvailable = firstClaim != null
|
||||
if (harborAvailable) {
|
||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
@@ -160,7 +161,7 @@ open class PlaylistView : LinearLayout {
|
||||
_imageNeopassChannel?.visibility = View.GONE
|
||||
}
|
||||
|
||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
|
||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -6,21 +6,18 @@ import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.toHumanBytesSpeed
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanTimeIndicator
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
@@ -107,7 +104,7 @@ class SubscriptionViewHolder : ViewHolder {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.debug.Stopwatch
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
@@ -46,7 +47,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
val contentDetails = StatePlatform.instance.getContentDetails(video.url).await();
|
||||
stopwatch.logAndNext(TAG, "Retrieving video detail (IO thread)")
|
||||
return@TaskHandler Pair(viewHolder, contentDetails)
|
||||
}).success { previewContentDetails(it.first, it.second) }
|
||||
}).exception<Throwable> { Logger.e(TAG, "Failed to retrieve preview content.", it) }.success { previewContentDetails(it.first, it.second) }
|
||||
|
||||
constructor(context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
|
||||
initialPlay: Boolean = false, viewsToPrepend: ArrayList<View> = arrayListOf(),
|
||||
|
||||
+1
-1
@@ -334,7 +334,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
} else if (_imageChannel != null) {
|
||||
val dp_28 = 28.dp(context.resources);
|
||||
|
||||
+2
-5
@@ -1,6 +1,5 @@
|
||||
package com.futo.platformplayer.views.adapters.viewholders
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
@@ -8,12 +7,10 @@ import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
@@ -76,7 +73,7 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
@@ -148,7 +145,7 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
||||
+11
-24
@@ -3,45 +3,31 @@ package com.futo.platformplayer.views.adapters.viewholders
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
||||
class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
|
||||
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_group_bar, _viewGroup, false)) {
|
||||
private var _group: SubscriptionGroup? = null;
|
||||
|
||||
private val _root: FrameLayout;
|
||||
private val _image: ShapeableImageView;
|
||||
private val _textSubGroup: TextView;
|
||||
|
||||
|
||||
val onClick = Event1<SubscriptionGroup>();
|
||||
val onClickLong = Event1<SubscriptionGroup>();
|
||||
|
||||
init {
|
||||
_root = _view.findViewById(R.id.root);
|
||||
_image = _view.findViewById(R.id.image);
|
||||
_textSubGroup = _view.findViewById(R.id.text_sub_group);
|
||||
|
||||
val dp6 = 6.dp(_view.resources);
|
||||
_image.shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
|
||||
.build()
|
||||
|
||||
_view.setOnClickListener {
|
||||
_group?.let {
|
||||
onClick.emit(it);
|
||||
@@ -58,9 +44,9 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
override fun bind(value: SubscriptionGroup) {
|
||||
_group = value;
|
||||
val img = value.image;
|
||||
if(img != null)
|
||||
if(img != null) {
|
||||
img.setImageView(_image)
|
||||
else {
|
||||
} else {
|
||||
_image.setImageResource(0);
|
||||
|
||||
if(value is SubscriptionGroup.Add)
|
||||
@@ -68,10 +54,11 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
}
|
||||
_textSubGroup.text = value.name;
|
||||
|
||||
if(value is SubscriptionGroup.Selectable && value.selected)
|
||||
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
|
||||
else
|
||||
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
|
||||
if (value is SubscriptionGroup.Selectable && value.selected) {
|
||||
_root.setBackgroundResource(R.drawable.background_primary_round_6dp)
|
||||
} else {
|
||||
_root.background = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -18,12 +18,20 @@ import android.widget.TextView
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.animation.doOnStart
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.others.CircularProgressBar
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class GestureControlView : LinearLayout {
|
||||
private val _scope = CoroutineScope(Dispatchers.Main);
|
||||
@@ -95,22 +103,23 @@ class GestureControlView : LinearLayout {
|
||||
if(p0 == null)
|
||||
return false;
|
||||
|
||||
val minDistance = Math.min(width, height)
|
||||
if (_isFullScreen && _adjustingBrightness) {
|
||||
val adjustAmount = (distanceY * 2) / height;
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_progressBrightness.progress = _brightnessFactor;
|
||||
onBrightnessAdjusted.emit(_brightnessFactor);
|
||||
} else if (_isFullScreen && _adjustingSound) {
|
||||
val adjustAmount = (distanceY * 2) / height;
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_progressSound.progress = _soundFactor;
|
||||
onSoundAdjusted.emit(_soundFactor);
|
||||
} else if (_adjustingFullscreenUp) {
|
||||
val adjustAmount = (distanceY * 2) / height;
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_layoutControlsFullscreen.alpha = _fullScreenFactorUp;
|
||||
} else if (_adjustingFullscreenDown) {
|
||||
val adjustAmount = (-distanceY * 2) / height;
|
||||
val adjustAmount = (-distanceY * 2) / minDistance;
|
||||
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
|
||||
} else {
|
||||
|
||||
@@ -12,12 +12,16 @@ import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.views.IdenticonView
|
||||
import userpackage.Protocol
|
||||
|
||||
class CreatorThumbnail : ConstraintLayout {
|
||||
private val _root: ConstraintLayout;
|
||||
private val _imageChannelThumbnail: ImageView;
|
||||
private val _imageNewActivity: ImageView;
|
||||
private val _imageNeoPass: ImageView;
|
||||
private val _identicon: IdenticonView;
|
||||
private var _harborAnimator: ObjectAnimator? = null;
|
||||
private var _imageAnimator: ObjectAnimator? = null;
|
||||
|
||||
@@ -28,19 +32,23 @@ class CreatorThumbnail : ConstraintLayout {
|
||||
|
||||
_root = findViewById(R.id.root);
|
||||
_imageChannelThumbnail = findViewById(R.id.image_channel_thumbnail);
|
||||
_identicon = findViewById(R.id.identicon);
|
||||
_imageChannelThumbnail.clipToOutline = true;
|
||||
_identicon.clipToOutline = true;
|
||||
_imageChannelThumbnail.visibility = View.GONE
|
||||
_imageNewActivity = findViewById(R.id.image_new_activity);
|
||||
_imageNeoPass = findViewById(R.id.image_neopass);
|
||||
|
||||
if (!isInEditMode) {
|
||||
setHarborAvailable(false, animate = false);
|
||||
setHarborAvailable(false, animate = false, system = null);
|
||||
setNewActivity(false);
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_imageChannelThumbnail.visibility = View.GONE;
|
||||
_imageChannelThumbnail.setImageResource(R.drawable.placeholder_channel_thumbnail);
|
||||
setHarborAvailable(false, animate = false);
|
||||
setHarborAvailable(false, animate = false, system = null);
|
||||
setNewActivity(false);
|
||||
}
|
||||
|
||||
@@ -50,13 +58,24 @@ class CreatorThumbnail : ConstraintLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
_imageChannelThumbnail.visibility = View.VISIBLE;
|
||||
|
||||
_harborAnimator?.cancel();
|
||||
_harborAnimator = null;
|
||||
|
||||
_imageAnimator?.cancel();
|
||||
_imageAnimator = null;
|
||||
|
||||
setHarborAvailable(url.startsWith("polycentric://"), animate);
|
||||
if (url.startsWith("polycentric://")) {
|
||||
try {
|
||||
val dataLink = PolycentricCache.getDataLinkFromUrl(url)
|
||||
setHarborAvailable(true, animate, dataLink?.system);
|
||||
} catch (e: Throwable) {
|
||||
setHarborAvailable(false, animate, null);
|
||||
}
|
||||
} else {
|
||||
setHarborAvailable(false, animate, null);
|
||||
}
|
||||
|
||||
if (animate) {
|
||||
Glide.with(_imageChannelThumbnail)
|
||||
@@ -72,7 +91,7 @@ class CreatorThumbnail : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun setHarborAvailable(available: Boolean, animate: Boolean) {
|
||||
fun setHarborAvailable(available: Boolean, animate: Boolean, system: Protocol.PublicKey?) {
|
||||
_harborAnimator?.cancel();
|
||||
_harborAnimator = null;
|
||||
|
||||
@@ -85,6 +104,13 @@ class CreatorThumbnail : ConstraintLayout {
|
||||
} else {
|
||||
_imageNeoPass.visibility = View.GONE;
|
||||
}
|
||||
|
||||
if (system != null) {
|
||||
_identicon.hashString = system.toString()
|
||||
_identicon.visibility = View.VISIBLE
|
||||
} else {
|
||||
_identicon.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun setChannelImageResource(resource: Int?, animate: Boolean) {
|
||||
|
||||
@@ -1,55 +1,32 @@
|
||||
package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.shapes.Shape
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import android.widget.FrameLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.PresetImages
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
||||
import com.futo.platformplayer.views.SearchView
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SelectableCreatorBarViewHolder
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import java.io.File
|
||||
|
||||
class CreatorSelectOverlay: ConstraintLayout {
|
||||
private val _buttonSelect: Button;
|
||||
private val _buttonSelect: FrameLayout;
|
||||
private val _topbar: OverlayTopbar;
|
||||
|
||||
private val _searchBar: SearchView;
|
||||
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
|
||||
|
||||
private val _creators: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
|
||||
private val _creatorsFiltered: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
|
||||
|
||||
private var _selected: MutableList<String> = mutableListOf();
|
||||
|
||||
@@ -66,7 +43,7 @@ class CreatorSelectOverlay: ConstraintLayout {
|
||||
else
|
||||
_creators.addAll(subs
|
||||
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
|
||||
_recyclerCreators.notifyContentChanged();
|
||||
filterCreators();
|
||||
}
|
||||
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { }
|
||||
init {
|
||||
@@ -74,7 +51,8 @@ class CreatorSelectOverlay: ConstraintLayout {
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_buttonSelect = findViewById(R.id.button_select);
|
||||
val dp6 = 6.dp(resources);
|
||||
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creators, RecyclerView.HORIZONTAL) { creatorView ->
|
||||
_searchBar = findViewById(R.id.search_bar);
|
||||
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creatorsFiltered, RecyclerView.HORIZONTAL) { creatorView ->
|
||||
creatorView.itemView.setPadding(0, dp6, 0, dp6);
|
||||
creatorView.onClick.subscribe {
|
||||
if(it.channel.thumbnail == null) {
|
||||
@@ -92,19 +70,33 @@ class CreatorSelectOverlay: ConstraintLayout {
|
||||
this.orientation = LinearLayoutManager.VERTICAL;
|
||||
};
|
||||
_buttonSelect.setOnClickListener {
|
||||
_selected?.let {
|
||||
if (_selected.isNotEmpty()) {
|
||||
select();
|
||||
}
|
||||
};
|
||||
_topbar.onClose.subscribe {
|
||||
onClose.emit();
|
||||
}
|
||||
_searchBar.onSearchChanged.subscribe {
|
||||
filterCreators();
|
||||
};
|
||||
updateSelected();
|
||||
filterCreators();
|
||||
}
|
||||
|
||||
fun updateSelected() {
|
||||
_creators.forEach { p -> p.active = _selected.contains(p.channel.url) };
|
||||
_recyclerCreators.notifyContentChanged();
|
||||
val changed = arrayListOf<SelectableCreatorBarViewHolder.Selectable>()
|
||||
for(creator in _creators) {
|
||||
val act = _selected.contains(creator.channel.url);
|
||||
if(creator.active != act) {
|
||||
creator.active = act;
|
||||
changed.add(creator);
|
||||
}
|
||||
}
|
||||
for(change in changed) {
|
||||
val index = _creatorsFiltered.indexOf(change);
|
||||
_recyclerCreators.notifyContentChanged(index);
|
||||
}
|
||||
|
||||
if(_selected.isNotEmpty())
|
||||
_buttonSelect.alpha = 1f;
|
||||
@@ -113,6 +105,17 @@ class CreatorSelectOverlay: ConstraintLayout {
|
||||
}
|
||||
|
||||
|
||||
private fun filterCreators(withUpdate: Boolean = true) {
|
||||
val query = _searchBar.textSearch.text.toString().lowercase();
|
||||
val filteredEnabled = _creators.filter { query.isEmpty() || it.channel.name.lowercase().contains(query) };
|
||||
|
||||
//Optimize
|
||||
_creatorsFiltered.clear();
|
||||
_creatorsFiltered.addAll(filteredEnabled);
|
||||
if(withUpdate)
|
||||
_recyclerCreators.notifyContentChanged();
|
||||
}
|
||||
|
||||
fun select() {
|
||||
if(_creators.isEmpty())
|
||||
return;
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.net.toFile
|
||||
@@ -48,7 +49,7 @@ class ImageVariableOverlay: ConstraintLayout {
|
||||
private val _buttonGallery: BigButton;
|
||||
private val _imageGallerySelected: ImageView;
|
||||
private val _imageGallerySelectedContainer: LinearLayout;
|
||||
private val _buttonSelect: Button;
|
||||
private val _buttonSelect: TextView;
|
||||
private val _topbar: OverlayTopbar;
|
||||
private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>;
|
||||
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.views.overlays
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
@@ -16,6 +17,21 @@ class LoaderOverlay(context: Context, attrs: AttributeSet?) : FrameLayout(contex
|
||||
inflate(context, R.layout.overlay_loader, this);
|
||||
_container = findViewById(R.id.container);
|
||||
_loader = findViewById(R.id.loader);
|
||||
|
||||
val centerLoader: Boolean;
|
||||
if (attrs != null) {
|
||||
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.LoaderOverlay, 0, 0);
|
||||
centerLoader = attrArr.getBoolean(R.styleable.LoaderOverlay_centerLoader, false);
|
||||
attrArr.recycle();
|
||||
} else {
|
||||
centerLoader = false;
|
||||
}
|
||||
|
||||
if (centerLoader) {
|
||||
(_loader.layoutParams as LayoutParams).apply {
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun show() {
|
||||
|
||||
@@ -3,10 +3,13 @@ package com.futo.platformplayer.views.overlays
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.marginRight
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||
|
||||
class OverlayTopbar : ConstraintLayout {
|
||||
@@ -16,6 +19,8 @@ class OverlayTopbar : ConstraintLayout {
|
||||
|
||||
private val _button_close: ImageView;
|
||||
|
||||
private val _button_list: LinearLayout;
|
||||
|
||||
val onClose = Event0();
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
@@ -24,6 +29,7 @@ class OverlayTopbar : ConstraintLayout {
|
||||
_name = findViewById(R.id.text_name);
|
||||
_meta = findViewById(R.id.text_meta);
|
||||
_button_close = findViewById(R.id.button_close);
|
||||
_button_list = findViewById(R.id.button_list);
|
||||
|
||||
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.OverlayTopbar, 0, 0);
|
||||
val attrText = attrArr.getText(R.styleable.OverlayTopbar_title) ?: "";
|
||||
@@ -42,4 +48,20 @@ class OverlayTopbar : ConstraintLayout {
|
||||
_name.text = name;
|
||||
_meta.text = meta;
|
||||
}
|
||||
|
||||
fun setButtons(vararg buttons: Pair<Int, ()->Unit>) {
|
||||
_button_list.removeAllViews();
|
||||
val dp40 = 40.dp(resources);
|
||||
val dp5 = 5.dp(resources);
|
||||
for(button in buttons) {
|
||||
_button_list.addView(ImageView(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(dp40, dp40)
|
||||
setPadding(dp5, dp5, dp5 * 2, dp5);
|
||||
setImageResource(button.first);
|
||||
setOnClickListener {
|
||||
button.second();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
@@ -102,7 +102,8 @@ class RepliesOverlay : LinearLayout {
|
||||
}
|
||||
|
||||
_creatorThumbnail.setThumbnail(parentComment.author.thumbnail, false);
|
||||
_creatorThumbnail.setHarborAvailable(parentComment is PolycentricPlatformComment,false);
|
||||
val polycentricPlatformComment = if (parentComment is PolycentricPlatformComment) parentComment else null
|
||||
_creatorThumbnail.setHarborAvailable(polycentricPlatformComment != null,false, polycentricPlatformComment?.eventPointer?.system?.toProto());
|
||||
}
|
||||
|
||||
_topbar.setInfo(context.getString(R.string.Replies), metadata);
|
||||
|
||||
@@ -2,12 +2,14 @@ package com.futo.platformplayer.views.subscriptions
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
@@ -24,7 +26,8 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class SubscriptionBar : LinearLayout {
|
||||
private var _adapterView: AnyAdapterView<Subscription, SubscriptionBarViewHolder>? = null;
|
||||
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>
|
||||
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>;
|
||||
private var _subGroupsExplore: SubscriptionExploreButton;
|
||||
private val _tagsContainer: LinearLayout;
|
||||
|
||||
private val _groups: ArrayList<SubscriptionGroup>;
|
||||
@@ -64,7 +67,32 @@ class SubscriptionBar : LinearLayout {
|
||||
onHoldGroup.emit(g);
|
||||
}
|
||||
}
|
||||
_subGroupsExplore = findViewById(R.id.subgroup_explore);
|
||||
_tagsContainer = findViewById(R.id.container_tags);
|
||||
|
||||
_subGroupsExplore.onClick.subscribe {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_subscriptions, "Subscription Groups",
|
||||
"Subscription groups are an easy way to navigate your subscriptions.\n\nDefine your own subsets, and in the near future share them with others.", null, 0,
|
||||
UIDialogs.Action("Hide Bar", {
|
||||
Settings.instance.subscriptions.showSubscriptionGroups = false;
|
||||
Settings.instance.save();
|
||||
reloadGroups();
|
||||
|
||||
UIDialogs.showDialogOk(context, R.drawable.ic_quiz, "Subscription groups can be re-enabled in settings")
|
||||
}),
|
||||
UIDialogs.Action("Create", {
|
||||
onToggleGroup.emit(SubscriptionGroup.Add()); //Shortcut..
|
||||
}, UIDialogs.ActionStyle.PRIMARY))
|
||||
};
|
||||
|
||||
updateExplore();
|
||||
}
|
||||
|
||||
fun selectGroup(group: SubscriptionGroup) {
|
||||
val relevantGroup = _groups.find { it.id == group.id };
|
||||
if(relevantGroup != null && _group != relevantGroup) {
|
||||
groupClicked(relevantGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private fun groupClicked(g: SubscriptionGroup) {
|
||||
@@ -100,6 +128,8 @@ class SubscriptionBar : LinearLayout {
|
||||
_groups.clear();
|
||||
_groups.addAll(results);
|
||||
_subGroups.notifyContentChanged();
|
||||
|
||||
updateExplore();
|
||||
}
|
||||
private fun getGroups(): List<SubscriptionGroup> {
|
||||
return if(Settings.instance.subscriptions.showSubscriptionGroups)
|
||||
@@ -110,6 +140,18 @@ class SubscriptionBar : LinearLayout {
|
||||
else listOf();
|
||||
}
|
||||
|
||||
fun updateExplore() {
|
||||
val show = Settings.instance.subscriptions.showSubscriptionGroups &&
|
||||
_groups.all { it is SubscriptionGroup.Add };
|
||||
if(show) {
|
||||
_subGroupsExplore.visibility = View.VISIBLE;
|
||||
_subGroups.view.visibility = View.GONE;
|
||||
}
|
||||
else {
|
||||
_subGroupsExplore.visibility = View.GONE;
|
||||
_subGroups.view.visibility = View.VISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
fun setToggles(vararg buttons: Toggle) {
|
||||
_tagsContainer.removeAllViews();
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.futo.platformplayer.views.subscriptions
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
||||
class SubscriptionExploreButton : ConstraintLayout {
|
||||
val onClick = Event0();
|
||||
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_subscription_group_bar_explore, this);
|
||||
|
||||
val dp10 = 10.dp(resources);
|
||||
findViewById<ShapeableImageView>(R.id.image)
|
||||
.apply {
|
||||
adjustViewBounds = true
|
||||
scaleType = ImageView.ScaleType.CENTER_CROP;
|
||||
shapeAppearanceModel = ShapeAppearanceModel.builder().setAllCorners(CornerFamily.ROUNDED, dp10.toFloat()).build()
|
||||
}
|
||||
|
||||
findViewById<ConstraintLayout>(R.id.root).setOnClickListener {
|
||||
onClick.emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -11,6 +14,9 @@ import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.PlayerControlView
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
@@ -39,6 +45,14 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
|
||||
|
||||
//Events
|
||||
private val _evMuteChanged = mutableListOf<(FutoThumbnailPlayer, Boolean)->Unit>();
|
||||
private val _loadArtwork = object: CustomTarget<Bitmap>() {
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
setArtwork(BitmapDrawable(resources, resource));
|
||||
}
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
setArtwork(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@@ -113,11 +127,38 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
|
||||
fun setPreview(video: IPlatformVideoDetails) {
|
||||
val videoSource = VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
|
||||
val audioSource = VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
setSource(videoSource, audioSource,true, false);
|
||||
if (video.live != null) {
|
||||
setSource(video.live, null,true, false);
|
||||
} else {
|
||||
val videoSource = VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
|
||||
val audioSource = VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
if (videoSource == null && audioSource != null) {
|
||||
val thumbnail = video.thumbnails.getHQThumbnail();
|
||||
if (!thumbnail.isNullOrBlank()) {
|
||||
Glide.with(videoView).asBitmap().load(thumbnail).into(_loadArtwork);
|
||||
} else {
|
||||
Glide.with(videoView).clear(_loadArtwork);
|
||||
setArtwork(null);
|
||||
}
|
||||
} else {
|
||||
Glide.with(videoView).clear(_loadArtwork);
|
||||
}
|
||||
|
||||
setSource(videoSource, audioSource,true, false);
|
||||
}
|
||||
}
|
||||
override fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun setArtwork(drawable: Drawable?) {
|
||||
if (drawable != null) {
|
||||
videoView.defaultArtwork = drawable;
|
||||
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL;
|
||||
} else {
|
||||
videoView.defaultArtwork = null;
|
||||
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:radius="10dp"
|
||||
android:topRightRadius="10dp"
|
||||
android:bottomRightRadius="10dp"
|
||||
android:bottomLeftRadius="10dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#6F6F6F" />
|
||||
</shape>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:radius="10dp"
|
||||
android:topRightRadius="10dp"
|
||||
android:bottomRightRadius="10dp"
|
||||
android:bottomLeftRadius="10dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#6F6F6F" />
|
||||
<solid android:color="#99000000" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#99000000" />
|
||||
<corners android:radius="6dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#2D63ED" />
|
||||
<corners android:radius="6dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -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="M481.54,685.39Q494.85,685.39 503.96,676.27Q513.08,667.15 513.08,653.85Q513.08,640.54 503.96,631.42Q494.85,622.31 481.54,622.31Q468.23,622.31 459.11,631.42Q450,640.54 450,653.85Q450,667.15 459.11,676.27Q468.23,685.39 481.54,685.39ZM460.92,552.92L499.54,552.92Q501.08,526.15 509.46,510.31Q517.85,494.46 541.54,470.77Q570.39,441.92 583.35,420.73Q596.31,399.54 596.31,372.61Q596.31,325.77 564.15,297.11Q532,268.46 485.61,268.46Q442.92,268.46 412.11,291.23Q381.31,314 367.08,345.85L403.85,361.08Q413.92,337.15 432.61,321.42Q451.31,305.69 483.15,305.69Q520.92,305.69 539.31,326.35Q557.69,347 557.69,373.31Q557.69,393.39 546.69,410.08Q535.69,426.77 515.85,445.15Q483.61,474.92 472.27,498.73Q460.92,522.54 460.92,552.92ZM224.62,800Q197,800 178.5,781.5Q160,763 160,735.39L160,224.61Q160,197 178.5,178.5Q197,160 224.62,160L735.39,160Q763,160 781.5,178.5Q800,197 800,224.61L800,735.39Q800,763 781.5,781.5Q763,800 735.39,800L224.62,800ZM224.62,760L735.39,760Q744.61,760 752.31,752.31Q760,744.61 760,735.39L760,224.61Q760,215.39 752.31,207.69Q744.61,200 735.39,200L224.62,200Q215.38,200 207.69,207.69Q200,215.39 200,224.61L200,735.39Q200,744.61 207.69,752.31Q215.38,760 224.62,760ZM200,200L200,200Q200,200 200,206.92Q200,213.85 200,224.61L200,735.39Q200,746.15 200,753.08Q200,760 200,760L200,760Q200,760 200,753.08Q200,746.15 200,735.39L200,224.61Q200,213.85 200,206.92Q200,200 200,200Z"/>
|
||||
</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="M481.54,685.39Q494.85,685.39 503.96,676.27Q513.08,667.15 513.08,653.85Q513.08,640.54 503.96,631.42Q494.85,622.31 481.54,622.31Q468.23,622.31 459.11,631.42Q450,640.54 450,653.85Q450,667.15 459.11,676.27Q468.23,685.39 481.54,685.39ZM460.92,552.92L499.54,552.92Q501.08,526.15 509.46,510.31Q517.85,494.46 541.54,470.77Q570.39,441.92 583.35,420.73Q596.31,399.54 596.31,372.61Q596.31,325.77 564.15,297.11Q532,268.46 485.61,268.46Q442.92,268.46 412.11,291.23Q381.31,314 367.08,345.85L403.85,361.08Q413.92,337.15 432.61,321.42Q451.31,305.69 483.15,305.69Q520.92,305.69 539.31,326.35Q557.69,347 557.69,373.31Q557.69,393.39 546.69,410.08Q535.69,426.77 515.85,445.15Q483.61,474.92 472.27,498.73Q460.92,522.54 460.92,552.92ZM224.62,800Q197,800 178.5,781.5Q160,763 160,735.39L160,224.61Q160,197 178.5,178.5Q197,160 224.62,160L735.39,160Q763,160 781.5,178.5Q800,197 800,224.61L800,735.39Q800,763 781.5,781.5Q763,800 735.39,800L224.62,800Z"/>
|
||||
</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="M459.39,744.61L498.61,744.61L498.61,697.69Q541.69,694.08 581.15,666.77Q620.61,639.46 620.61,582Q620.61,540 595.08,510.39Q569.54,480.77 497.54,454.77Q431.39,431.69 413,415.15Q394.61,398.61 394.61,368Q394.61,337.39 418.5,317Q442.39,296.61 482,296.61Q512.46,296.61 532.77,310.58Q553.08,324.54 565.69,346L600.46,332.31Q586.39,303.46 559.19,284Q532,264.54 500.61,262.31L500.61,215.39L461.39,215.39L461.39,262.31Q409.08,271 382.23,301.31Q355.39,331.61 355.39,368Q355.39,411.15 382.5,437.08Q409.61,463 474,486.31Q538.54,510.08 560.73,529.23Q582.92,548.39 582.92,582Q582.92,624.23 552.11,642.81Q521.31,661.39 486,661.39Q451.46,661.39 423.65,641.27Q395.85,621.15 379.23,584L344,599.23Q361.08,640.31 389.81,663.27Q418.54,686.23 459.39,695.69L459.39,744.61ZM480,840Q405.46,840 339.77,811.58Q274.08,783.15 225.46,734.54Q176.85,685.92 148.42,620.23Q120,554.54 120,480Q120,405.46 148.42,339.77Q176.85,274.08 225.46,225.46Q274.08,176.85 339.77,148.42Q405.46,120 480,120Q554.54,120 620.23,148.42Q685.92,176.85 734.54,225.46Q783.15,274.08 811.58,339.77Q840,405.46 840,480Q840,554.54 811.58,620.23Q783.15,685.92 734.54,734.54Q685.92,783.15 620.23,811.58Q554.54,840 480,840Z"/>
|
||||
</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:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M120,640L120,560L440,560L440,640L120,640ZM120,480L120,400L600,400L600,480L120,480ZM120,320L120,240L600,240L600,320L120,320ZM640,840L640,520L880,680L640,840Z"/>
|
||||
</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="M540,581.54Q552.39,581.54 561.81,572.11Q571.23,562.69 571.23,550.31Q571.23,537.92 561.81,528.5Q552.39,519.08 540,519.08Q527.61,519.08 518.19,528.5Q508.77,537.92 508.77,550.31Q508.77,562.69 518.19,572.11Q527.61,581.54 540,581.54ZM522.31,468.92L557.69,468.92Q559.23,443.77 565.62,431.04Q572,418.31 596.31,395.54Q621.69,372.46 631.69,354.35Q641.69,336.23 641.69,312.77Q641.69,272.39 612.89,245.42Q584.08,218.46 540,218.46Q506.69,218.46 480.81,236.46Q454.92,254.46 441.39,285.54L473.85,299.85Q485.15,276.39 501.42,264.65Q517.69,252.92 540,252.92Q568.61,252.92 587.46,269.89Q606.31,286.85 606.31,313.69Q606.31,330 597.15,344.04Q588,358.08 565.69,377.85Q540.39,399.92 531.35,418.35Q522.31,436.77 522.31,468.92ZM324.61,680Q297,680 278.5,661.5Q260,643 260,615.39L260,184.61Q260,157 278.5,138.5Q297,120 324.61,120L755.39,120Q783,120 801.5,138.5Q820,157 820,184.61L820,615.39Q820,643 801.5,661.5Q783,680 755.39,680L324.61,680ZM204.62,800Q177,800 158.5,781.5Q140,763 140,735.39L140,264.61L180,264.61L180,735.39Q180,744.62 187.69,752.31Q195.38,760 204.62,760L675.39,760L675.39,800L204.62,800Z"/>
|
||||
</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="M405.38,840L390.92,724.31Q371.77,718.54 349.5,706.15Q327.23,693.77 311.61,679.62L204.92,725L130.31,595L222.54,525.46Q220.77,514.61 219.62,503.11Q218.46,491.61 218.46,480.77Q218.46,470.69 219.62,459.58Q220.77,448.46 222.54,434.54L130.31,365L204.92,236.54L310.85,281.15Q328.77,266.23 349.61,254.23Q370.46,242.23 390.15,235.69L405.38,120L554.62,120L569.08,236.46Q592.08,244.54 609.73,255Q627.39,265.46 646.08,281.15L755.08,236.54L829.69,365L734.39,436.85Q737.69,449.23 738.08,459.58Q738.46,469.92 738.46,480Q738.46,489.31 737.69,499.65Q736.92,510 734.15,524.69L827.92,595L753.31,725L646.08,678.85Q627.39,694.54 608.46,705.77Q589.54,717 569.08,723.54L554.62,840L405.38,840ZM478.92,580Q520.77,580 549.85,550.92Q578.92,521.85 578.92,480Q578.92,438.15 549.85,409.08Q520.77,380 478.92,380Q436.85,380 407.88,409.08Q378.92,438.15 378.92,480Q378.92,521.85 407.88,550.92Q436.85,580 478.92,580Z"/>
|
||||
</vector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -94,4 +94,11 @@
|
||||
android:text="@string/import_profile" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
android:id="@+id/loader_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:centerLoader="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -51,6 +51,21 @@
|
||||
app:layout_constraintLeft_toLeftOf="@id/image_polycentric"
|
||||
app:layout_constraintRight_toRightOf="@id/image_polycentric" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_system"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="gX0eCWctTm6WHVGot4sMAh7NDAIwWsIM5tRsOz9dX04="
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textSize="10dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="middle"
|
||||
android:textColor="@color/gray_67"
|
||||
android:layout_marginTop="20dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_profile_name"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_buttons"
|
||||
android:layout_width="match_parent"
|
||||
@@ -91,4 +106,11 @@
|
||||
android:layout_marginTop="8dp"
|
||||
app:buttonBackground="@drawable/background_big_button_red"/>
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
android:id="@+id/loader_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:centerLoader="true"
|
||||
android:visibility="gone" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -81,43 +81,38 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_remembered_devices"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="3"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/remembered_devices"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:textSize="14dp"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/white"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular" />
|
||||
|
||||
<Button
|
||||
<ImageButton
|
||||
android:id="@+id/button_scan_qr"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1.7"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scan_qr"
|
||||
android:textSize="14dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:srcCompat="@drawable/ic_qr"
|
||||
app:tint="@color/primary" />
|
||||
|
||||
<Button
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add"
|
||||
android:textSize="14dp"
|
||||
android:textAlignment="textEnd"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:srcCompat="@drawable/ic_add"
|
||||
app:tint="@color/primary"
|
||||
android:layout_marginEnd="20dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/failed_to_retrieve_data_are_you_connected"
|
||||
android:textSize="14dp"
|
||||
android:textSize="15dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textAlignment="center"
|
||||
@@ -43,7 +43,7 @@
|
||||
android:textAlignment="center"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:textSize="9dp"
|
||||
android:textSize="11dp"
|
||||
android:layout_height="wrap_content" />
|
||||
<TextView
|
||||
android:id="@+id/dialog_text_code"
|
||||
|
||||
@@ -180,20 +180,28 @@
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/container_top"
|
||||
app:layout_constraintBottom_toTopOf="@id/button_creator_add"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<Button
|
||||
<FrameLayout
|
||||
android:id="@+id/button_creator_add"
|
||||
android:layout_width="match_parent"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:layout_height="50dp"
|
||||
android:layout_margin="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:text="Add Creator" />
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/add_creator"
|
||||
android:textSize="16dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay"
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:gravity="top"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="5dp">
|
||||
android:paddingBottom="0dp">
|
||||
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/creator_thumbnail"
|
||||
@@ -179,6 +179,7 @@
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray_e0"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:textSize="12dp"
|
||||
tools:text="57K views • 1 day ago" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -140,37 +140,22 @@
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:paddingBottom="3dp"
|
||||
app:srcCompat="@drawable/ic_queue_16dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_to"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
|
||||
android:padding="5dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp"
|
||||
android:background="@color/transparent"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/options" />
|
||||
|
||||
@@ -185,6 +170,23 @@
|
||||
android:layout_marginEnd="4dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:src="@drawable/ic_queue"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="7dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_video_name"
|
||||
android:layout_width="fill_parent"
|
||||
@@ -195,8 +197,9 @@
|
||||
android:textSize="13dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
tools:text="Legendary grant recipient: Marvin Wißfeld of MicroG Very loong title"
|
||||
tools:text="Legendary grant recipient: Marvin Wißfeld of MicroG Very loong title fff"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
@@ -174,37 +174,23 @@
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:paddingBottom="3dp"
|
||||
app:srcCompat="@drawable/ic_queue_16dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_to"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
|
||||
android:padding="5dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp"
|
||||
android:background="@color/transparent"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/options" />
|
||||
|
||||
@@ -219,6 +205,23 @@
|
||||
android:layout_marginEnd="4dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:src="@drawable/ic_queue"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="7dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_video_name"
|
||||
android:layout_width="fill_parent"
|
||||
|
||||
@@ -16,29 +16,44 @@
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
|
||||
<com.futo.platformplayer.views.SearchView
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_creators"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintBottom_toTopOf="@id/container_select"
|
||||
app:layout_constraintTop_toBottomOf="@id/search_bar"
|
||||
app:layout_constraintBottom_toTopOf="@id/button_select"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_select"
|
||||
<FrameLayout
|
||||
android:id="@+id/button_select"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="50dp"
|
||||
android:background="@drawable/background_button_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
<Button
|
||||
android:id="@+id/button_select"
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/colorPrimary"
|
||||
android:text="Select" />
|
||||
</LinearLayout>
|
||||
android:layout_height="match_parent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/select"
|
||||
android:textSize="16dp"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -116,20 +116,29 @@
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container_select"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="50dp"
|
||||
android:background="@drawable/background_button_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
<Button
|
||||
|
||||
<TextView
|
||||
android:id="@+id/button_select"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="10dp"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:text="Select" />
|
||||
</LinearLayout>
|
||||
android:layout_height="match_parent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/select"
|
||||
android:textSize="16dp"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -17,5 +17,6 @@
|
||||
android:layout_gravity="top|center_horizontal"
|
||||
android:alpha="0.7"
|
||||
android:layout_marginTop="80dp"
|
||||
android:layout_marginBottom="80dp"
|
||||
android:contentDescription="@string/loading" />
|
||||
</FrameLayout>
|
||||
@@ -40,6 +40,10 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:id="@+id/button_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent" />
|
||||
<ImageView
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="40dp"
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
android:id="@id/exo_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginBottom="-1dp"
|
||||
android:layout_marginBottom="-2dp"
|
||||
android:paddingStart="0dp"
|
||||
app:scrubber_drawable="@drawable/player_thumb"
|
||||
app:bar_height="2dp"
|
||||
|
||||
@@ -7,12 +7,23 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/root">
|
||||
|
||||
<com.futo.platformplayer.views.IdenticonView
|
||||
android:id="@+id/identicon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
android:background="@drawable/rounded_outline"
|
||||
android:clipToOutline="true"
|
||||
android:contentDescription="@string/channel_image"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_channel_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:srcCompat="@drawable/ic_futo_logo"
|
||||
android:background="@drawable/rounded_outline"
|
||||
android:clipToOutline="true"
|
||||
android:scaleType="centerCrop"
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/root">
|
||||
|
||||
|
||||
@@ -28,4 +28,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" />
|
||||
<com.futo.platformplayer.views.subscriptions.SubscriptionExploreButton
|
||||
android:id="@+id/subgroup_explore"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="78dp"
|
||||
android:layout_height="54dp"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="2dp"
|
||||
android:layout_margin="2dp"
|
||||
android:clickable="true"
|
||||
android:id="@+id/root">
|
||||
android:id="@+id/root"
|
||||
android:background="@drawable/background_primary_round_6dp">
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/xp_book" />
|
||||
<LinearLayout
|
||||
android:src="@drawable/xp_book"
|
||||
app:shapeAppearanceOverlay="@style/roundedCorners_6dp"
|
||||
android:layout_margin="2dp" />
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#99000000"
|
||||
android:gravity="center">
|
||||
<TextView
|
||||
android:id="@+id/text_sub_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:textSize="12dp"
|
||||
android:textAlignment="center"
|
||||
android:text="News" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:background="@drawable/background_dark_round_6dp"
|
||||
android:gravity="center"
|
||||
android:layout_margin="2dp" />
|
||||
<TextView
|
||||
android:id="@+id/text_sub_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:textSize="12dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_gravity="center"
|
||||
tools:text="News" />
|
||||
</FrameLayout>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="54dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="1dp"
|
||||
android:clickable="true"
|
||||
android:id="@+id/root">
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/sub_group_demo" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/background_button_explore_inner"
|
||||
android:gravity="center">
|
||||
<TextView
|
||||
android:id="@+id/text_sub_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/inter_medium"
|
||||
android:textSize="13dp"
|
||||
android:textAlignment="center"
|
||||
android:text="Explore Subscription Groups" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="LoaderOverlay">
|
||||
<attr name="centerLoader" format="boolean" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@@ -569,7 +569,7 @@
|
||||
<string name="playlist_copied_as_local_playlist">Playlist copied as local playlist</string>
|
||||
<string name="are_you_sure_you_want_to_delete_the_downloaded_videos">Are you sure you want to delete the downloaded videos?</string>
|
||||
<string name="create_new_playlist">Create new playlist</string>
|
||||
<string name="create_new_subgroup">Create new subscription group</string>
|
||||
<string name="create_new_subgroup">Create new group</string>
|
||||
<string name="expected_media_content_found">Expected media content, found</string>
|
||||
<string name="failed_to_load_post">Failed to load post.</string>
|
||||
<string name="replies">replies</string>
|
||||
@@ -724,6 +724,8 @@
|
||||
<string name="position">Position</string>
|
||||
<string name="tutorials">Tutorials</string>
|
||||
<string name="do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button">Do you want to see the tutorials? You can find them at any time through the more button.</string>
|
||||
<string name="add_creator">Add Creators</string>
|
||||
<string name="select">Select</string>
|
||||
<string-array name="home_screen_array">
|
||||
<item>Recommendations</item>
|
||||
<item>Subscriptions</item>
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">4dp</item>
|
||||
</style>
|
||||
<style name="roundedCorners_6dp" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">6dp</item>
|
||||
</style>
|
||||
<style name="roundedCorners_10dp" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">10dp</item>
|
||||
|
||||
Submodule app/src/stable/assets/sources/kick updated: 396dd16987...8d957b6fc4
Submodule app/src/stable/assets/sources/nebula updated: 863d0be132...01270edbb4
Submodule app/src/stable/assets/sources/patreon updated: 55aef15f4b...139444608d
Submodule app/src/stable/assets/sources/rumble updated: b0e35a9b66...263ed8c7df
Submodule app/src/stable/assets/sources/youtube updated: d41cc8e848...a1980eeac4
Submodule app/src/unstable/assets/sources/kick updated: 396dd16987...8d957b6fc4
Submodule app/src/unstable/assets/sources/patreon updated: 55aef15f4b...139444608d
Submodule app/src/unstable/assets/sources/rumble updated: b0e35a9b66...263ed8c7df
Submodule app/src/unstable/assets/sources/youtube updated: 13551ab67f...a1980eeac4
+1
-1
Submodule dep/polycentricandroid updated: 86cd96c41f...7695198eea
Reference in New Issue
Block a user