Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Koen 2023-12-05 15:20:05 +01:00
commit 9075a2599c
20 changed files with 409 additions and 169 deletions

View File

@ -23,6 +23,7 @@ import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormFieldButton import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -44,7 +45,7 @@ class Settings : FragmentedStorageFileJson() {
@Transient @Transient
val onTabsChanged = Event0(); val onTabsChanged = Event0();
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -5) @FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
@FormFieldButton(R.drawable.ic_person) @FormFieldButton(R.drawable.ic_person)
fun managePolycentricIdentity() { fun managePolycentricIdentity() {
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
@ -60,7 +61,7 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -4) @FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
@FormFieldButton(R.drawable.ic_quiz) @FormFieldButton(R.drawable.ic_quiz)
fun openFAQ() { fun openFAQ() {
try { try {
@ -70,7 +71,7 @@ class Settings : FragmentedStorageFileJson() {
//Ignored //Ignored
} }
} }
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -3) @FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
@FormFieldButton(R.drawable.ic_data_alert) @FormFieldButton(R.drawable.ic_data_alert)
fun openIssues() { fun openIssues() {
try { try {
@ -102,7 +103,7 @@ class Settings : FragmentedStorageFileJson() {
} }
}*/ }*/
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -2) @FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
@FormFieldButton(R.drawable.ic_tabs) @FormFieldButton(R.drawable.ic_tabs)
fun manageTabs() { fun manageTabs() {
try { try {
@ -114,6 +115,17 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
@FormFieldButton(R.drawable.ic_move_up)
fun import() {
val act = SettingsActivity.getActivity() ?: return;
val intent = MainActivity.getImportOptionsIntent(act);
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
act.startActivity(intent);
}
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1) @FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
@FormFieldButton(R.drawable.ic_link) @FormFieldButton(R.drawable.ic_link)
fun manageLinks() { fun manageLinks() {
@ -405,6 +417,9 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0) @FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.comment_sections) @DropdownFieldOptionsId(R.array.comment_sections)
var defaultCommentSection: Int = 0; var defaultCommentSection: Int = 0;
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
var badReputationCommentsFading: Boolean = true;
} }
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7) @FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
@ -712,25 +727,16 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3) @FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
fun export() { fun export() {
StateBackup.startExternalBackup(); val activity = SettingsActivity.getActivity() ?: return;
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
StateBackup.shareExternalBackup();
}),
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
StateBackup.saveExternalBackup(activity);
})
)
} }
/*
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, 4)
fun import() {
val act = SettingsActivity.getActivity() ?: return;
StateApp.instance.requestFileReadAccess(act, null) {
if(it != null && it.exists()) {
val name = it.name;
val contents = it.readBytes(act);
if(contents != null) {
if(name != null && name.endsWith(".zip", true))
StateBackup.importZipBytes(act, act.lifecycleScope, contents);
}
}
}
}*/
} }
@FormField(R.string.payment, FieldForm.GROUP, -1, 17) @FormField(R.string.payment, FieldForm.GROUP, -1, 17)

View File

@ -13,6 +13,7 @@ import android.view.View
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.* import android.widget.*
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.dialogs.* import com.futo.platformplayer.dialogs.*
@ -189,8 +190,10 @@ class UIDialogs {
view.findViewById<TextView>(R.id.dialog_text_code).apply { view.findViewById<TextView>(R.id.dialog_text_code).apply {
if(code == null) if(code == null)
this.visibility = View.GONE; this.visibility = View.GONE;
else else {
this.text = code; this.text = code;
this.visibility = View.VISIBLE;
}
}; };
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply { view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
val buttons = actions.map<Action, TextView> { act -> val buttons = actions.map<Action, TextView> { act ->
@ -326,6 +329,12 @@ class UIDialogs {
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} }
fun showImportOptionsDialog(context: MainActivity) {
val dialog = ImportOptionsDialog(context);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
fun showCastingDialog(context: Context) { fun showCastingDialog(context: Context) {

View File

@ -1,30 +1,21 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.content.ContentResolver import android.content.ContentResolver
import android.graphics.Color
import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -40,12 +31,10 @@ import com.futo.platformplayer.views.pills.RoundButton
import com.futo.platformplayer.views.pills.RoundButtonGroup import com.futo.platformplayer.views.pills.RoundButtonGroup
import com.futo.platformplayer.views.overlays.slideup.* import com.futo.platformplayer.views.overlays.slideup.*
import com.futo.platformplayer.views.video.FutoVideoPlayerBase import com.futo.platformplayer.views.video.FutoVideoPlayerBase
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.internal.notifyAll
import java.lang.IllegalStateException import java.lang.IllegalStateException
class UISlideOverlays { class UISlideOverlays {

View File

@ -515,6 +515,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
val url = intent.getStringExtra("VIDEO"); val url = intent.getStringExtra("VIDEO");
navigate(_fragVideoDetail, url); navigate(_fragVideoDetail, url);
} }
"IMPORT_OPTIONS" -> {
UIDialogs.showImportOptionsDialog(this);
}
"TAB" -> { "TAB" -> {
when(intent.getStringExtra("TAB")){ when(intent.getStringExtra("TAB")){
"Sources" -> { "Sources" -> {
@ -730,18 +733,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (!newPipeSubsParsed.has("subscriptions") || !newPipeSubsParsed["subscriptions"].isJsonArray) if (!newPipeSubsParsed.has("subscriptions") || !newPipeSubsParsed["subscriptions"].isJsonArray)
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found"); return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
val jsonSubs = newPipeSubsParsed["subscriptions"] StateBackup.importNewPipeSubs(this, newPipeSubsParsed);
val jsonSubsArray = jsonSubs.asJsonArray;
val jsonSubsArrayItt = jsonSubsArray.iterator();
val subs = mutableListOf<String>()
while(jsonSubsArrayItt.hasNext()) {
val jsonSubObj = jsonSubsArrayItt.next().asJsonObject;
if(jsonSubObj.has("url"))
subs.add(jsonSubObj["url"].asString);
}
navigate(_fragImportSubscriptions, subs);
} }
catch(ex: Exception) { catch(ex: Exception) {
Logger.e(TAG, ex.message, ex); Logger.e(TAG, ex.message, ex);
@ -1054,5 +1046,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return sourcesIntent; return sourcesIntent;
} }
fun getImportOptionsIntent(context: Context): Intent {
val sourcesIntent = Intent(context, MainActivity::class.java);
sourcesIntent.action = "IMPORT_OPTIONS";
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return sourcesIntent;
}
} }
} }

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
@ -30,6 +31,8 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
private var _isFinished = false; private var _isFinished = false;
lateinit var overlay: FrameLayout;
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext") Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@ -44,6 +47,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
_buttonDev = findViewById(R.id.button_dev); _buttonDev = findViewById(R.id.button_dev);
_devSets = findViewById(R.id.dev_settings); _devSets = findViewById(R.id.dev_settings);
_loaderView = findViewById(R.id.loader); _loaderView = findViewById(R.id.loader);
overlay = findViewById(R.id.overlay_container);
_form.onChanged.subscribe { field, value -> _form.onChanged.subscribe { field, value ->
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving"); Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");

View File

@ -0,0 +1,73 @@
package com.futo.platformplayer.dialogs
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.Button
import com.futo.platformplayer.R
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.readBytes
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.views.buttons.BigButton
class ImportOptionsDialog: AlertDialog {
private val _context: MainActivity;
private lateinit var _button_import_zip: BigButton;
private lateinit var _button_import_ezip: BigButton;
private lateinit var _button_import_txt: BigButton;
private lateinit var _button_import_newpipe_subs: BigButton;
private lateinit var _button_close: Button;
constructor(context: MainActivity): super(context) {
_context = context;
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(LayoutInflater.from(context).inflate(R.layout.dialog_import_options, null));
_button_import_zip = findViewById(R.id.button_import_zip);
_button_import_ezip = findViewById(R.id.button_import_ezip);
_button_import_txt = findViewById(R.id.button_import_txt);
_button_import_newpipe_subs = findViewById(R.id.button_import_newpipe_subs);
_button_close = findViewById(R.id.button_cancel);
_button_import_zip.onClick.subscribe {
dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/zip") {
val zipBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
};
}
_button_import_ezip.setOnClickListener {
}
_button_import_txt.onClick.subscribe {
dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "text/plain") {
val txtBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
val txt = String(txtBytes);
StateBackup.importTxt(_context, txt);
};
}
_button_import_newpipe_subs.onClick.subscribe {
dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/json") {
val jsonBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
val json = String(jsonBytes);
StateBackup.importNewPipeSubs(_context, json);
};
};
_button_close.setOnClickListener {
dismiss();
}
}
override fun dismiss() {
super.dismiss();
}
}

View File

@ -6,8 +6,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.Spinner import android.widget.Spinner
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R import com.futo.platformplayer.R
@ -21,9 +23,13 @@ class CreatorsFragment : MainFragment() {
private var _spinnerSortBy: Spinner? = null; private var _spinnerSortBy: Spinner? = null;
private var _overlayContainer: FrameLayout? = null; private var _overlayContainer: FrameLayout? = null;
private var _containerSearch: FrameLayout? = null;
private var _editSearch: EditText? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_creators, container, false); val view = inflater.inflate(R.layout.fragment_creators, container, false);
_containerSearch = view.findViewById(R.id.container_search);
_editSearch = view.findViewById(R.id.edit_search);
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)); val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) }; adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
@ -44,6 +50,10 @@ class CreatorsFragment : MainFragment() {
_spinnerSortBy = spinnerSortBy; _spinnerSortBy = spinnerSortBy;
_editSearch?.addTextChangedListener {
adapter.query = it.toString();
}
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions); val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
recyclerView.adapter = adapter; recyclerView.adapter = adapter;
recyclerView.layoutManager = LinearLayoutManager(view.context); recyclerView.layoutManager = LinearLayoutManager(view.context);
@ -54,6 +64,8 @@ class CreatorsFragment : MainFragment() {
super.onDestroyMainView(); super.onDestroyMainView();
_spinnerSortBy = null; _spinnerSortBy = null;
_overlayContainer = null; _overlayContainer = null;
_editSearch = null;
_containerSearch = null;
} }
companion object { companion object {

View File

@ -235,14 +235,33 @@ class StateApp {
return state; return state;
} }
fun requestFileReadAccess(activity: IWithResultLauncher, path: Uri?, handle: (DocumentFile?)->Unit) { fun requestFileReadAccess(activity: IWithResultLauncher, path: Uri?, contentType: String, handle: (DocumentFile?)->Unit) {
if(activity is Context) { if(activity is Context) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT); val intent = Intent(Intent.ACTION_OPEN_DOCUMENT);
if(path != null) if(path != null)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION); .or(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setType(contentType);
activity.launchForResult(intent, 98) {
if(it.resultCode == Activity.RESULT_OK) {
val uri = it.data?.data;
if(uri != null)
handle(DocumentFile.fromSingleUri(activity, uri));
}
else
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
};
}
}
fun requestFileCreateAccess(activity: IWithResultLauncher, path: Uri?, contentType: String, handle: (DocumentFile?)->Unit) {
if(activity is Context) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT);
if(path != null)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setType(contentType);
activity.launchForResult(intent, 98) { activity.launchForResult(intent, 98) {
if(it.resultCode == Activity.RESULT_OK) { if(it.resultCode == Activity.RESULT_OK) {
val uri = it.data?.data; val uri = it.data?.data;

View File

@ -8,17 +8,21 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.copyTo import com.futo.platformplayer.copyTo
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0 import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
import com.futo.platformplayer.getNowDiffHours import com.futo.platformplayer.getNowDiffHours
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.readBytes import com.futo.platformplayer.readBytes
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.writeBytes import com.futo.platformplayer.writeBytes
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -52,15 +56,6 @@ class StateBackup {
val secondaryBackupFile = dir.findFile("GrayjayBackup.ezip.old") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip.old") else null; val secondaryBackupFile = dir.findFile("GrayjayBackup.ezip.old") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip.old") else null;
return Pair(mainBackupFile, secondaryBackupFile); return Pair(mainBackupFile, secondaryBackupFile);
} }
/*
private fun getAutomaticBackupFiles(): Pair<File, File> {
val dir = StateApp.instance.getExternalRootDirectory();
if(dir == null)
throw IllegalStateException("Can't access external files");
return Pair(File(dir, "GrayjayBackup.ezip"), File(dir, "GrayjayBackup.ezip.old"))
}*/
fun getAllMigrationStores(): List<ManagedStore<*>> = listOf( fun getAllMigrationStores(): List<ManagedStore<*>> = listOf(
StateSubscriptions.instance.toMigrateCheck(), StateSubscriptions.instance.toMigrateCheck(),
StatePlaylists.instance.toMigrateCheck() StatePlaylists.instance.toMigrateCheck()
@ -192,7 +187,19 @@ class StateBackup {
importZipBytes(context, scope, backupBytes); importZipBytes(context, scope, backupBytes);
} }
fun startExternalBackup() { fun saveExternalBackup(activity: IWithResultLauncher) {
val data = export();
if(activity is Context)
StateApp.instance.requestFileCreateAccess(activity, null, "application/zip") {
if(it == null) {
UIDialogs.toast("Cancelled");
return@requestFileCreateAccess;
}
it.writeBytes(activity, data.asZip());
UIDialogs.toast("Export saved");
};
}
fun shareExternalBackup() {
val data = export(); val data = export();
val now = OffsetDateTime.now(); val now = OffsetDateTime.now();
val exportFile = File( val exportFile = File(
@ -401,6 +408,46 @@ class StateBackup {
).withCondition { doImport } else null ).withCondition { doImport } else null
); );
} }
fun importTxt(context: MainActivity, text: String, allowFailure: Boolean = false): Boolean {
if(text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
context.navigate(context.getFragment<ImportSubscriptionsFragment>(), lines);
return true;
}
else if(allowFailure) {
UIDialogs.showGeneralErrorDialog(context, "Unknown text header [${text}]");
}
return false;
}
fun importNewPipeSubs(context: MainActivity, json: String) {
val newPipeSubsParsed = JsonParser.parseString(json).asJsonObject;
if (!newPipeSubsParsed.has("subscriptions") || !newPipeSubsParsed["subscriptions"].isJsonArray)
UIDialogs.showGeneralErrorDialog(context, "Invalid json");
else {
importNewPipeSubs(context, newPipeSubsParsed);
}
}
fun importNewPipeSubs(context: MainActivity, obj: JsonObject) {
try {
val jsonSubs = obj["subscriptions"]
val jsonSubsArray = jsonSubs.asJsonArray;
val jsonSubsArrayItt = jsonSubsArray.iterator();
val subs = mutableListOf<String>()
while(jsonSubsArrayItt.hasNext()) {
val jsonSubObj = jsonSubsArrayItt.next().asJsonObject;
if(jsonSubObj.has("url"))
subs.add(jsonSubObj["url"].asString);
}
context.navigate(context.getFragment<ImportSubscriptionsFragment>(), subs);
}
catch(ex: Exception) {
Logger.e("StateBackup", ex.message, ex);
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_parse_newpipe_subscriptions), ex);
}
}
} }
class ExportStructure( class ExportStructure(

View File

@ -117,7 +117,8 @@ class CommentViewHolder : ViewHolder {
val rating = comment.rating; val rating = comment.rating;
if (rating is RatingLikeDislikes) { if (rating is RatingLikeDislikes) {
_layoutComment.alpha = if (rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f; _layoutComment.alpha = if (Settings.instance.comments.badReputationCommentsFading &&
rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
} else { } else {
_layoutComment.alpha = 1.0f; _layoutComment.alpha = 1.0f;
} }

View File

@ -23,7 +23,6 @@ import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
import com.futo.polycentric.core.Opinion import com.futo.polycentric.core.Opinion
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import userpackage.Protocol
import java.util.IdentityHashMap import java.util.IdentityHashMap
class CommentWithReferenceViewHolder : ViewHolder { class CommentWithReferenceViewHolder : ViewHolder {
@ -135,7 +134,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
val rating = comment.rating; val rating = comment.rating;
if (rating is RatingLikeDislikes) { if (rating is RatingLikeDislikes) {
_layoutComment.alpha = if (rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f; _layoutComment.alpha = if (Settings.instance.comments.badReputationCommentsFading &&
rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
} else { } else {
_layoutComment.alpha = 1.0f; _layoutComment.alpha = 1.0f;
} }

View File

@ -15,11 +15,16 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
var onClick = Event1<Subscription>(); var onClick = Event1<Subscription>();
var onSettings = Event1<Subscription>(); var onSettings = Event1<Subscription>();
var sortBy: Int = 3 var sortBy: Int = 5
set(value) { set(value) {
field = value field = value
updateDataset() updateDataset()
} }
var query: String? = null
set(value) {
field = value;
updateDataset();
}
constructor(inflater: LayoutInflater, confirmationMessage: String) : super() { constructor(inflater: LayoutInflater, confirmationMessage: String) : super() {
_inflater = inflater; _inflater = inflater;
@ -53,6 +58,7 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
} }
private fun updateDataset() { private fun updateDataset() {
val queryLower = query?.lowercase() ?: "";
_sortedDataset = when (sortBy) { _sortedDataset = when (sortBy) {
0 -> StateSubscriptions.instance.getSubscriptions().sortedBy({ u -> u.channel.name.lowercase() }) 0 -> StateSubscriptions.instance.getSubscriptions().sortedBy({ u -> u.channel.name.lowercase() })
1 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending({ u -> u.channel.name.lowercase() }) 1 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending({ u -> u.channel.name.lowercase() })
@ -61,7 +67,9 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
4 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackSeconds } 4 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackSeconds }
5 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds } 5 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds }
else -> throw IllegalStateException("Invalid sorting algorithm selected."); else -> throw IllegalStateException("Invalid sorting algorithm selected.");
}.toList(); }
.filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) }
.toList();
notifyDataSetChanged(); notifyDataSetChanged();
} }

View File

@ -25,7 +25,7 @@ class SlideUpMenuItem : RelativeLayout {
init(); init();
} }
constructor(context: Context, imageRes: Int = 0, mainText: String, subText: String = "", tag: Any, call: (()->Unit)? = null, invokeParent: Boolean = true): super(context){ constructor(context: Context, imageRes: Int = 0, mainText: String, subText: String = "", tag: Any?, call: (()->Unit)? = null, invokeParent: Boolean = true): super(context){
init(); init();
_image.setImageResource(imageRes); _image.setImageResource(imageRes);
_text.text = mainText; _text.text = mainText;

View File

@ -1,89 +1,105 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:background="@color/black">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="vertical"
android:gravity="center_vertical" android:paddingStart="20dp"
android:paddingTop="20dp" android:paddingEnd="20dp"
android:paddingBottom="15dp"> android:background="@color/black">
<ImageButton
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingRight="20dp"
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings"
android:textSize="24dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_extra_light" />
</FrameLayout>
<Space
android:layout_width="20dp"
android:layout_height="match_parent" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
<com.futo.platformplayer.views.LoaderView android:orientation="horizontal"
android:id="@+id/loader" android:gravity="center_vertical"
android:layout_marginBottom="15dp" android:paddingTop="20dp"
android:layout_marginTop="15dp" android:paddingBottom="15dp">
android:layout_width="match_parent"
android:layout_height="60dp" />
<com.futo.platformplayer.views.fields.FieldForm <ImageButton
android:id="@+id/settings_form" android:id="@+id/button_back"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="match_parent"
android:paddingRight="20dp"
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings"
android:textSize="24dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_extra_light" />
</FrameLayout>
<Space
android:layout_width="20dp"
android:layout_height="match_parent" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:id="@+id/dev_settings"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <com.futo.platformplayer.views.LoaderView
android:id="@+id/loader"
android:layout_marginBottom="15dp"
android:layout_marginTop="15dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="60dp" />
android:textColor="@color/white"
android:textSize="14dp"
android:textAlignment="center"
android:layout_margin="5dp"
android:text="@string/you_re_apparantly_a_developer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_dev"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/developer_settings" />
</LinearLayout>
</LinearLayout>
</ScrollView> <com.futo.platformplayer.views.fields.FieldForm
</LinearLayout> android:id="@+id/settings_form"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/dev_settings"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="14dp"
android:textAlignment="center"
android:layout_margin="5dp"
android:text="@string/you_re_apparantly_a_developer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_dev"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/developer_settings" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone"
android:elevation="15dp">
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:background="@color/gray_1d"> android:background="@color/gray_1d">
@ -13,7 +13,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center" android:gravity="center"
android:paddingTop="40dp"> android:paddingTop="20dp">
<FrameLayout <FrameLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -21,8 +21,8 @@
<ImageView <ImageView
android:id="@+id/update_spinner" android:id="@+id/update_spinner"
android:layout_width="100dp" android:layout_width="70dp"
android:layout_height="100dp" android:layout_height="70dp"
app:srcCompat="@drawable/ic_move_up" /> app:srcCompat="@drawable/ic_move_up" />
<TextView <TextView
@ -46,7 +46,19 @@
android:textSize="14dp" android:textSize="14dp"
android:textColor="@color/white" android:textColor="@color/white"
android:fontFamily="@font/inter_regular" android:fontFamily="@font/inter_regular"
android:layout_marginTop="30dp" android:layout_marginTop="10dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="You can open and share files directly to Grayjay as well."
android:textAlignment="center"
android:textSize="13dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:layout_marginTop="10dp"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:layout_marginEnd="30dp" /> android:layout_marginEnd="30dp" />
@ -55,39 +67,47 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center" android:gravity="center"
android:padding="10dp" android:paddingBottom="10dp"
android:layout_marginTop="28dp" android:paddingTop="10dp"
android:layout_marginBottom="28dp"> android:layout_marginTop="5dp"
android:layout_marginBottom="10dp">
<com.futo.platformplayer.views.buttons.BigButton <com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_import_zip"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scaleY="0.9"
android:scaleX="0.9"
app:buttonIcon="@drawable/ic_zip" app:buttonIcon="@drawable/ic_zip"
app:buttonText="Import Grayjay export (.zip)" app:buttonText="Import Grayjay export (.zip)"
android:layout_margin="5dp"
app:buttonBackground="@drawable/background_big_button_black" app:buttonBackground="@drawable/background_big_button_black"
app:buttonSubText="Pick a Grayjay export zip file" /> app:buttonSubText="Pick a Grayjay export zip file" />
<com.futo.platformplayer.views.buttons.BigButton <com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_import_ezip"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:buttonIcon="@drawable/ic_encrypted" app:buttonIcon="@drawable/ic_encrypted"
android:scaleY="0.9"
android:scaleX="0.9"
android:alpha="0.5" android:alpha="0.5"
app:buttonBackground="@drawable/background_big_button_black" app:buttonBackground="@drawable/background_big_button_black"
app:buttonText="Import Grayjay Auto-Backup (.ezip)" app:buttonText="Import Grayjay Auto-Backup (.ezip)"
android:layout_margin="5dp"
app:buttonSubText="Pick a Grayjay auto-backup encrypted zip file" /> app:buttonSubText="Pick a Grayjay auto-backup encrypted zip file" />
<com.futo.platformplayer.views.buttons.BigButton <com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_import_txt"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp" android:scaleY="0.9"
android:scaleX="0.9"
app:buttonIcon="@drawable/ic_lines" app:buttonIcon="@drawable/ic_lines"
android:alpha="0.5"
app:buttonBackground="@drawable/background_big_button_black" app:buttonBackground="@drawable/background_big_button_black"
app:buttonText="Import Line Text file (.txt)" app:buttonText="Import Line Text file (.txt)"
app:buttonSubText="Pick a text file with one entry per line" /> app:buttonSubText="Pick a text file with one entry per line" />
<com.futo.platformplayer.views.buttons.BigButton <com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_import_newpipe_subs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp" android:scaleY="0.9"
android:scaleX="0.9"
app:buttonIcon="@drawable/ic_play" app:buttonIcon="@drawable/ic_play"
app:buttonBackground="@drawable/background_big_button_black" app:buttonBackground="@drawable/background_big_button_black"
app:buttonText="Import NewPipe Subscriptions (.json)" app:buttonText="Import NewPipe Subscriptions (.json)"
@ -98,7 +118,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/close" android:text="@string/close"
android:layout_marginTop="20dp" android:layout_marginTop="10dp"
android:textSize="14dp" android:textSize="14dp"
android:fontFamily="@font/inter_regular" android:fontFamily="@font/inter_regular"
android:textColor="@color/colorPrimary" android:textColor="@color/colorPrimary"

View File

@ -50,7 +50,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:textColor="#AAAAAA" android:textColor="#AAAAAA"
android:fontFamily="monospace" android:fontFamily="monospace"
android:text="source.getVideoDetails(...)" android:text=""
android:textAlignment="center" android:textAlignment="center"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:layout_marginEnd="30dp" android:layout_marginEnd="30dp"
@ -58,6 +58,7 @@
android:padding="5dp" android:padding="5dp"
android:background="#111111" android:background="#111111"
android:textSize="8dp" android:textSize="8dp"
android:visibility="gone"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:ignore="HardcodedText" /> tools:ignore="HardcodedText" />

View File

@ -16,33 +16,67 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="100dp"
android:minHeight="0dp" android:minHeight="0dp"
app:layout_scrollFlags="scroll" app:layout_scrollFlags="scroll"
app:contentInsetStart="0dp" app:contentInsetStart="0dp"
app:contentInsetEnd="0dp"> app:contentInsetEnd="0dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical"> android:orientation="vertical">
<TextView <!--Search Text-->
android:layout_width="wrap_content" <FrameLayout
android:id="@+id/container_search"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="14dp" android:visibility="visible"
android:textColor="@color/gray_ac" android:layout_margin="10dp">
android:fontFamily="@font/inter_light"
android:text="@string/sort_by"
android:paddingStart="20dp" />
<Spinner <EditText
android:id="@+id/spinner_sortby" android:id="@+id/edit_search"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_weight="1" android:layout_height="wrap_content"
android:inputType="text"
android:imeOptions="actionDone"
android:singleLine="true"
android:hint="Search"
android:paddingEnd="46dp" />
<ImageButton
android:id="@+id/button_clear_search"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="18dp"
android:paddingEnd="18dp"
android:layout_gravity="right|center_vertical"
android:visibility="invisible"
android:src="@drawable/ic_clear_16dp" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="20dp" android:gravity="center_vertical">
android:paddingEnd="20dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14dp"
android:textColor="@color/gray_ac"
android:fontFamily="@font/inter_light"
android:text="@string/sort_by"
android:paddingStart="20dp" />
<Spinner
android:id="@+id/spinner_sortby"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>

View File

@ -370,6 +370,8 @@
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string> <string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
<string name="primary_language">Primary Language</string> <string name="primary_language">Primary Language</string>
<string name="default_comment_section">Default Comment Section</string> <string name="default_comment_section">Default Comment Section</string>
<string name="bad_reputation_comments_fading">Bad Reputation Comment Fading</string>
<string name="bad_reputation_comments_fading_description">If comments with a very bad reputation should be faded. Disabling may worsen experience.</string>
<string name="reinstall_embedded_plugins">Reinstall Embedded Plugins</string> <string name="reinstall_embedded_plugins">Reinstall Embedded Plugins</string>
<string name="remove_cached_version">Remove Cached Version</string> <string name="remove_cached_version">Remove Cached Version</string>
<string name="remove_the_last_downloaded_version">Remove the last downloaded version</string> <string name="remove_the_last_downloaded_version">Remove the last downloaded version</string>

@ -1 +1 @@
Subproject commit 8f10daba1ef9cbcd99f3c640d86808f8c94aa84a Subproject commit 07aa5a9aab441657f89ae14ff3cfd9d9ca977fe6

@ -1 +1 @@
Subproject commit 8f10daba1ef9cbcd99f3c640d86808f8c94aa84a Subproject commit 07aa5a9aab441657f89ae14ff3cfd9d9ca977fe6