mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Merge branch 'master' into aw/polycentric-profiles
This commit is contained in:
@@ -1,2 +1,6 @@
|
|||||||
aar/* filter=lfs diff=lfs merge=lfs -text
|
aar/* filter=lfs diff=lfs merge=lfs -text
|
||||||
app/aar/* filter=lfs diff=lfs merge=lfs -text
|
app/aar/* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
app/src/main/jniLibs/arm64-v8a filter=lfs diff=lfs merge=lfs -text
|
||||||
|
app/src/main/jniLibs/armeabi-v7a filter=lfs diff=lfs merge=lfs -text
|
||||||
|
app/src/main/jniLibs/x86 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
app/src/main/jniLibs/x86_64 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ android {
|
|||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
|
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||||
assets {
|
assets {
|
||||||
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
object AppCaUpdater {
|
||||||
|
private const val CA_URL = "https://curl.se/ca/cacert.pem"
|
||||||
|
private const val CACHE_FILENAME = "curl-ca-bundle.pem"
|
||||||
|
private const val MAX_AGE_DAYS = 30
|
||||||
|
|
||||||
|
suspend fun ensureCaBundle(context: Context): File = withContext(Dispatchers.IO) {
|
||||||
|
val file = File(context.noBackupFilesDir, CACHE_FILENAME)
|
||||||
|
val needsUpdate = !file.exists() || isOlderThanDays(file, MAX_AGE_DAYS)
|
||||||
|
if (needsUpdate) {
|
||||||
|
downloadToFile(CA_URL, file)
|
||||||
|
}
|
||||||
|
return@withContext file
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isOlderThanDays(file: File, days: Int): Boolean {
|
||||||
|
val ageMs = System.currentTimeMillis() - file.lastModified()
|
||||||
|
return ageMs > days * 24L * 60L * 60L * 1000L
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadToFile(urlStr: String, dest: File) {
|
||||||
|
val conn = (URL(urlStr).openConnection() as HttpURLConnection).apply {
|
||||||
|
connectTimeout = 15000
|
||||||
|
readTimeout = 15000
|
||||||
|
instanceFollowRedirects = true
|
||||||
|
}
|
||||||
|
conn.inputStream.use { input ->
|
||||||
|
dest.parentFile?.mkdirs()
|
||||||
|
dest.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import com.caoccao.javet.values.reference.V8ValueError
|
|||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.caoccao.javet.values.reference.V8ValuePromise
|
import com.caoccao.javet.values.reference.V8ValuePromise
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||||
@@ -387,4 +388,15 @@ suspend fun <T> Deferred<T>.awaitCancelConverted(): T {
|
|||||||
}
|
}
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> IPager<T>.toList(): List<T> {
|
||||||
|
val list = this.getResults().toMutableList();
|
||||||
|
|
||||||
|
while(this.hasMorePages()) {
|
||||||
|
this.nextPage();
|
||||||
|
list.addAll(this.getResults());
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.toList();
|
||||||
}
|
}
|
||||||
@@ -10,11 +10,11 @@ import com.futo.platformplayer.activities.MainActivity
|
|||||||
import com.futo.platformplayer.activities.ManageTabsActivity
|
import com.futo.platformplayer.activities.ManageTabsActivity
|
||||||
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
||||||
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
import com.futo.platformplayer.activities.SyncHomeActivity
|
import com.futo.platformplayer.activities.SyncHomeActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
@@ -42,7 +42,6 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -64,7 +63,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
||||||
@FormFieldButton(R.drawable.ic_update)
|
@FormFieldButton(R.drawable.ic_update)
|
||||||
fun syncGrayjay() {
|
fun syncGrayjay() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp?.instance?.activity?.let {
|
||||||
it.startActivity(Intent(it, SyncHomeActivity::class.java))
|
it.startActivity(Intent(it, SyncHomeActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +72,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
||||||
@FormFieldButton(R.drawable.ic_person)
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp?.instance?.activity?.let {
|
||||||
if (StatePolycentric.instance.enabled) {
|
if (StatePolycentric.instance.enabled) {
|
||||||
if (StatePolycentric.instance.processHandle != null) {
|
if (StatePolycentric.instance.processHandle != null) {
|
||||||
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
||||||
@@ -91,7 +90,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
|
||||||
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
StateApp?.instance?.activity?.startActivity(browserIntent);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
@@ -101,7 +100,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun openIssues() {
|
fun openIssues() {
|
||||||
try {
|
try {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
|
||||||
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
StateApp?.instance?.activity?.startActivity(browserIntent);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
@@ -132,7 +131,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormFieldButton(R.drawable.ic_tabs)
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp?.instance?.activity?.let {
|
||||||
it.startActivity(Intent(it, ManageTabsActivity::class.java));
|
it.startActivity(Intent(it, ManageTabsActivity::class.java));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -145,7 +144,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
||||||
@FormFieldButton(R.drawable.ic_move_up)
|
@FormFieldButton(R.drawable.ic_move_up)
|
||||||
fun import() {
|
fun import() {
|
||||||
val act = SettingsActivity.getActivity() ?: return;
|
val act = StateApp.instance.activity ?: return;
|
||||||
val intent = MainActivity.getImportOptionsIntent(act);
|
val intent = MainActivity.getImportOptionsIntent(act);
|
||||||
act.startActivity(intent);
|
act.startActivity(intent);
|
||||||
}
|
}
|
||||||
@@ -154,7 +153,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormFieldButton(R.drawable.ic_link)
|
@FormFieldButton(R.drawable.ic_link)
|
||||||
fun manageLinks() {
|
fun manageLinks() {
|
||||||
try {
|
try {
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
|
StateApp.instance.activity?.let { UIDialogs.showUrlHandlingPrompt(it) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to show url handling prompt", e)
|
Logger.e(TAG, "Failed to show url handling prompt", e)
|
||||||
}
|
}
|
||||||
@@ -163,7 +162,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
|
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
|
||||||
@FormFieldButton(R.drawable.battery_full_24px)
|
@FormFieldButton(R.drawable.battery_full_24px)
|
||||||
fun ignoreBatteryOptimization() {
|
fun ignoreBatteryOptimization() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
||||||
@@ -244,7 +243,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun clearHidden() {
|
fun clearHidden() {
|
||||||
StateMeta.instance.removeAllHiddenCreators();
|
StateMeta.instance.removeAllHiddenCreators();
|
||||||
StateMeta.instance.removeAllHiddenVideos();
|
StateMeta.instance.removeAllHiddenVideos();
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
UIDialogs.toast(it, "Creators and videos should show up again");
|
UIDialogs.toast(it, "Creators and videos should show up again");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,9 +373,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
||||||
fun clearChannelCache() {
|
fun clearChannelCache() {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
UIDialogs.toast(StateApp.instance.activity!!, "Started clearing..");
|
||||||
StateCache.instance.clear();
|
StateCache.instance.clear();
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
|
UIDialogs.toast(StateApp.instance.activity!!, "Finished clearing");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,7 +759,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
try {
|
try {
|
||||||
if (!Logger.submitLogs()) {
|
if (!Logger.submitLogs()) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
|
StateApp.instance.activity?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -777,7 +776,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
|
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
|
||||||
fun resetAnnouncements() {
|
fun resetAnnouncements() {
|
||||||
StateAnnouncement.instance.resetAnnouncements();
|
StateAnnouncement.instance.resetAnnouncements();
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
StateApp.instance.activity?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,13 +844,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
|
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
|
||||||
fun changeStorageGeneral() {
|
fun changeStorageGeneral() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
StateApp.instance.changeExternalGeneralDirectory(it);
|
StateApp.instance.changeExternalGeneralDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
|
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
|
||||||
fun changeStorageDownload() {
|
fun changeStorageDownload() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
StateApp.instance.changeExternalDownloadDirectory(it);
|
StateApp.instance.changeExternalDownloadDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -860,7 +859,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun clearStorageDownload() {
|
fun clearStorageDownload() {
|
||||||
Settings.instance.storage.storage_download = null;
|
Settings.instance.storage.storage_download = null;
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") };
|
StateApp.instance.activity?.let { UIDialogs.toast(it, "Cleared download storage directory") };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -897,13 +896,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
|
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
|
||||||
fun manualCheck() {
|
fun manualCheck() {
|
||||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
StateUpdate.instance.checkForUpdates(it, true)
|
StateUpdate.instance.checkForUpdates(it, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
try {
|
try {
|
||||||
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
|
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
@@ -915,7 +914,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
|
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
|
||||||
fun viewChangelog() {
|
fun viewChangelog() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
@@ -955,7 +954,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
class Backup {
|
class Backup {
|
||||||
@Serializable(with = OffsetDateTimeSerializer::class)
|
@Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
var lastAutoBackupTime: OffsetDateTime = OffsetDateTime.MIN;
|
var lastAutoBackupTime: OffsetDateTime = OffsetDateTime.MIN;
|
||||||
var didAskAutoBackup: Boolean = false;
|
var didAskAutoBackup: Boolean = true;
|
||||||
var autoBackupPassword: String? = null;
|
var autoBackupPassword: String? = null;
|
||||||
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
||||||
|
|
||||||
@@ -964,13 +963,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
|
@FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
|
||||||
fun configureAutomaticBackup() {
|
fun configureAutomaticBackup() {
|
||||||
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
|
UIDialogs.showAutomaticBackupDialog(StateApp.instance.activity!!, autoBackupPassword != null) {
|
||||||
SettingsActivity.getActivity()?.reloadSettings();
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
|
@FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
|
||||||
fun restoreAutomaticBackup() {
|
fun restoreAutomaticBackup() {
|
||||||
val activity = SettingsActivity.getActivity()!!
|
val activity = StateApp.instance.activity!!
|
||||||
|
|
||||||
if(!StateBackup.hasAutomaticBackup())
|
if(!StateBackup.hasAutomaticBackup())
|
||||||
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
|
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
|
||||||
@@ -981,8 +980,9 @@ 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() {
|
||||||
val activity = SettingsActivity.getActivity() ?: return;
|
val activity = StateApp.instance.activity ?: return;
|
||||||
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
val fragView = SettingsFragment.currentView ?: return;
|
||||||
|
UISlideOverlays.showOverlay(fragView.overlay, "Select export type", null, {},
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
||||||
StateBackup.shareExternalBackup();
|
StateBackup.shareExternalBackup();
|
||||||
}),
|
}),
|
||||||
@@ -998,11 +998,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Payment {
|
class Payment {
|
||||||
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
|
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
|
||||||
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
|
val paymentStatus: String get() = StateApp.instance.activity?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
|
||||||
|
|
||||||
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
|
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
|
||||||
fun viewLicenseStatus() {
|
fun viewLicenseStatus() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
try {
|
try {
|
||||||
if (StatePayment.instance.hasPaid) {
|
if (StatePayment.instance.hasPaid) {
|
||||||
val paymentKey = StatePayment.instance.getPaymentKey()
|
val paymentKey = StatePayment.instance.getPaymentKey()
|
||||||
@@ -1018,12 +1018,12 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
|
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
|
||||||
fun clearPayment() {
|
fun clearPayment() {
|
||||||
SettingsActivity.getActivity()?.let { context ->
|
StateApp.instance.activity?.let { context ->
|
||||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
||||||
StatePayment.instance.clearLicenses();
|
StatePayment.instance.clearLicenses();
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||||
it.reloadSettings();
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1120,7 +1120,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@AdvancedField
|
@AdvancedField
|
||||||
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
|
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
|
||||||
fun configureSyncServer() {
|
fun configureSyncServer() {
|
||||||
SettingsActivity.getActivity()?.let { context ->
|
StateApp.instance.activity?.let { context ->
|
||||||
UIDialogs.showDialog(context, R.drawable.device_sync, false,
|
UIDialogs.showDialog(context, R.drawable.device_sync, false,
|
||||||
"Enter the url to your relay server",
|
"Enter the url to your relay server",
|
||||||
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
|
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
|
||||||
@@ -1131,13 +1131,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
UIDialogs.Action("Reset", {
|
UIDialogs.Action("Reset", {
|
||||||
syncServerUrl = null;
|
syncServerUrl = null;
|
||||||
instance.save();
|
instance.save();
|
||||||
context.reloadSettings();
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
UIDialogs.toast("Sync server changes require a restart");
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
}, UIDialogs.ActionStyle.ACCENT),
|
}, UIDialogs.ActionStyle.ACCENT),
|
||||||
UIDialogs.Action.withInput("Configure", {
|
UIDialogs.Action.withInput("Configure", {
|
||||||
syncServerUrl = it?.text
|
syncServerUrl = it?.text
|
||||||
instance.save();
|
instance.save();
|
||||||
context.reloadSettings();
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
UIDialogs.toast("Sync server changes require a restart");
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import androidx.work.OneTimeWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.futo.platformplayer.activities.DeveloperActivity
|
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
@@ -20,6 +18,8 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
|||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.DeveloperFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
@@ -97,10 +97,10 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
fun subscriptionsCache5000() {
|
fun subscriptionsCache5000() {
|
||||||
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"Started caching 5000 sub items"
|
"Started caching 5000 sub items"
|
||||||
);
|
);
|
||||||
val button = DeveloperActivity.getActivity()?.getField("subscription_cache_button");
|
val button = DeveloperFragment.currentView?.getField("subscription_cache_button");
|
||||||
if(button is ButtonField)
|
if(button is ButtonField)
|
||||||
button.setButtonEnabled(false);
|
button.setButtonEnabled(false);
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
@@ -121,7 +121,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
val diff = System.currentTimeMillis() - lastToast;
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
lastToast = System.currentTimeMillis();
|
lastToast = System.currentTimeMillis();
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"FINISHED Page: ${page}, Total: ${total}"
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -152,10 +152,10 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
fun historyCache100() {
|
fun historyCache100() {
|
||||||
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"Started caching 100 history items (from home)"
|
"Started caching 100 history items (from home)"
|
||||||
);
|
);
|
||||||
val button = DeveloperActivity.getActivity()?.getField("history_cache_button");
|
val button = DeveloperFragment.currentView?.getField("history_cache_button");
|
||||||
if(button is ButtonField)
|
if(button is ButtonField)
|
||||||
button.setButtonEnabled(false);
|
button.setButtonEnabled(false);
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
@@ -186,7 +186,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
val diff = System.currentTimeMillis() - lastToast;
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
lastToast = System.currentTimeMillis();
|
lastToast = System.currentTimeMillis();
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"FINISHED Page: ${page}, Total: ${total}"
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -235,9 +235,9 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
||||||
R.string.test_background_worker_description, 4)
|
R.string.test_background_worker_description, 4)
|
||||||
fun triggerBackgroundUpdate() {
|
fun triggerBackgroundUpdate() {
|
||||||
val act = SettingsActivity.getActivity()!!;
|
val act = StateApp.instance.activity!!;
|
||||||
try {
|
try {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
UIDialogs.toast(StateApp.instance.activity!!, "Starting test background worker");
|
||||||
|
|
||||||
val wm = WorkManager.getInstance(act);
|
val wm = WorkManager.getInstance(act);
|
||||||
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||||
@@ -251,9 +251,9 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
||||||
R.string.test_background_worker_description, 4)
|
R.string.test_background_worker_description, 4)
|
||||||
fun clearChannelContentCache() {
|
fun clearChannelContentCache() {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
|
UIDialogs.toast(StateApp.instance.activity!!, "Clearing cache");
|
||||||
StateCache.instance.clearToday();
|
StateCache.instance.clearToday();
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
|
UIDialogs.toast(StateApp.instance.activity!!, "Cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
|
|||||||
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
|
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
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.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@@ -74,6 +73,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
||||||
|
|
||||||
class UISlideOverlays {
|
class UISlideOverlays {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -331,15 +331,9 @@ class UISlideOverlays {
|
|||||||
0,
|
0,
|
||||||
UIDialogs.Action("Cancel", {}),
|
UIDialogs.Action("Cancel", {}),
|
||||||
UIDialogs.Action("Configure", {
|
UIDialogs.Action("Configure", {
|
||||||
val intent = Intent(
|
StateApp.instance.activity?.let {
|
||||||
mainContext,
|
it.navigate(it.getFragment<SettingsFragment>(), mainContext.getString(R.string.background_update))
|
||||||
SettingsActivity::class.java
|
}
|
||||||
);
|
|
||||||
intent.putExtra(
|
|
||||||
"query",
|
|
||||||
mainContext.getString(R.string.background_update)
|
|
||||||
);
|
|
||||||
mainContext.startActivity(intent);
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package com.futo.platformplayer.activities
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.futo.platformplayer.*
|
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
|
||||||
import com.futo.platformplayer.views.fields.IField
|
|
||||||
|
|
||||||
class DeveloperActivity : AppCompatActivity() {
|
|
||||||
private lateinit var _form: FieldForm;
|
|
||||||
private lateinit var _buttonBack: ImageButton;
|
|
||||||
|
|
||||||
fun getField(id: String): IField? {
|
|
||||||
return _form.findField(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
DeveloperActivity._lastActivity = this;
|
|
||||||
setContentView(R.layout.activity_dev);
|
|
||||||
setNavigationBarColorAndIcons();
|
|
||||||
|
|
||||||
_buttonBack = findViewById(R.id.button_back);
|
|
||||||
_form = findViewById(R.id.settings_form);
|
|
||||||
|
|
||||||
_form.fromObject(SettingsDev.instance);
|
|
||||||
_form.onChanged.subscribe { _, _ ->
|
|
||||||
_form.setObjectValues();
|
|
||||||
SettingsDev.instance.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
_buttonBack.setOnClickListener {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
|
||||||
super.finish()
|
|
||||||
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
//TODO: Temporary for solving Settings issues
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private var _lastActivity: DeveloperActivity? = null;
|
|
||||||
|
|
||||||
fun getActivity(): DeveloperActivity? {
|
|
||||||
val act = _lastActivity;
|
|
||||||
if(act != null)
|
|
||||||
return act;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,6 +33,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.withStateAtLeast
|
import androidx.lifecycle.withStateAtLeast
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import com.curlbind.Libcurl
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.RootInsetsController
|
import com.futo.platformplayer.RootInsetsController
|
||||||
@@ -52,17 +53,27 @@ import com.futo.platformplayer.fragment.mainactivity.main.CommentsFragment
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.ContentSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ContentSearchResultsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.DeveloperFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.DownloadsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.DownloadsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFilesFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibrarySearchFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryVideosFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
||||||
@@ -76,6 +87,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.St
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
@@ -147,6 +159,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragTopBarNavigation: NavigationTopBarFragment;
|
lateinit var _fragTopBarNavigation: NavigationTopBarFragment;
|
||||||
lateinit var _fragTopBarImport: ImportTopBarFragment;
|
lateinit var _fragTopBarImport: ImportTopBarFragment;
|
||||||
lateinit var _fragTopBarAdd: AddTopBarFragment;
|
lateinit var _fragTopBarAdd: AddTopBarFragment;
|
||||||
|
lateinit var _fragTopBarFiles: FilesTopBarFragment;
|
||||||
|
|
||||||
//Frags BotBar
|
//Frags BotBar
|
||||||
lateinit var _fragBotBarMenu: MenuBottomBarFragment;
|
lateinit var _fragBotBarMenu: MenuBottomBarFragment;
|
||||||
@@ -179,6 +192,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragBuy: BuyFragment;
|
lateinit var _fragBuy: BuyFragment;
|
||||||
lateinit var _fragSubGroup: SubscriptionGroupFragment;
|
lateinit var _fragSubGroup: SubscriptionGroupFragment;
|
||||||
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
|
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
|
||||||
|
lateinit var _fragLibrary: LibraryFragment;
|
||||||
|
lateinit var _fragLibraryAlbums: LibraryAlbumsFragment;
|
||||||
|
lateinit var _fragLibraryAlbum: LibraryAlbumFragment;
|
||||||
|
lateinit var _fragLibraryArtists: LibraryArtistsFragment;
|
||||||
|
lateinit var _fragLibraryArtist: LibraryArtistFragment;
|
||||||
|
lateinit var _fragLibraryVideos: LibraryVideosFragment;
|
||||||
|
lateinit var _fragLibrarySearch: LibrarySearchFragment;
|
||||||
|
lateinit var _fragLibraryFiles: LibraryFilesFragment;
|
||||||
|
lateinit var _fragSettings: SettingsFragment;
|
||||||
|
lateinit var _fragDeveloper: DeveloperFragment;
|
||||||
|
|
||||||
lateinit var _fragBrowser: BrowserFragment;
|
lateinit var _fragBrowser: BrowserFragment;
|
||||||
|
|
||||||
@@ -220,6 +243,17 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val _notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
private val _notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
if (isGranted)
|
||||||
|
UIDialogs.toast(this, "Notification permission granted");
|
||||||
|
else
|
||||||
|
UIDialogs.toast(this, "Notification permission denied");
|
||||||
|
};
|
||||||
|
|
||||||
|
fun requestNotificationPermissions() {
|
||||||
|
_notificationPermissionLauncher?.launch(_notifPermission);
|
||||||
|
}
|
||||||
|
|
||||||
val mainId = UUID.randomUUID().toString().substring(0, 5)
|
val mainId = UUID.randomUUID().toString().substring(0, 5)
|
||||||
|
|
||||||
@@ -275,6 +309,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Logger.w(TAG, "MainActivity Starting [$mainId]");
|
Logger.w(TAG, "MainActivity Starting [$mainId]");
|
||||||
|
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
|
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
|
|
||||||
@@ -318,6 +353,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragTopBarNavigation = NavigationTopBarFragment.newInstance();
|
_fragTopBarNavigation = NavigationTopBarFragment.newInstance();
|
||||||
_fragTopBarImport = ImportTopBarFragment.newInstance();
|
_fragTopBarImport = ImportTopBarFragment.newInstance();
|
||||||
_fragTopBarAdd = AddTopBarFragment.newInstance();
|
_fragTopBarAdd = AddTopBarFragment.newInstance();
|
||||||
|
_fragTopBarFiles = FilesTopBarFragment.newInstance();
|
||||||
|
|
||||||
//BotBars
|
//BotBars
|
||||||
_fragBotBarMenu = MenuBottomBarFragment.newInstance();
|
_fragBotBarMenu = MenuBottomBarFragment.newInstance();
|
||||||
@@ -350,6 +386,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragBuy = BuyFragment.newInstance();
|
_fragBuy = BuyFragment.newInstance();
|
||||||
_fragSubGroup = SubscriptionGroupFragment.newInstance();
|
_fragSubGroup = SubscriptionGroupFragment.newInstance();
|
||||||
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
|
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
|
||||||
|
_fragLibrary = LibraryFragment.newInstance();
|
||||||
|
_fragLibraryAlbums = LibraryAlbumsFragment.newInstance();
|
||||||
|
_fragLibraryAlbum = LibraryAlbumFragment.newInstance();
|
||||||
|
_fragLibraryArtists = LibraryArtistsFragment.newInstance();
|
||||||
|
_fragLibraryArtist = LibraryArtistFragment.newInstance();
|
||||||
|
_fragLibraryVideos = LibraryVideosFragment.newInstance();
|
||||||
|
_fragLibraryFiles = LibraryFilesFragment.newInstance();
|
||||||
|
_fragLibrarySearch = LibrarySearchFragment.newInstance();
|
||||||
|
_fragSettings = SettingsFragment.newInstance();
|
||||||
|
_fragDeveloper = DeveloperFragment.newInstance();
|
||||||
|
|
||||||
_fragBrowser = BrowserFragment.newInstance();
|
_fragBrowser = BrowserFragment.newInstance();
|
||||||
|
|
||||||
@@ -481,6 +527,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
||||||
_fragImportPlaylists.topBar = _fragTopBarImport;
|
_fragImportPlaylists.topBar = _fragTopBarImport;
|
||||||
_fragSubGroupList.topBar = _fragTopBarAdd;
|
_fragSubGroupList.topBar = _fragTopBarAdd;
|
||||||
|
_fragLibrary.topBar = _fragTopBarGeneral;
|
||||||
|
_fragLibraryAlbums.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryAlbum.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryArtists.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryArtist.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryVideos.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryFiles.topBar = _fragTopBarFiles;
|
||||||
|
_fragLibrarySearch.topBar = _fragTopBarSearch;
|
||||||
|
_fragSettings.topBar = _fragTopBarNavigation;
|
||||||
|
_fragDeveloper.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
@@ -1256,6 +1312,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
VideoDetailFragment::class -> _fragVideoDetail as T;
|
VideoDetailFragment::class -> _fragVideoDetail as T;
|
||||||
MenuBottomBarFragment::class -> _fragBotBarMenu as T;
|
MenuBottomBarFragment::class -> _fragBotBarMenu as T;
|
||||||
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
||||||
|
FilesTopBarFragment::class -> _fragTopBarFiles as T;
|
||||||
SearchTopBarFragment::class -> _fragTopBarSearch as T;
|
SearchTopBarFragment::class -> _fragTopBarSearch as T;
|
||||||
CreatorsFragment::class -> _fragMainSubscriptions as T;
|
CreatorsFragment::class -> _fragMainSubscriptions as T;
|
||||||
CommentsFragment::class -> _fragMainComments as T;
|
CommentsFragment::class -> _fragMainComments as T;
|
||||||
@@ -1280,6 +1337,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
BuyFragment::class -> _fragBuy as T;
|
BuyFragment::class -> _fragBuy as T;
|
||||||
SubscriptionGroupFragment::class -> _fragSubGroup as T;
|
SubscriptionGroupFragment::class -> _fragSubGroup as T;
|
||||||
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
|
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
|
||||||
|
LibraryFragment::class -> _fragLibrary as T;
|
||||||
|
LibraryAlbumsFragment::class -> _fragLibraryAlbums as T;
|
||||||
|
LibraryAlbumFragment::class -> _fragLibraryAlbum as T;
|
||||||
|
LibraryArtistsFragment::class -> _fragLibraryArtists as T;
|
||||||
|
LibraryArtistFragment::class -> _fragLibraryArtist as T;
|
||||||
|
LibraryVideosFragment::class -> _fragLibraryVideos as T;
|
||||||
|
LibraryFilesFragment::class -> _fragLibraryFiles as T;
|
||||||
|
LibrarySearchFragment::class -> _fragLibrarySearch as T;
|
||||||
|
SettingsFragment:: class -> _fragSettings as T;
|
||||||
|
DeveloperFragment::class -> _fragDeveloper as T;
|
||||||
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
package com.futo.platformplayer.activities
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.futo.platformplayer.*
|
|
||||||
import com.futo.platformplayer.constructs.Event0
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.views.LoaderView
|
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
|
||||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
|
||||||
import com.google.android.material.button.MaterialButton
|
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|
||||||
private lateinit var _form: FieldForm;
|
|
||||||
private lateinit var _buttonBack: ImageButton;
|
|
||||||
private lateinit var _loaderView: LoaderView;
|
|
||||||
|
|
||||||
private lateinit var _devSets: LinearLayout;
|
|
||||||
private lateinit var _buttonDev: MaterialButton;
|
|
||||||
|
|
||||||
private var _isFinished = false;
|
|
||||||
|
|
||||||
lateinit var overlay: FrameLayout;
|
|
||||||
|
|
||||||
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
|
||||||
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
|
||||||
if (isGranted)
|
|
||||||
UIDialogs.toast(this, "Notification permission granted");
|
|
||||||
else
|
|
||||||
UIDialogs.toast(this, "Notification permission denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
|
||||||
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
|
||||||
}
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_settings);
|
|
||||||
setNavigationBarColorAndIcons();
|
|
||||||
|
|
||||||
_form = findViewById(R.id.settings_form);
|
|
||||||
_buttonBack = findViewById(R.id.button_back);
|
|
||||||
_buttonDev = findViewById(R.id.button_dev);
|
|
||||||
_devSets = findViewById(R.id.dev_settings);
|
|
||||||
_loaderView = findViewById(R.id.loader);
|
|
||||||
overlay = findViewById(R.id.overlay_container);
|
|
||||||
|
|
||||||
_form.onChanged.subscribe { field, _ ->
|
|
||||||
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
|
||||||
_form.setObjectValues();
|
|
||||||
Settings.instance.save();
|
|
||||||
|
|
||||||
if(field.descriptor?.id == "app_language") {
|
|
||||||
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
|
||||||
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(field.descriptor?.id == "background_update") {
|
|
||||||
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
|
|
||||||
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
|
|
||||||
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
|
||||||
if(!notifManager.areNotificationsEnabled()) {
|
|
||||||
UIDialogs.toast(this, "Notifications aren't enabled");
|
|
||||||
|
|
||||||
when {
|
|
||||||
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
|
||||||
|
|
||||||
}
|
|
||||||
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
|
|
||||||
"Notifications need to be enabled for background updating to function", null, 0,
|
|
||||||
UIDialogs.Action("Cancel", {}),
|
|
||||||
UIDialogs.Action("Enable", {
|
|
||||||
requestPermissionLauncher.launch(notifPermission);
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
requestPermissionLauncher.launch(notifPermission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_buttonBack.setOnClickListener {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
_buttonDev.setOnClickListener {
|
|
||||||
startActivity(Intent(this, DeveloperActivity::class.java));
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastActivity = this;
|
|
||||||
|
|
||||||
reloadSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
var isFirstLoad = true;
|
|
||||||
fun reloadSettings() {
|
|
||||||
val firstLoad = isFirstLoad;
|
|
||||||
isFirstLoad = false;
|
|
||||||
_form.setSearchVisible(false);
|
|
||||||
_loaderView.start();
|
|
||||||
_form.fromObject(lifecycleScope, Settings.instance) {
|
|
||||||
_loaderView.stop();
|
|
||||||
_form.setSearchVisible(true);
|
|
||||||
|
|
||||||
var devCounter = 0;
|
|
||||||
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
|
||||||
devCounter++;
|
|
||||||
if(devCounter > 5) {
|
|
||||||
devCounter = 0;
|
|
||||||
SettingsDev.instance.developerMode = true;
|
|
||||||
SettingsDev.instance.save();
|
|
||||||
updateDevMode();
|
|
||||||
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if(firstLoad) {
|
|
||||||
val query = intent.getStringExtra("query");
|
|
||||||
if(!query.isNullOrEmpty()) {
|
|
||||||
_form.setSearchQuery(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
updateDevMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateDevMode() {
|
|
||||||
if(SettingsDev.instance.developerMode)
|
|
||||||
_devSets.visibility = View.VISIBLE;
|
|
||||||
else
|
|
||||||
_devSets.visibility = View.GONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
|
||||||
super.finish()
|
|
||||||
_isFinished = true;
|
|
||||||
if(_lastActivity == this)
|
|
||||||
_lastActivity = null;
|
|
||||||
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
|
||||||
private var requestCode: Int? = -1;
|
|
||||||
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
result: ActivityResult ->
|
|
||||||
val handler = synchronized(resultLauncherMap) {
|
|
||||||
resultLauncherMap.remove(requestCode);
|
|
||||||
}
|
|
||||||
if(handler != null)
|
|
||||||
handler(result);
|
|
||||||
};
|
|
||||||
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
|
|
||||||
synchronized(resultLauncherMap) {
|
|
||||||
resultLauncherMap[code] = handler;
|
|
||||||
}
|
|
||||||
requestCode = code;
|
|
||||||
resultLauncher.launch(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
settingsActivityClosed.emit()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
//TODO: Temporary for solving Settings issues
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private var _lastActivity: SettingsActivity? = null;
|
|
||||||
|
|
||||||
val settingsActivityClosed = Event0()
|
|
||||||
|
|
||||||
fun getActivity(): SettingsActivity? {
|
|
||||||
val act = _lastActivity;
|
|
||||||
if(act != null && !act._isFinished)
|
|
||||||
return act;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+157
-2
@@ -1,5 +1,160 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.local
|
package com.futo.platformplayer.api.media.platforms.local
|
||||||
|
|
||||||
class LocalClient {
|
import android.content.ContentResolver
|
||||||
//TODO
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import java.net.MalformedURLException
|
||||||
|
|
||||||
|
class LocalClient: IPlatformClient {
|
||||||
|
override val id: String = "LOCAL"
|
||||||
|
override val name: String = "Local"
|
||||||
|
override val icon: ImageVariable? = ImageVariable.fromResource(R.drawable.ic_library)
|
||||||
|
override val capabilities: PlatformClientCapabilities = PlatformClientCapabilities()
|
||||||
|
|
||||||
|
override fun initialize() {}
|
||||||
|
|
||||||
|
override fun disable() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHome(): IPager<IPlatformContent>
|
||||||
|
= EmptyPager();
|
||||||
|
|
||||||
|
override fun isContentDetailsUrl(url: String): Boolean {
|
||||||
|
try {
|
||||||
|
val uri = Uri.parse(url);
|
||||||
|
return ContentResolver.SCHEME_CONTENT == uri.scheme
|
||||||
|
&& (
|
||||||
|
MediaStore.AUTHORITY == uri.authority ||
|
||||||
|
uri.authority == "com.android.externalstorage.documents"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
catch(ex: MalformedURLException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioExtensions = listOf(".mp3", ".wav", ".flac", ".mp4a", ".m4a");
|
||||||
|
override fun getContentDetails(url: String): IPlatformContentDetails {
|
||||||
|
val uri = Uri.parse(url);
|
||||||
|
|
||||||
|
if("audio" in uri.pathSegments) {
|
||||||
|
return StateLibrary.getAudioTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||||
|
}
|
||||||
|
else if("video" in uri.pathSegments) {
|
||||||
|
return StateLibrary.getVideoTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||||
|
}
|
||||||
|
else if(uri.toString().contains("com.android.externalstorage.documents")) {
|
||||||
|
if(audioExtensions.any { uri.lastPathSegment?.lowercase()?.endsWith(it) ?: false })
|
||||||
|
return StateLibrary.getAudioTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||||
|
else
|
||||||
|
return StateLibrary.getVideoTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw Exception("Unknown content url [${url}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSearchCapabilities(): ResultCapabilities
|
||||||
|
= ResultCapabilities();
|
||||||
|
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> {
|
||||||
|
return EmptyPager(); //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSearchChannelContentsCapabilities(): ResultCapabilities
|
||||||
|
= ResultCapabilities();
|
||||||
|
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> {
|
||||||
|
return EmptyPager(); //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchChannels(query: String): IPager<PlatformAuthorLink> {
|
||||||
|
return EmptyPager(); //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> {
|
||||||
|
return EmptyPager(); //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isChannelUrl(url: String): Boolean {
|
||||||
|
return false //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChannel(channelUrl: String): IPlatformChannel {
|
||||||
|
throw NotImplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChannelCapabilities(): ResultCapabilities
|
||||||
|
= ResultCapabilities();
|
||||||
|
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> {
|
||||||
|
return EmptyPager();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> {
|
||||||
|
return EmptyPager();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPeekChannelTypes(): List<String> = listOf();
|
||||||
|
|
||||||
|
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent>
|
||||||
|
= listOf();
|
||||||
|
|
||||||
|
override fun getShorts(): IPager<IPlatformVideo> = EmptyPager();
|
||||||
|
|
||||||
|
override fun searchSuggestions(query: String): Array<String> = arrayOf();
|
||||||
|
|
||||||
|
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun getContentChapters(url: String): List<IChapter>
|
||||||
|
= listOf();
|
||||||
|
|
||||||
|
override fun getPlaybackTracker(url: String): IPlaybackTracker?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun getContentRecommendations(url: String): IPager<IPlatformContent>?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun getComments(url: String): IPager<IPlatformComment>
|
||||||
|
= EmptyPager();
|
||||||
|
|
||||||
|
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment>
|
||||||
|
= EmptyPager();
|
||||||
|
|
||||||
|
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent>
|
||||||
|
= throw NotImplementedError();
|
||||||
|
|
||||||
|
override fun isPlaylistUrl(url: String): Boolean = false;
|
||||||
|
|
||||||
|
override fun getPlaylist(url: String): IPlatformPlaylistDetails
|
||||||
|
= throw NotImplementedError();
|
||||||
|
override fun getUserPlaylists(): Array<String> = throw NotImplementedError();
|
||||||
|
override fun getUserSubscriptions(): Array<String> = throw NotImplementedError();
|
||||||
|
override fun getUserHistory(): IPager<IPlatformContent> = throw NotImplementedError();
|
||||||
|
override fun isClaimTypeSupported(claimType: Int): Boolean = false;
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,7 @@ import com.futo.platformplayer.engine.internal.V8Converter
|
|||||||
import com.futo.platformplayer.engine.packages.PackageBridge
|
import com.futo.platformplayer.engine.packages.PackageBridge
|
||||||
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
||||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||||
|
import com.futo.platformplayer.engine.packages.PackageHttpImp
|
||||||
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
||||||
import com.futo.platformplayer.engine.packages.PackageUtilities
|
import com.futo.platformplayer.engine.packages.PackageUtilities
|
||||||
import com.futo.platformplayer.engine.packages.V8Package
|
import com.futo.platformplayer.engine.packages.V8Package
|
||||||
@@ -383,6 +384,7 @@ class V8Plugin {
|
|||||||
return when(packageName) {
|
return when(packageName) {
|
||||||
"DOMParser" -> PackageDOMParser(this)
|
"DOMParser" -> PackageDOMParser(this)
|
||||||
"Http" -> PackageHttp(this, config)
|
"Http" -> PackageHttp(this, config)
|
||||||
|
"HttpImp" -> PackageHttpImp(this, config)
|
||||||
"Utilities" -> PackageUtilities(this, config)
|
"Utilities" -> PackageUtilities(this, config)
|
||||||
"JSDOM" -> PackageJSDOM(this, config)
|
"JSDOM" -> PackageJSDOM(this, config)
|
||||||
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package com.curlbind
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import kotlin.collections.iterator
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
object Libcurl {
|
||||||
|
init {
|
||||||
|
System.loadLibrary("curl-impersonate")
|
||||||
|
System.loadLibrary("curl-impersonate-jni")
|
||||||
|
// CURL_GLOBAL_ALL = 3
|
||||||
|
require(ce_global_init(3) == CURLcode.CURLE_OK) { "curl_global_init failed" }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class Request(
|
||||||
|
var url: String,
|
||||||
|
var method: String = "GET",
|
||||||
|
var headers: Map<String, String> = emptyMap(),
|
||||||
|
var body: ByteArray? = null,
|
||||||
|
var impersonateTarget: String = "chrome136",
|
||||||
|
var useBuiltInHeaders: Boolean = true,
|
||||||
|
var timeoutMs: Int = 30_000,
|
||||||
|
var cookieJarPath: String? = null,
|
||||||
|
var sendCookies: Boolean = true,
|
||||||
|
var persistCookies: Boolean = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class Response(
|
||||||
|
val status: Int,
|
||||||
|
val effectiveUrl: String,
|
||||||
|
val bodyBytes: ByteArray,
|
||||||
|
val headers: Map<String, List<String>>
|
||||||
|
)
|
||||||
|
|
||||||
|
object CURLcode {
|
||||||
|
const val CURLE_OK = 0
|
||||||
|
const val CURLE_UNKNOWN_OPTION = 48
|
||||||
|
}
|
||||||
|
|
||||||
|
object CurlInfoConsts {
|
||||||
|
const val CURLINFO_STRING = 0x100000
|
||||||
|
const val CURLINFO_LONG = 0x200000
|
||||||
|
const val CURLINFO_DOUBLE = 0x300000
|
||||||
|
const val CURLINFO_SLIST = 0x400000
|
||||||
|
const val CURLINFO_PTR = 0x400000
|
||||||
|
const val CURLINFO_SOCKET = 0x500000
|
||||||
|
const val CURLINFO_OFF_T = 0x600000
|
||||||
|
const val CURLINFO_MASK = 0x0fffff
|
||||||
|
const val CURLINFO_TYPEMASK = 0xf00000
|
||||||
|
}
|
||||||
|
|
||||||
|
object CURLINFO {
|
||||||
|
const val NONE = 0
|
||||||
|
const val EFFECTIVE_URL = CurlInfoConsts.CURLINFO_STRING + 1
|
||||||
|
const val RESPONSE_CODE = CurlInfoConsts.CURLINFO_LONG + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
object CURLOPT {
|
||||||
|
const val URL = 10002
|
||||||
|
const val FOLLOWLOCATION = 52
|
||||||
|
const val MAXREDIRS = 68
|
||||||
|
const val CONNECTTIMEOUT_MS = 156
|
||||||
|
const val TIMEOUT_MS = 155
|
||||||
|
const val HTTP_VERSION = 84
|
||||||
|
const val ACCEPT_ENCODING = 10102
|
||||||
|
const val HTTPHEADER = 10023
|
||||||
|
const val COOKIEFILE = 10031
|
||||||
|
const val COOKIEJAR = 10082
|
||||||
|
const val CUSTOMREQUEST = 10036
|
||||||
|
const val IPRESOLVE = 113
|
||||||
|
const val POSTFIELDS = 10015
|
||||||
|
const val POSTFIELDSIZE = 60
|
||||||
|
const val WRITEFUNCTION = 20011
|
||||||
|
const val HEADERFUNCTION = 20079
|
||||||
|
const val WRITEDATA = 10001
|
||||||
|
const val HEADERDATA = 10029
|
||||||
|
const val COPYPOSTFIELDS = 10165
|
||||||
|
const val CURLOPT_DNS_SERVERS = 10211
|
||||||
|
const val CAPATH = 10097
|
||||||
|
const val CAINFO = 10065
|
||||||
|
}
|
||||||
|
|
||||||
|
object CURL_HTTP_VERSION { const val TWO_TLS = 4 }
|
||||||
|
object CURL_IPRESOLVE { const val WHATEVER = 0; const val V4 = 1; const val V6 = 2 }
|
||||||
|
|
||||||
|
@Keep interface WriteCallback { fun onWrite(chunk: ByteArray): Int }
|
||||||
|
@Keep interface HeaderCallback { fun onHeader(line: ByteArray): Int }
|
||||||
|
|
||||||
|
@Volatile private var defaultCAPath: String? = null
|
||||||
|
@Keep fun setDefaultCAPath(path: String) { defaultCAPath = path }
|
||||||
|
|
||||||
|
fun perform(req: Request): Response {
|
||||||
|
val easy = ce_easy_init()
|
||||||
|
require(easy != 0L) { "curl_easy_init failed" }
|
||||||
|
|
||||||
|
var slist: Long = 0L
|
||||||
|
val bodySink = ByteArrayOutputStream(64 * 1024)
|
||||||
|
val rawHeaderLines = ArrayList<String>(64)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val imp = ce_easy_impersonate(easy, req.impersonateTarget, req.useBuiltInHeaders)
|
||||||
|
if (imp != CURLcode.CURLE_OK && imp != CURLcode.CURLE_UNKNOWN_OPTION) {
|
||||||
|
error("curl_easy_impersonate failed: ${ce_easy_strerror(imp)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOK(ce_setopt_str(easy, CURLOPT.URL, req.url))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.FOLLOWLOCATION, 1))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.MAXREDIRS, 10))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.CONNECTTIMEOUT_MS, req.timeoutMs.toLong()))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.TIMEOUT_MS, req.timeoutMs.toLong()))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.HTTP_VERSION, CURL_HTTP_VERSION.TWO_TLS.toLong()))
|
||||||
|
checkOK(ce_setopt_str(easy, CURLOPT.ACCEPT_ENCODING, "")) // enable auto-decompress
|
||||||
|
|
||||||
|
if (req.headers.isNotEmpty()) {
|
||||||
|
for ((k, v) in req.headers) slist = ce_slist_append(slist, "$k: $v")
|
||||||
|
if (slist != 0L) checkOK(ce_setopt_ptr(easy, CURLOPT.HTTPHEADER, slist))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.sendCookies || req.persistCookies) {
|
||||||
|
val jar = (req.cookieJarPath ?: defaultCookieJarPath())
|
||||||
|
if (req.sendCookies) checkOK(ce_setopt_str(easy, CURLOPT.COOKIEFILE, jar))
|
||||||
|
if (req.persistCookies) checkOK(ce_setopt_str(easy, CURLOPT.COOKIEJAR, jar))
|
||||||
|
}
|
||||||
|
|
||||||
|
val method = req.method
|
||||||
|
if (!method.equals("GET", ignoreCase = true)) {
|
||||||
|
checkOK(ce_setopt_str(easy, CURLOPT.CUSTOMREQUEST, method))
|
||||||
|
val body = req.body
|
||||||
|
if (body != null && body.isNotEmpty()) {
|
||||||
|
checkOK(ce_set_postfields(easy, body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOK(ce_set_write_callback(easy, object : WriteCallback {
|
||||||
|
override fun onWrite(chunk: ByteArray): Int {
|
||||||
|
bodySink.write(chunk)
|
||||||
|
return chunk.size
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
checkOK(ce_set_header_callback(easy, object : HeaderCallback {
|
||||||
|
override fun onHeader(line: ByteArray): Int {
|
||||||
|
// Keep raw but trim CRLF for convenience
|
||||||
|
val s = line.toString(Charset.forName("ISO-8859-1")).trimEnd('\r', '\n')
|
||||||
|
if (s.isNotBlank()) rawHeaderLines.add(s)
|
||||||
|
return line.size
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
checkOK(ce_setopt_str(easy, CURLOPT.CURLOPT_DNS_SERVERS, "1.1.1.1,8.8.8.8"));
|
||||||
|
defaultCAPath?.let { checkOK(ce_setopt_str(easy, CURLOPT.CAINFO, it)) }
|
||||||
|
|
||||||
|
val rc = ce_easy_perform(easy)
|
||||||
|
if (rc != CURLcode.CURLE_OK) error("curl_easy_perform failed: ${ce_easy_strerror(rc)}")
|
||||||
|
|
||||||
|
val codeArr = longArrayOf(0)
|
||||||
|
checkOK(ce_easy_getinfo_long(easy, CURLINFO.RESPONSE_CODE, codeArr))
|
||||||
|
val effective = ce_easy_getinfo_string(easy, CURLINFO.EFFECTIVE_URL) ?: req.url
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status = codeArr[0].toInt(),
|
||||||
|
effectiveUrl = effective,
|
||||||
|
bodyBytes = bodySink.toByteArray(),
|
||||||
|
headers = parseHeaders(rawHeaderLines)
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
if (slist != 0L) ce_slist_free_all(slist)
|
||||||
|
ce_easy_cleanup(easy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultCookieJarPath(): String {
|
||||||
|
val tmp = System.getProperty("java.io.tmpdir") ?: "/data/local/tmp"
|
||||||
|
return if (tmp.endsWith("/")) "${tmp}imphttp.cookies.txt" else "$tmp/imphttp.cookies.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkOK(code: Int) {
|
||||||
|
if (code != CURLcode.CURLE_OK) throw IllegalStateException("libcurl error: ${ce_easy_strerror(code)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseHeaders(lines: List<String>): Map<String, List<String>> {
|
||||||
|
val map = linkedMapOf<String, MutableList<String>>()
|
||||||
|
for (line in lines) {
|
||||||
|
val idx = line.indexOf(':')
|
||||||
|
if (idx <= 0) continue
|
||||||
|
val name = line.substring(0, idx).trim()
|
||||||
|
val value = line.substring(min(idx + 1, line.length)).trim()
|
||||||
|
map.getOrPut(name) { mutableListOf() }.add(value)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic external fun ce_set_write_callback(easy: Long, cb: WriteCallback?): Int
|
||||||
|
@JvmStatic external fun ce_set_header_callback(easy: Long, cb: HeaderCallback?): Int
|
||||||
|
|
||||||
|
@JvmStatic external fun ce_global_init(flags: Long): Int
|
||||||
|
@JvmStatic external fun ce_global_cleanup()
|
||||||
|
@JvmStatic external fun ce_easy_init(): Long
|
||||||
|
@JvmStatic external fun ce_easy_cleanup(easy: Long)
|
||||||
|
@JvmStatic external fun ce_easy_perform(easy: Long): Int
|
||||||
|
|
||||||
|
@JvmStatic external fun ce_easy_impersonate(easy: Long, target: String, defaultHeaders: Boolean): Int
|
||||||
|
@JvmStatic external fun ce_setopt_long(easy: Long, opt: Int, value: Long): Int
|
||||||
|
@JvmStatic external fun ce_setopt_str(easy: Long, opt: Int, value: String): Int
|
||||||
|
@JvmStatic external fun ce_setopt_ptr(easy: Long, opt: Int, ptr: Long): Int
|
||||||
|
@JvmStatic external fun ce_slist_append(list: Long, header: String): Long
|
||||||
|
@JvmStatic external fun ce_slist_free_all(list: Long)
|
||||||
|
@JvmStatic external fun ce_easy_getinfo_long(easy: Long, info: Int, outVal: LongArray): Int
|
||||||
|
@JvmStatic external fun ce_easy_getinfo_string(easy: Long, info: Int): String?
|
||||||
|
|
||||||
|
@JvmStatic external fun ce_set_postfields(easy: Long, body: ByteArray): Int
|
||||||
|
@JvmStatic external fun ce_easy_strerror(code: Int): String
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ class PackageDOMParser : V8Package {
|
|||||||
}
|
}
|
||||||
@V8Property
|
@V8Property
|
||||||
fun lastChild(): DOMNode? {
|
fun lastChild(): DOMNode? {
|
||||||
val result = _element.firstElementChild()?.let { DOMNode(_package, it) };
|
val result = _element.lastElementChild()?.let { DOMNode(_package, it) };
|
||||||
if(result != null)
|
if(result != null)
|
||||||
_children.add(result);
|
_children.add(result);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,787 @@
|
|||||||
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import com.caoccao.javet.annotations.V8Convert
|
||||||
|
import com.caoccao.javet.annotations.V8Function
|
||||||
|
import com.caoccao.javet.annotations.V8Property
|
||||||
|
import com.caoccao.javet.enums.V8ConversionMode
|
||||||
|
import com.caoccao.javet.enums.V8ProxyMode
|
||||||
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
||||||
|
import com.curlbind.Libcurl
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||||
|
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
import java.util.concurrent.ForkJoinTask
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class PackageHttpImp : V8Package {
|
||||||
|
@Transient
|
||||||
|
internal val _config: IV8PluginConfig
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private val _packageClient: PackageHttpClient
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private val _packageClientAuth: PackageHttpClient
|
||||||
|
|
||||||
|
override val name: String get() = "HttpImp"
|
||||||
|
override val variableName: String get() = "httpimp"
|
||||||
|
|
||||||
|
private var _batchPoolLock: Any = Any()
|
||||||
|
private var _batchPool: ForkJoinPool? = null
|
||||||
|
|
||||||
|
private val _clients = mutableMapOf<String, PackageHttpClient>()
|
||||||
|
|
||||||
|
constructor(plugin: V8Plugin, config: IV8PluginConfig) : super(plugin) {
|
||||||
|
_config = config
|
||||||
|
_packageClient = PackageHttpClient(this, withAuth = false)
|
||||||
|
_packageClientAuth = PackageHttpClient(this, withAuth = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanup() {
|
||||||
|
Logger.w(TAG, "PackageHttpImp Cleaning up")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T, R> autoParallelPool(
|
||||||
|
data: List<T>,
|
||||||
|
parallelism: Int,
|
||||||
|
handle: (T) -> R
|
||||||
|
): List<Pair<R?, Throwable?>> {
|
||||||
|
synchronized(_batchPoolLock) {
|
||||||
|
val threadsToUse = if (parallelism <= 0) data.size else min(parallelism, data.size)
|
||||||
|
if (_batchPool == null) {
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse)
|
||||||
|
}
|
||||||
|
var pool = _batchPool ?: return listOf()
|
||||||
|
if (pool.poolSize < threadsToUse) {
|
||||||
|
pool.shutdown()
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse)
|
||||||
|
pool = _batchPool ?: return listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
val resultTasks = mutableListOf<ForkJoinTask<Pair<R?, Throwable?>>>()
|
||||||
|
for (item in data) {
|
||||||
|
resultTasks.add(
|
||||||
|
pool.submit<Pair<R?, Throwable?>> {
|
||||||
|
try {
|
||||||
|
Pair(handle(item), null)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Pair<R?, Throwable?>(null, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return resultTasks.map { it.join() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun newClient(withAuth: Boolean): PackageHttpClient {
|
||||||
|
val client = PackageHttpClient(this, withAuth)
|
||||||
|
client.clientId()?.let { _clients[it] = client }
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun getDefaultClient(withAuth: Boolean): PackageHttpClient {
|
||||||
|
return if (withAuth) _packageClientAuth else _packageClient
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getClient(id: String?): PackageHttpClient {
|
||||||
|
if (id == null) throw IllegalArgumentException("Http client $id doesn't exist")
|
||||||
|
if (_packageClient.clientId() == id) return _packageClient
|
||||||
|
if (_packageClientAuth.clientId() == id) return _packageClientAuth
|
||||||
|
return _clients[id] ?: throw IllegalArgumentException("Http client $id doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun batch(): BatchBuilder {
|
||||||
|
return BatchBuilder(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun request(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useAuth: Boolean = false,
|
||||||
|
bytesResult: Boolean = false
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||||
|
return client.requestInternal(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
if (bytesResult) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun requestWithBody(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useAuth: Boolean = false,
|
||||||
|
bytesResult: Boolean = false
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||||
|
return client.requestWithBodyInternal(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
if (bytesResult) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun GET(
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useAuth: Boolean = false,
|
||||||
|
useByteResponse: Boolean = false
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||||
|
return client.GETInternal(
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
if (useByteResponse) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun POST(
|
||||||
|
url: String,
|
||||||
|
body: Any,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useAuth: Boolean = false,
|
||||||
|
useByteResponse: Boolean = false
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||||
|
|
||||||
|
return when (body) {
|
||||||
|
is V8ValueString ->
|
||||||
|
client.POSTInternal(url, body.value, headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
is String ->
|
||||||
|
client.POSTInternal(url, body, headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
is V8ValueTypedArray ->
|
||||||
|
client.POSTInternal(url, body.toBytes(), headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
is ByteArray ->
|
||||||
|
client.POSTInternal(url, body, headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
is ArrayList<*> ->
|
||||||
|
client.POSTInternal(
|
||||||
|
url,
|
||||||
|
body.map { (it as Double).toInt().toByte() }.toByteArray(),
|
||||||
|
headers,
|
||||||
|
if (useByteResponse) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
)
|
||||||
|
else -> throw NotImplementedError("Body type ${body?.javaClass?.name} not implemented for POST")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> logExceptions(handle: () -> T): T {
|
||||||
|
try {
|
||||||
|
return handle()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.e("Plugin[${_config.name}]", ex.message, ex)
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IBridgeHttpResponse {
|
||||||
|
val url: String
|
||||||
|
val code: Int
|
||||||
|
val headers: Map<String, List<String>>?
|
||||||
|
}
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
class BridgeHttpStringResponse(
|
||||||
|
override val url: String,
|
||||||
|
override val code: Int,
|
||||||
|
val body: String?,
|
||||||
|
override val headers: Map<String, List<String>>? = null
|
||||||
|
) : IV8Convertable, IBridgeHttpResponse {
|
||||||
|
val isOk = code in 200..299
|
||||||
|
|
||||||
|
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||||
|
val obj = runtime.createV8ValueObject()
|
||||||
|
obj.set("url", url)
|
||||||
|
obj.set("code", code)
|
||||||
|
obj.set("body", body)
|
||||||
|
obj.set("headers", headers)
|
||||||
|
obj.set("isOk", isOk)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
class BridgeHttpBytesResponse(
|
||||||
|
override val url: String,
|
||||||
|
override val code: Int,
|
||||||
|
val body: ByteArray? = null,
|
||||||
|
override val headers: Map<String, List<String>>? = null
|
||||||
|
) : IV8Convertable, IBridgeHttpResponse {
|
||||||
|
val isOk: Boolean = code in 200..299
|
||||||
|
|
||||||
|
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||||
|
val obj = runtime.createV8ValueObject()
|
||||||
|
obj.set("url", url)
|
||||||
|
obj.set("code", code)
|
||||||
|
if (body != null) {
|
||||||
|
obj.set("body", body)
|
||||||
|
}
|
||||||
|
obj.set("headers", headers)
|
||||||
|
obj.set("isOk", isOk)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
||||||
|
class BatchBuilder(
|
||||||
|
@Transient private val _package: PackageHttpImp,
|
||||||
|
existingRequests: MutableList<Pair<PackageHttpClient, RequestDescriptor>> = mutableListOf()
|
||||||
|
) : V8BindObject() {
|
||||||
|
@Transient
|
||||||
|
private val _reqs = existingRequests
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun request(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useAuth: Boolean = false
|
||||||
|
): BatchBuilder {
|
||||||
|
return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun requestWithBody(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useAuth: Boolean = false
|
||||||
|
): BatchBuilder {
|
||||||
|
return clientRequestWithBody(_package.getDefaultClient(useAuth).clientId(), method, url, body, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun GET(
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useAuth: Boolean = false
|
||||||
|
): BatchBuilder =
|
||||||
|
clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun POST(
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useAuth: Boolean = false
|
||||||
|
): BatchBuilder =
|
||||||
|
clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun DUMMY(): BatchBuilder {
|
||||||
|
_reqs.add(
|
||||||
|
Pair(
|
||||||
|
_package.getDefaultClient(false),
|
||||||
|
RequestDescriptor("DUMMY", "", mutableMapOf())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return BatchBuilder(_package, _reqs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun clientRequest(
|
||||||
|
clientId: String?,
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap()
|
||||||
|
): BatchBuilder {
|
||||||
|
_reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers)))
|
||||||
|
return BatchBuilder(_package, _reqs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun clientRequestWithBody(
|
||||||
|
clientId: String?,
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap()
|
||||||
|
): BatchBuilder {
|
||||||
|
_reqs.add(
|
||||||
|
Pair(
|
||||||
|
_package.getClient(clientId),
|
||||||
|
RequestDescriptor(method, url, headers, body)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return BatchBuilder(_package, _reqs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun clientGET(
|
||||||
|
clientId: String?,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap()
|
||||||
|
): BatchBuilder =
|
||||||
|
clientRequest(clientId, "GET", url, headers)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun clientPOST(
|
||||||
|
clientId: String?,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap()
|
||||||
|
): BatchBuilder =
|
||||||
|
clientRequestWithBody(clientId, "POST", url, body, headers)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun execute(): List<IBridgeHttpResponse?> {
|
||||||
|
return _package.autoParallelPool(_reqs, -1) {
|
||||||
|
if (it.second.method == "DUMMY") {
|
||||||
|
return@autoParallelPool null
|
||||||
|
}
|
||||||
|
if (it.second.body != null) {
|
||||||
|
it.first.requestWithBodyInternal(
|
||||||
|
it.second.method,
|
||||||
|
it.second.url,
|
||||||
|
it.second.body!!,
|
||||||
|
it.second.headers,
|
||||||
|
it.second.respType
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
it.first.requestInternal(
|
||||||
|
it.second.method,
|
||||||
|
it.second.url,
|
||||||
|
it.second.headers,
|
||||||
|
it.second.respType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.map {
|
||||||
|
if (it.second != null) throw it.second!!
|
||||||
|
it.first
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
||||||
|
class PackageHttpClient : V8BindObject {
|
||||||
|
@Transient
|
||||||
|
private val _package: PackageHttpImp
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private val _withAuth: Boolean
|
||||||
|
|
||||||
|
val parentConfig: IV8PluginConfig
|
||||||
|
get() = _package._config
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private val _defaultHeaders = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private val _clientId: String = UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var timeoutMs: Int = 30_000
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var sendCookies: Boolean = true
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var persistCookies: Boolean = true
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var cookieJarPath: String? = null
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var impersonateTarget: String = "chrome136"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var useBuiltInHeaders: Boolean = true
|
||||||
|
|
||||||
|
@V8Property
|
||||||
|
fun clientId(): String? = _clientId
|
||||||
|
|
||||||
|
constructor(pack: PackageHttpImp, withAuth: Boolean) : super() {
|
||||||
|
_package = pack
|
||||||
|
_withAuth = withAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureCookieJarPath(): String {
|
||||||
|
val existing = cookieJarPath
|
||||||
|
if (existing != null) return existing
|
||||||
|
|
||||||
|
val tmp = System.getProperty("java.io.tmpdir") ?: "/data/local/tmp"
|
||||||
|
val safeName = parentConfig.name.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||||
|
val fileName =
|
||||||
|
if (_withAuth) "imphttp.$safeName.auth.cookies.txt" else "imphttp.$safeName.cookies.txt"
|
||||||
|
val path = if (tmp.endsWith("/")) tmp + fileName else "$tmp/$fileName"
|
||||||
|
cookieJarPath = path
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun setDefaultHeaders(defaultHeaders: Map<String, String>) {
|
||||||
|
synchronized(_defaultHeaders) {
|
||||||
|
for (pair in defaultHeaders) {
|
||||||
|
_defaultHeaders[pair.key] = pair.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun setDoApplyCookies(apply: Boolean) {
|
||||||
|
sendCookies = apply
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun setDoUpdateCookies(update: Boolean) {
|
||||||
|
persistCookies = update
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun setDoAllowNewCookies(allow: Boolean) {
|
||||||
|
persistCookies = allow
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun setTimeout(timeoutMs: Int) {
|
||||||
|
this.timeoutMs = timeoutMs
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun request(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useBytes: Boolean = false
|
||||||
|
): IBridgeHttpResponse =
|
||||||
|
requestInternal(method, url, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
|
||||||
|
fun requestInternal(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
returnType: ReturnType
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
applyDefaultHeaders(headers)
|
||||||
|
return logExceptions {
|
||||||
|
catchHttp {
|
||||||
|
val resp = performCurl(method, url, headers, null)
|
||||||
|
responseToBridge(resp, returnType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun requestWithBody(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useBytes: Boolean = false
|
||||||
|
): IBridgeHttpResponse =
|
||||||
|
requestWithBodyInternal(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
)
|
||||||
|
|
||||||
|
fun requestWithBodyInternal(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
returnType: ReturnType
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
applyDefaultHeaders(headers)
|
||||||
|
return logExceptions {
|
||||||
|
catchHttp {
|
||||||
|
val resp = performCurl(method, url, headers, body.toByteArray())
|
||||||
|
responseToBridge(resp, returnType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun GET(
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useBytes: Boolean = false
|
||||||
|
): IBridgeHttpResponse =
|
||||||
|
GETInternal(url, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
|
||||||
|
fun GETInternal(
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
returnType: ReturnType = ReturnType.STRING
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
applyDefaultHeaders(headers)
|
||||||
|
return logExceptions {
|
||||||
|
catchHttp {
|
||||||
|
val resp = performCurl("GET", url, headers, null)
|
||||||
|
responseToBridge(resp, returnType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun POST(
|
||||||
|
url: String,
|
||||||
|
body: Any,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
useBytes: Boolean = false
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
return when (body) {
|
||||||
|
is V8ValueString ->
|
||||||
|
POSTInternal(url, body.value, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
is String ->
|
||||||
|
POSTInternal(url, body, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
is V8ValueTypedArray ->
|
||||||
|
POSTInternal(url, body.toBytes(), headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
is ByteArray ->
|
||||||
|
POSTInternal(url, body, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
is ArrayList<*> ->
|
||||||
|
POSTInternal(
|
||||||
|
url,
|
||||||
|
body.map { (it as Double).toInt().toByte() }.toByteArray(),
|
||||||
|
headers,
|
||||||
|
if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
)
|
||||||
|
else -> throw NotImplementedError("Body type ${body?.javaClass?.name} not implemented for POST")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun POSTInternal(
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
returnType: ReturnType = ReturnType.STRING
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
applyDefaultHeaders(headers)
|
||||||
|
return logExceptions {
|
||||||
|
catchHttp {
|
||||||
|
val resp = performCurl("POST", url, headers, body.toByteArray())
|
||||||
|
responseToBridge(resp, returnType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun POSTInternal(
|
||||||
|
url: String,
|
||||||
|
body: ByteArray,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
returnType: ReturnType = ReturnType.STRING
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
applyDefaultHeaders(headers)
|
||||||
|
return logExceptions {
|
||||||
|
catchHttp {
|
||||||
|
val resp = performCurl("POST", url, headers, body)
|
||||||
|
responseToBridge(resp, returnType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performCurl(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
bodyBytes: ByteArray?
|
||||||
|
): Libcurl.Response {
|
||||||
|
val jar = ensureCookieJarPath()
|
||||||
|
|
||||||
|
val req = Libcurl.Request(
|
||||||
|
url = url,
|
||||||
|
method = method,
|
||||||
|
headers = headers,
|
||||||
|
body = bodyBytes,
|
||||||
|
impersonateTarget = impersonateTarget,
|
||||||
|
useBuiltInHeaders = useBuiltInHeaders,
|
||||||
|
timeoutMs = timeoutMs,
|
||||||
|
cookieJarPath = jar,
|
||||||
|
sendCookies = sendCookies,
|
||||||
|
persistCookies = persistCookies
|
||||||
|
)
|
||||||
|
return Libcurl.perform(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun responseToBridge(
|
||||||
|
resp: Libcurl.Response,
|
||||||
|
returnType: ReturnType
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
val sanitizedHeaders = sanitizeResponseHeaders(resp.headers, shouldWhitelistHeaders())
|
||||||
|
return when (returnType) {
|
||||||
|
ReturnType.STRING -> {
|
||||||
|
val bodyStr = decodeBody(resp)
|
||||||
|
BridgeHttpStringResponse(resp.effectiveUrl, resp.status, bodyStr, sanitizedHeaders)
|
||||||
|
}
|
||||||
|
ReturnType.BYTES -> {
|
||||||
|
BridgeHttpBytesResponse(resp.effectiveUrl, resp.status, resp.bodyBytes, sanitizedHeaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeBody(resp: Libcurl.Response): String {
|
||||||
|
if (resp.bodyBytes.isEmpty()) return ""
|
||||||
|
|
||||||
|
val contentTypeHeader = resp.headers.entries.firstOrNull {
|
||||||
|
it.key.equals("content-type", ignoreCase = true)
|
||||||
|
}?.value?.firstOrNull()
|
||||||
|
|
||||||
|
val charset: Charset = contentTypeHeader
|
||||||
|
?.let { parseCharset(it) }
|
||||||
|
?: Charsets.UTF_8
|
||||||
|
|
||||||
|
return String(resp.bodyBytes, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseCharset(contentType: String): Charset? {
|
||||||
|
val parts = contentType.split(";")
|
||||||
|
for (part in parts) {
|
||||||
|
val trimmed = part.trim()
|
||||||
|
val lower = trimmed.lowercase()
|
||||||
|
if (lower.startsWith("charset=")) {
|
||||||
|
val value = trimmed.substringAfter("=", "").trim().trim('"', '\'')
|
||||||
|
return try {
|
||||||
|
Charset.forName(value)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldWhitelistHeaders(): Boolean {
|
||||||
|
val cfg = parentConfig
|
||||||
|
return !(cfg is SourcePluginConfig && cfg.allowAllHttpHeaderAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyDefaultHeaders(headerMap: MutableMap<String, String>) {
|
||||||
|
synchronized(_defaultHeaders) {
|
||||||
|
for (toApply in _defaultHeaders) {
|
||||||
|
if (!headerMap.containsKey(toApply.key)) {
|
||||||
|
headerMap[toApply.key] = toApply.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sanitizeResponseHeaders(
|
||||||
|
headers: Map<String, List<String>>?,
|
||||||
|
onlyWhitelisted: Boolean = false
|
||||||
|
): Map<String, List<String>> {
|
||||||
|
val result = mutableMapOf<String, List<String>>()
|
||||||
|
if (onlyWhitelisted) {
|
||||||
|
headers?.forEach { (header, values) ->
|
||||||
|
val lowerCaseHeader = header.lowercase()
|
||||||
|
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
|
||||||
|
result[lowerCaseHeader] = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headers?.forEach { (header, values) ->
|
||||||
|
val lowerCaseHeader = header.lowercase()
|
||||||
|
if (lowerCaseHeader == "set-cookie" &&
|
||||||
|
!values.any { it.lowercase().contains("httponly") }
|
||||||
|
) {
|
||||||
|
result[lowerCaseHeader] = values
|
||||||
|
} else {
|
||||||
|
result[lowerCaseHeader] = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logRequest(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String> = HashMap(),
|
||||||
|
body: String?
|
||||||
|
) {
|
||||||
|
Logger.v(TAG) {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.appendLine("HTTP request (libcurl)")
|
||||||
|
sb.appendLine("$method $url")
|
||||||
|
for (pair in headers) {
|
||||||
|
sb.appendLine("${pair.key}: ${pair.value}")
|
||||||
|
}
|
||||||
|
if (body != null) {
|
||||||
|
sb.appendLine()
|
||||||
|
sb.appendLine(body)
|
||||||
|
}
|
||||||
|
sb.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> logExceptions(handle: () -> T): T {
|
||||||
|
try {
|
||||||
|
return handle()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.e("Plugin[${_package._config.name}]", ex.message, ex)
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun catchHttp(handle: () -> IBridgeHttpResponse): IBridgeHttpResponse {
|
||||||
|
return try {
|
||||||
|
handle()
|
||||||
|
} catch (ex: SocketTimeoutException) {
|
||||||
|
BridgeHttpStringResponse("", 408, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RequestDescriptor(
|
||||||
|
val method: String,
|
||||||
|
val url: String,
|
||||||
|
val headers: MutableMap<String, String>,
|
||||||
|
val body: String? = null,
|
||||||
|
val contentType: String? = null,
|
||||||
|
val respType: ReturnType = ReturnType.STRING
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun catchHttp(handle: () -> BridgeHttpStringResponse): BridgeHttpStringResponse {
|
||||||
|
return try {
|
||||||
|
handle()
|
||||||
|
} catch (ex: SocketTimeoutException) {
|
||||||
|
BridgeHttpStringResponse("", 408, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ReturnType(val value: Int) {
|
||||||
|
STRING(0),
|
||||||
|
BYTES(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PackageHttpImp"
|
||||||
|
private val WHITELISTED_RESPONSE_HEADERS = listOf(
|
||||||
|
"content-type",
|
||||||
|
"date",
|
||||||
|
"content-length",
|
||||||
|
"last-modified",
|
||||||
|
"etag",
|
||||||
|
"cache-control",
|
||||||
|
"content-encoding",
|
||||||
|
"content-disposition",
|
||||||
|
"connection"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-7
@@ -20,7 +20,6 @@ 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.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
|
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.*
|
import com.futo.platformplayer.fragment.mainactivity.main.*
|
||||||
@@ -143,6 +142,10 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
moreOverlay.visibility = VISIBLE
|
moreOverlay.visibility = VISIBLE
|
||||||
val animations = arrayListOf<Animator>()
|
val animations = arrayListOf<Animator>()
|
||||||
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
|
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
|
||||||
|
_bottomButtons.find { it.definition.id == 99 }?.let {
|
||||||
|
animations.add(ObjectAnimator.ofFloat(it, "alpha", 0.4f, 1.0f)
|
||||||
|
.setDuration(duration));
|
||||||
|
}
|
||||||
|
|
||||||
for ((index, button) in _moreButtons.withIndex()) {
|
for ((index, button) in _moreButtons.withIndex()) {
|
||||||
val i = _moreButtons.size - index
|
val i = _moreButtons.size - index
|
||||||
@@ -158,7 +161,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
animatorSet.start()
|
animatorSet.start()
|
||||||
} else {
|
} else {
|
||||||
val animations = arrayListOf<Animator>()
|
val animations = arrayListOf<Animator>()
|
||||||
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 1.0f, 0.0f).setDuration(duration))
|
animations
|
||||||
|
.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 1.0f, 0.0f)
|
||||||
|
.setDuration(duration))
|
||||||
|
_bottomButtons.find { it.definition.id == 99 }?.let {
|
||||||
|
animations.add(ObjectAnimator.ofFloat(it, "alpha", 1.0f, 0.4f)
|
||||||
|
.setDuration(duration));
|
||||||
|
}
|
||||||
|
|
||||||
for ((index, button) in _moreButtons.withIndex()) {
|
for ((index, button) in _moreButtons.withIndex()) {
|
||||||
val i = _moreButtons.size - index
|
val i = _moreButtons.size - index
|
||||||
@@ -260,7 +269,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
for(button in _bottomButtons.toList())
|
for(button in _bottomButtons.toList())
|
||||||
button.updateActive(_fragment);
|
button.updateActive(_fragment);
|
||||||
for(button in _moreButtons.toList())
|
for(button in _moreButtons.toList())
|
||||||
button.updateActive(_fragment);
|
button.updateActive(_fragment, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||||
@@ -354,7 +363,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
this.definition = def;
|
this.definition = def;
|
||||||
|
|
||||||
_buttonImage = findViewById(R.id.image_button);
|
_buttonImage = findViewById(R.id.image_button);
|
||||||
_buttonImage.setImageResource(if (def.isActive(fragment)) def.iconActive else def.icon);
|
//_buttonImage.setImageResource(if (def.isActive(fragment)) def.iconActive else def.icon);
|
||||||
|
_buttonImage.setImageResource(definition.iconActive);
|
||||||
|
if(definition.isActive(fragment) || isMore) {
|
||||||
|
this.alpha = 1f;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.alpha = 0.4f;
|
||||||
|
}
|
||||||
|
|
||||||
_textButton = findViewById(R.id.text_button);
|
_textButton = findViewById(R.id.text_button);
|
||||||
_textButton.text = resources.getString(def.string);
|
_textButton.text = resources.getString(def.string);
|
||||||
@@ -365,8 +381,16 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateActive(fragment: MenuBottomBarFragment) {
|
fun updateActive(fragment: MenuBottomBarFragment, isMore: Boolean = false, overrideValue: Boolean? = null) {
|
||||||
_buttonImage.setImageResource(if (definition.isActive(fragment)) definition.iconActive else definition.icon);
|
//_buttonImage.setImageResource(if (definition.isActive(fragment)) definition.iconActive else definition.icon);
|
||||||
|
_buttonImage.setImageResource(definition.iconActive);
|
||||||
|
val isActive = overrideValue ?: definition.isActive(fragment) || isMore
|
||||||
|
if(isActive) {
|
||||||
|
this.alpha = 1f;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.alpha = 0.4f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,6 +413,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>(withHistory = false) }),
|
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>(withHistory = false) }),
|
||||||
|
ButtonDefinition(12, R.drawable.ic_library, R.drawable.ic_library, R.string.library, canToggle = false, { it.currentMain is LibraryFragment }, { it.navigate<LibraryFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
|
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
|
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
|
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
|
||||||
@@ -399,6 +424,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
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>(withHistory = false) }),
|
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>(withHistory = 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>(withHistory = 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>(withHistory = false) }),
|
||||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
||||||
|
it.navigate<SettingsFragment>();
|
||||||
|
/*
|
||||||
val c = it.context ?: return@ButtonDefinition;
|
val c = it.context ?: return@ButtonDefinition;
|
||||||
Logger.i(TAG, "settings preventPictureInPicture()");
|
Logger.i(TAG, "settings preventPictureInPicture()");
|
||||||
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
||||||
@@ -406,7 +433,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
c.startActivity(intent);
|
c.startActivity(intent);
|
||||||
if (c is Activity) {
|
if (c is Activity) {
|
||||||
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
||||||
}
|
}*/
|
||||||
}),
|
}),
|
||||||
ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = true, { false }, {
|
ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = true, { false }, {
|
||||||
UIDialogs.showDialog(it.context ?: return@ButtonDefinition, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
|
UIDialogs.showDialog(it.context ?: return@ButtonDefinition, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
|
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
|
||||||
_exoPlayer = player;
|
_exoPlayer = player;
|
||||||
|
|
||||||
return PreviewContentListAdapter(fragment.lifecycleScope, context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
return PreviewContentListAdapter(fragment.lifecycleScope, context, feedStyle ?: FeedStyle.THUMBNAIL, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
||||||
attachAdapterEvents(this);
|
attachAdapterEvents(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.SettingsDev
|
||||||
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
|
import com.futo.platformplayer.views.fields.IField
|
||||||
|
|
||||||
|
|
||||||
|
class DeveloperFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: FragView? = null;
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val newView = FragView(this);
|
||||||
|
view = newView;
|
||||||
|
_currentView = view;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
_currentView = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = DeveloperFragment().apply {}
|
||||||
|
|
||||||
|
private var _currentView: FragView? = null;
|
||||||
|
val currentView: FragView?
|
||||||
|
get() = _currentView;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: DeveloperFragment;
|
||||||
|
|
||||||
|
private lateinit var _form: FieldForm;
|
||||||
|
private lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
|
private var _isFinished = false;
|
||||||
|
|
||||||
|
lateinit var overlay: FrameLayout;
|
||||||
|
|
||||||
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
|
||||||
|
constructor(fragment: DeveloperFragment) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.activity_dev, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
|
||||||
|
val activity = fragment.activity;
|
||||||
|
findViewById<LinearLayout>(R.id.container_topbar).isVisible = false;
|
||||||
|
|
||||||
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
_form = findViewById(R.id.settings_form);
|
||||||
|
|
||||||
|
_form.fromObject(SettingsDev.instance);
|
||||||
|
_form.onChanged.subscribe { _, _ ->
|
||||||
|
_form.setObjectValues();
|
||||||
|
SettingsDev.instance.save();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getField(id: String): IField? {
|
||||||
|
return _form.findField(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ import java.time.OffsetDateTime
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
||||||
|
protected val _feedRoot: FrameLayout;
|
||||||
protected val _recyclerResults: RecyclerView;
|
protected val _recyclerResults: RecyclerView;
|
||||||
protected val _overlayContainer: FrameLayout;
|
protected val _overlayContainer: FrameLayout;
|
||||||
protected val _swipeRefresh: SwipeRefreshLayout;
|
protected val _swipeRefresh: SwipeRefreshLayout;
|
||||||
@@ -67,7 +68,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
private var _sortByOptions: List<String>? = null;
|
private var _sortByOptions: List<String>? = null;
|
||||||
private var _activeTags: List<String>? = null;
|
private var _activeTags: List<String>? = null;
|
||||||
|
|
||||||
private var _nextPageHandler: TaskHandler<TPager, List<TResult>>;
|
private var _nextPageHandler: TaskHandler<TPager, Pair<TPager, List<TResult>>>;
|
||||||
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
|
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
|
||||||
|
|
||||||
val fragment: TFragment;
|
val fragment: TFragment;
|
||||||
@@ -80,6 +81,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
this.fragment = fragment;
|
this.fragment = fragment;
|
||||||
inflater.inflate(R.layout.fragment_feed, this);
|
inflater.inflate(R.layout.fragment_feed, this);
|
||||||
|
|
||||||
|
_feedRoot = findViewById(R.id.feed_root);
|
||||||
_textCentered = findViewById(R.id.text_centered);
|
_textCentered = findViewById(R.id.text_centered);
|
||||||
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
|
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
|
||||||
_progressBar = findViewById(R.id.progress_bar);
|
_progressBar = findViewById(R.id.progress_bar);
|
||||||
@@ -135,23 +137,27 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
|
|
||||||
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
||||||
|
|
||||||
_nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, {
|
_nextPageHandler = TaskHandler<TPager, Pair<TPager, List<TResult>>>({fragment.lifecycleScope}, {
|
||||||
if (it is IAsyncPager<*>)
|
if (it is IAsyncPager<*>)
|
||||||
it.nextPageAsync();
|
it.nextPageAsync();
|
||||||
else
|
else
|
||||||
it.nextPage();
|
it.nextPage();
|
||||||
|
|
||||||
processPagerExceptions(it);
|
processPagerExceptions(it);
|
||||||
return@TaskHandler it.getResults();
|
return@TaskHandler Pair(it, it.getResults());
|
||||||
}).success {
|
}).success {
|
||||||
|
val pager = it.first;
|
||||||
|
val results = it.second
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
val posBefore = recyclerData.results.size;
|
val posBefore = recyclerData.results.size;
|
||||||
val filteredResults = filterResults(it);
|
val filteredResults = filterResults(results);
|
||||||
recyclerData.results.addAll(filteredResults);
|
recyclerData.results.addAll(filteredResults);
|
||||||
recyclerData.resultsUnfiltered.addAll(it);
|
recyclerData.resultsUnfiltered.addAll(results);
|
||||||
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
||||||
ensureEnoughContentVisible(filteredResults)
|
if(pager.hasMorePages())
|
||||||
|
ensureEnoughContentVisible(filteredResults)
|
||||||
}.exception<Throwable> {
|
}.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
@@ -390,6 +396,9 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
protected fun finishRefreshLayoutLoader() {
|
protected fun finishRefreshLayoutLoader() {
|
||||||
_swipeRefresh.isRefreshing = false;
|
_swipeRefresh.isRefreshing = false;
|
||||||
}
|
}
|
||||||
|
protected fun disableRefreshLayout() {
|
||||||
|
_swipeRefresh.isEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
fun clearResults(){
|
fun clearResults(){
|
||||||
setPager(EmptyPager<TResult>() as TPager);
|
setPager(EmptyPager<TResult>() as TPager);
|
||||||
|
|||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.toHumanDuration
|
||||||
|
import com.futo.platformplayer.views.AlbumHeaderView
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.TrackViewHolder
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryAlbumFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: FragView? = null;
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
|
||||||
|
val newView = FragView(this, inflater);
|
||||||
|
view = newView;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryAlbumFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView : FeedView<LibraryAlbumFragment, IPlatformVideo, IPlatformVideo, IPager<IPlatformVideo>, TrackViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
private val _header: AlbumHeaderView;
|
||||||
|
|
||||||
|
private var _album: Album? = null;
|
||||||
|
private var _tracks: List<IPlatformVideo>? = null;
|
||||||
|
private var _url: String? = null;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryAlbumFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
_header = AlbumHeaderView(context);
|
||||||
|
_toolbarContentView.addView(_header);
|
||||||
|
|
||||||
|
_header.onPlayAll.subscribe {
|
||||||
|
val playlist = _album?.toPlaylist(_tracks);
|
||||||
|
if (playlist != null) {
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, focus = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_header.onShuffle.subscribe {
|
||||||
|
val playlist = _album?.toPlaylist(_tracks);
|
||||||
|
if (playlist != null) {
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
_feedRoot.updateLayoutParams<LinearLayout.LayoutParams> {
|
||||||
|
this.setMargins(0,-50.dp(resources),0,0)
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown(parameter: Any?) {
|
||||||
|
val album = if(parameter is String)
|
||||||
|
StateLibrary.instance.getAlbum(parameter);
|
||||||
|
else if(parameter is Long)
|
||||||
|
StateLibrary.instance.getAlbum(parameter);
|
||||||
|
else if(parameter is Album)
|
||||||
|
parameter;
|
||||||
|
else null;
|
||||||
|
if(album == null) {
|
||||||
|
_album = null;
|
||||||
|
_tracks = null;
|
||||||
|
setPager(EmptyPager());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_header.setName(album.name);
|
||||||
|
_header.setThumbnail(album.thumbnail);
|
||||||
|
val tracks = album.getTracks();
|
||||||
|
_album = album;
|
||||||
|
_tracks = tracks;
|
||||||
|
_header.setMetadata("${tracks.size} tracks" + if(tracks.size > 0) (" • " + tracks.sumOf { it.duration }.toHumanDuration(false)) else "");
|
||||||
|
setPager(AdhocPager({listOf()}, tracks));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformVideo>): InsertedViewAdapterWithLoader<TrackViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = TrackViewHolder(viewGroup);
|
||||||
|
holder.onClick.subscribe { c ->
|
||||||
|
|
||||||
|
val playlist = _album?.toPlaylist(_tracks);
|
||||||
|
val index = playlist?.videos?.indexOfFirst { it.name == c.name } ?: -1;
|
||||||
|
if (playlist != null) {
|
||||||
|
if (index == -1)
|
||||||
|
return@subscribe;
|
||||||
|
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, index, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
holder.onOptions.subscribe {
|
||||||
|
if(it is IPlatformVideo)
|
||||||
|
UISlideOverlays.showVideoOptionsOverlay(it, _overlayContainer);
|
||||||
|
}
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, 1)
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryArtistsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout.GONE
|
||||||
|
import android.widget.LinearLayout.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.LibraryTypeHeaderView
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
class LibraryAlbumsFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryAlbumsFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView : FeedView<LibraryAlbumsFragment, Album, Album, IPager<Album>, AlbumTileViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
val libraryTypeHeader: LibraryTypeHeaderView;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryAlbumsFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
libraryTypeHeader = LibraryTypeHeaderView(context);
|
||||||
|
libraryTypeHeader.setSelectedType(LibraryTypeHeaderView.SelectedType.Albums);
|
||||||
|
libraryTypeHeader.setMetadata("");
|
||||||
|
|
||||||
|
libraryTypeHeader.onSelectedChanged.subscribe {
|
||||||
|
when(it) {
|
||||||
|
LibraryTypeHeaderView.SelectedType.Artists -> fragment.navigate<LibraryArtistsFragment>();
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_toolbarContentView.addView(libraryTypeHeader);
|
||||||
|
disableRefreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
val initialAlbums = StateLibrary.instance.getAlbums();
|
||||||
|
Logger.i(TAG, "Initial album count: " + initialAlbums.size);
|
||||||
|
|
||||||
|
libraryTypeHeader.setMetadata("${initialAlbums.size} albums");
|
||||||
|
setPager(AdhocPager<Album>({ listOf(); }, initialAlbums));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reload() {
|
||||||
|
super.reload();
|
||||||
|
finishRefreshLayoutLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Album>): InsertedViewAdapterWithLoader<AlbumTileViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = AlbumTileViewHolder(viewGroup);
|
||||||
|
holder.setAutoSize(resources.displayMetrics.widthPixels / resources.displayMetrics.density)
|
||||||
|
holder.onClick.subscribe { c -> fragment.navigate<LibraryAlbumFragment>(c) };
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, AlbumTileViewHolder.getAutoSizeColumns(resources.displayMetrics.widthPixels / resources.displayMetrics.density))
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
leftMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
3f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlbumViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<Album>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_album,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<Album?>();
|
||||||
|
|
||||||
|
protected var _album: Album? = null;
|
||||||
|
protected val _imageThumbnail: ImageView
|
||||||
|
protected val _textName: TextView
|
||||||
|
protected val _textMetadata: TextView
|
||||||
|
|
||||||
|
init {
|
||||||
|
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
|
||||||
|
_textName = _view.findViewById(R.id.text_name);
|
||||||
|
_textMetadata = _view.findViewById(R.id.text_metadata);
|
||||||
|
|
||||||
|
_view.setOnClickListener { onClick.emit(_album) };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun bind(album: Album) {
|
||||||
|
_album = album;
|
||||||
|
_imageThumbnail?.let {
|
||||||
|
if (album.thumbnail != null)
|
||||||
|
Glide.with(it)
|
||||||
|
.load(album.thumbnail)
|
||||||
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(it)
|
||||||
|
else
|
||||||
|
Glide.with(it).load(R.drawable.placeholder_channel_thumbnail).into(it);
|
||||||
|
};
|
||||||
|
|
||||||
|
_textName.text = album.name;
|
||||||
|
_textMetadata.text = album.artist ?: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+617
@@ -0,0 +1,617 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.assume
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumsFragment.AlbumViewHolder
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.SearchType
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.TrackViewHolder
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class LibraryArtistFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var _textMeta: TextView? = null;
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown(parameter, isBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryArtistFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView(fragment: LibraryArtistFragment, inflater: LayoutInflater) : LinearLayout(inflater.context) {
|
||||||
|
private val _fragment: LibraryArtistFragment = fragment
|
||||||
|
|
||||||
|
private var _textChannel: TextView
|
||||||
|
private var _textChannelSub: TextView
|
||||||
|
private var _creatorThumbnail: CreatorThumbnail
|
||||||
|
private var _imageBanner: AppCompatImageView
|
||||||
|
|
||||||
|
private var _tabs: TabLayout
|
||||||
|
private var _viewPager: ViewPager2
|
||||||
|
|
||||||
|
// private var _adapter: ChannelViewPagerAdapter;
|
||||||
|
private var _tabLayoutMediator: TabLayoutMediator
|
||||||
|
private var _buttonSubscribe: SubscribeButton
|
||||||
|
private var _buttonSubscriptionSettings: ImageButton
|
||||||
|
|
||||||
|
private var _overlayContainer: FrameLayout
|
||||||
|
private var _overlayLoading: LinearLayout
|
||||||
|
private var _overlayLoadingSpinner: ImageView
|
||||||
|
|
||||||
|
private var _slideUpOverlay: SlideUpMenuOverlay? = null
|
||||||
|
|
||||||
|
private var _isLoading: Boolean = false
|
||||||
|
private var _selectedTabIndex: Int = -1
|
||||||
|
var channel: Artist? = null
|
||||||
|
private set
|
||||||
|
private var _url: String? = null
|
||||||
|
|
||||||
|
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflater.inflate(R.layout.fragment_artist, this)
|
||||||
|
|
||||||
|
val tabs: TabLayout = findViewById(R.id.tabs)
|
||||||
|
val viewPager: ViewPager2 = findViewById(R.id.view_pager)
|
||||||
|
_textChannel = findViewById(R.id.text_channel_name)
|
||||||
|
_textChannelSub = findViewById(R.id.text_metadata)
|
||||||
|
_creatorThumbnail = findViewById(R.id.creator_thumbnail)
|
||||||
|
_imageBanner = findViewById(R.id.image_channel_banner)
|
||||||
|
_buttonSubscribe = findViewById(R.id.button_subscribe)
|
||||||
|
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
|
||||||
|
_overlayLoading = findViewById(R.id.channel_loading_overlay)
|
||||||
|
_overlayLoadingSpinner = findViewById(R.id.channel_loader_frag)
|
||||||
|
_overlayContainer = findViewById(R.id.overlay_container)
|
||||||
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
|
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
|
||||||
|
_buttonSubscriptionSettings.visibility =
|
||||||
|
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
_buttonSubscribe.onUnSubscribed.subscribe {
|
||||||
|
_buttonSubscriptionSettings.visibility =
|
||||||
|
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
_buttonSubscriptionSettings.setOnClickListener {
|
||||||
|
val url = channel?.contentUrl ?: _url ?: return@setOnClickListener
|
||||||
|
val sub =
|
||||||
|
StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener
|
||||||
|
UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Determine if this is really the only solution (isSaveEnabled=false)
|
||||||
|
viewPager.isSaveEnabled = false
|
||||||
|
viewPager.registerOnPageChangeCallback(_onPageChangeCallback)
|
||||||
|
val adapter = ArtistViewPagerAdapter(fragment, fragment.childFragmentManager, fragment.lifecycle)
|
||||||
|
adapter.onChannelClicked.subscribe { c -> fragment.navigate<ChannelFragment>(c) }
|
||||||
|
adapter.onContentClicked.subscribe { v, _ ->
|
||||||
|
when (v) {
|
||||||
|
is IPlatformVideo -> {
|
||||||
|
StatePlayer.instance.clearQueue()
|
||||||
|
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
is IPlatformPlaylist -> {
|
||||||
|
fragment.navigate<RemotePlaylistFragment>(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
is IPlatformPost -> {
|
||||||
|
fragment.navigate<PostDetailFragment>(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onShortClicked.subscribe { v, _, pagerPair ->
|
||||||
|
when (v) {
|
||||||
|
is IPlatformVideo -> {
|
||||||
|
StatePlayer.instance.clearQueue()
|
||||||
|
fragment.navigate<ShortsFragment>(Triple(v, pagerPair!!.first, pagerPair.second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onAddToClicked.subscribe { content ->
|
||||||
|
_overlayContainer.let {
|
||||||
|
if (content is IPlatformVideo) _slideUpOverlay =
|
||||||
|
UISlideOverlays.showVideoOptionsOverlay(content, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onAddToQueueClicked.subscribe { content ->
|
||||||
|
if (content is IPlatformVideo) {
|
||||||
|
StatePlayer.instance.addToQueue(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
|
if (content is IPlatformVideo) {
|
||||||
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||||
|
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||||
|
else
|
||||||
|
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onUrlClicked.subscribe { url ->
|
||||||
|
fragment.navigate<BrowserFragment>(url)
|
||||||
|
}
|
||||||
|
adapter.onContentUrlClicked.subscribe { url, contentType ->
|
||||||
|
when (contentType) {
|
||||||
|
ContentType.MEDIA -> {
|
||||||
|
StatePlayer.instance.clearQueue()
|
||||||
|
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onLongPress.subscribe { content ->
|
||||||
|
_overlayContainer.let {
|
||||||
|
if (content is IPlatformVideo) _slideUpOverlay =
|
||||||
|
UISlideOverlays.showVideoOptionsOverlay(content, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewPager.adapter = adapter
|
||||||
|
val tabLayoutMediator = TabLayoutMediator(
|
||||||
|
tabs, viewPager, (viewPager.adapter as ArtistViewPagerAdapter)::getTabNames
|
||||||
|
)
|
||||||
|
tabLayoutMediator.attach()
|
||||||
|
|
||||||
|
_tabLayoutMediator = tabLayoutMediator
|
||||||
|
_tabs = tabs
|
||||||
|
_viewPager = viewPager
|
||||||
|
if (_selectedTabIndex != -1) {
|
||||||
|
selectTab(_selectedTabIndex)
|
||||||
|
}
|
||||||
|
setLoading(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectTab(tab: ArtistTab) {
|
||||||
|
(_viewPager.adapter as ArtistViewPagerAdapter).getTabPosition(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanup() {
|
||||||
|
_tabLayoutMediator.detach()
|
||||||
|
_viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback)
|
||||||
|
hideSlideUpOverlay()
|
||||||
|
(_overlayLoadingSpinner.drawable as Animatable?)?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
hideSlideUpOverlay()
|
||||||
|
_selectedTabIndex = -1
|
||||||
|
|
||||||
|
if (!isBack || _url == null) {
|
||||||
|
_imageBanner.setImageDrawable(null)
|
||||||
|
|
||||||
|
when (parameter) {
|
||||||
|
is String -> {
|
||||||
|
_buttonSubscribe.setSubscribeChannel(parameter)
|
||||||
|
_buttonSubscriptionSettings.visibility =
|
||||||
|
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
_url = parameter
|
||||||
|
|
||||||
|
val parsed = Uri.parse(parameter);
|
||||||
|
val idLong = parsed.lastPathSegment?.toLongOrNull();
|
||||||
|
if(idLong != null) {
|
||||||
|
val artist = StateLibrary.instance.getArtist(idLong) ?: return;
|
||||||
|
showArtist(artist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Artist -> {
|
||||||
|
showArtist(parameter)
|
||||||
|
_url = parameter.contentUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectTab(selectedTabIndex: Int) {
|
||||||
|
_selectedTabIndex = selectedTabIndex
|
||||||
|
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
if (_isLoading == isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = isLoading
|
||||||
|
if (isLoading) {
|
||||||
|
_overlayLoading.visibility = View.VISIBLE
|
||||||
|
(_overlayLoadingSpinner.drawable as Animatable?)?.start()
|
||||||
|
} else {
|
||||||
|
(_overlayLoadingSpinner.drawable as Animatable?)?.stop()
|
||||||
|
_overlayLoading.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackPressed(): Boolean {
|
||||||
|
if (_slideUpOverlay != null) {
|
||||||
|
hideSlideUpOverlay()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideSlideUpOverlay() {
|
||||||
|
_slideUpOverlay?.hide(false)
|
||||||
|
_slideUpOverlay = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showArtist(channel: Artist) {
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
_fragment.topBar?.onShown(channel)
|
||||||
|
|
||||||
|
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
|
||||||
|
})
|
||||||
|
|
||||||
|
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.contentUrl ?: return@launch)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
buttons.add(Pair(R.drawable.ic_search) {
|
||||||
|
_fragment.navigate<SuggestionsFragment>(
|
||||||
|
SuggestionsFragmentData(
|
||||||
|
"", SearchType.VIDEO
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonSubscribe.visibility = GONE;
|
||||||
|
_buttonSubscriptionSettings.visibility =
|
||||||
|
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
|
||||||
|
_textChannel.text = channel.name
|
||||||
|
_textChannelSub.text = "${channel.countTracks} songs, ${channel.countAlbums} albums";
|
||||||
|
|
||||||
|
var supportsPlaylists = false;
|
||||||
|
val playlistPosition = 1
|
||||||
|
// keep the current tab selected
|
||||||
|
if (_viewPager.currentItem >= playlistPosition) {
|
||||||
|
_viewPager.setCurrentItem(_viewPager.currentItem + 1, false)
|
||||||
|
}
|
||||||
|
(_viewPager.adapter as ArtistViewPagerAdapter).insert(
|
||||||
|
playlistPosition,
|
||||||
|
ArtistTab.ALBUMS
|
||||||
|
)
|
||||||
|
|
||||||
|
// sets the channel for each tab
|
||||||
|
for (fragment in _fragment.childFragmentManager.fragments) {
|
||||||
|
(fragment as IArtistTabFragment).setArtist(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
(_viewPager.adapter as ArtistViewPagerAdapter).artist = channel
|
||||||
|
|
||||||
|
|
||||||
|
_viewPager.adapter!!.notifyDataSetChanged()
|
||||||
|
|
||||||
|
this.channel = channel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryArtistFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enum class ArtistTab {
|
||||||
|
SONGS, ALBUMS
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistViewPagerAdapter(private val fragment: LibraryArtistFragment, fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||||
|
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||||
|
private val _supportedFragments = mutableMapOf(
|
||||||
|
ArtistTab.SONGS.ordinal to ArtistTab.SONGS
|
||||||
|
)
|
||||||
|
private val _tabs = arrayListOf(ArtistTab.SONGS, ArtistTab.ALBUMS)
|
||||||
|
|
||||||
|
var artist: Artist? = null
|
||||||
|
|
||||||
|
val onContentUrlClicked = Event2<String, ContentType>()
|
||||||
|
val onUrlClicked = Event1<String>()
|
||||||
|
val onContentClicked = Event2<IPlatformContent, Long>()
|
||||||
|
val onShortClicked = Event3<IPlatformContent, Long, Pair<IPager<IPlatformContent>, ArrayList<IPlatformContent>>?>()
|
||||||
|
val onChannelClicked = Event1<PlatformAuthorLink>()
|
||||||
|
val onAddToClicked = Event1<IPlatformContent>()
|
||||||
|
val onAddToQueueClicked = Event1<IPlatformContent>()
|
||||||
|
val onAddToWatchLaterClicked = Event1<IPlatformContent>()
|
||||||
|
val onLongPress = Event1<IPlatformContent>()
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return _tabs[position].ordinal.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun containsItem(itemId: Long): Boolean {
|
||||||
|
return _supportedFragments.containsKey(itemId.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return _supportedFragments.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTabPosition(tab: ArtistTab): Int {
|
||||||
|
return _tabs.indexOf(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTabNames(tab: TabLayout.Tab, position: Int) {
|
||||||
|
tab.text = _tabs[position].name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insert(position: Int, tab: ArtistTab) {
|
||||||
|
_supportedFragments[tab.ordinal] = tab
|
||||||
|
_tabs.add(position, tab)
|
||||||
|
notifyItemInserted(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(position: Int) {
|
||||||
|
_supportedFragments.remove(_tabs[position].ordinal)
|
||||||
|
_tabs.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
val fragment: Fragment
|
||||||
|
when (_tabs[position]) {
|
||||||
|
ArtistTab.SONGS -> {
|
||||||
|
fragment = ChannelContentsFragment(this.fragment).apply {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistTab.ALBUMS -> {
|
||||||
|
fragment = ArtistAlbumsFragment(this.fragment).apply {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
artist?.let { (fragment as IArtistTabFragment).setArtist(it) }
|
||||||
|
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IArtistTabFragment {
|
||||||
|
fun setArtist(artist: Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChannelContentsFragment(private val frag: LibraryArtistFragment) : Fragment(), IArtistTabFragment {
|
||||||
|
|
||||||
|
var view: ArtistContentView? = null;
|
||||||
|
|
||||||
|
private var _lastArtist: Artist? = null;
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
view = ArtistContentView(frag, inflater);
|
||||||
|
_lastArtist?.let {
|
||||||
|
view?.setArtist(it);
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setArtist(artist: Artist) {
|
||||||
|
view?.setArtist(artist);
|
||||||
|
_lastArtist = artist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArtistContentView : FeedView<LibraryArtistFragment, IPlatformContent, IPlatformVideo, IPager<IPlatformContent>, TrackViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
protected var _artist: Artist? = null;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryArtistFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setArtist(artist: Artist) {
|
||||||
|
this._artist = artist;
|
||||||
|
val tracks = artist.getAudioTracks();
|
||||||
|
if(tracks.getResults().isEmpty())
|
||||||
|
UIDialogs.appToast("No tracks found");
|
||||||
|
setPager(tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformVideo> {
|
||||||
|
return results.filter { it is IPlatformVideo }.map { it as IPlatformVideo };
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformVideo>): InsertedViewAdapterWithLoader<TrackViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = TrackViewHolder(viewGroup);
|
||||||
|
holder.onClick.subscribe { c ->
|
||||||
|
|
||||||
|
val playlist = _artist?.toPlaylist();
|
||||||
|
if (playlist != null) {
|
||||||
|
val index = playlist.videos.indexOf(c);
|
||||||
|
if (index == -1)
|
||||||
|
return@subscribe;
|
||||||
|
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, index, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
holder.onOptions.subscribe {
|
||||||
|
if(it is IPlatformVideo)
|
||||||
|
UISlideOverlays.showVideoOptionsOverlay(it, _overlayContainer);
|
||||||
|
}
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, 1)
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArtistAlbumsFragment(private val frag: LibraryArtistFragment) : Fragment(), IArtistTabFragment {
|
||||||
|
|
||||||
|
var view: ArtistAlbumsView? = null;
|
||||||
|
|
||||||
|
private var _lastArtist: Artist? = null;
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
view = ArtistAlbumsView(frag, inflater);
|
||||||
|
_lastArtist?.let {
|
||||||
|
view?.setArtist(it);
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setArtist(artist: Artist) {
|
||||||
|
view?.setArtist(artist);
|
||||||
|
_lastArtist = artist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArtistAlbumsView : FeedView<LibraryArtistFragment, Album, Album, IPager<Album>, AlbumTileViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryArtistFragment, inflater: LayoutInflater) : super(fragment, inflater)
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setArtist(artist: Artist) {
|
||||||
|
val initialAlbums = artist.getAlbums();
|
||||||
|
Logger.i(TAG, "Initial album count: " + initialAlbums.size);
|
||||||
|
|
||||||
|
setPager(AdhocPager({ listOf() }, initialAlbums));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Album>): InsertedViewAdapterWithLoader<AlbumTileViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = AlbumTileViewHolder(viewGroup);
|
||||||
|
holder.setAutoSize(resources.displayMetrics.widthPixels / resources.displayMetrics.density)
|
||||||
|
holder.onClick.subscribe { c -> fragment.navigate<LibraryAlbumFragment>(c) };
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, AlbumTileViewHolder.getAutoSizeColumns(resources.displayMetrics.widthPixels / resources.displayMetrics.density))
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.LinearLayout.GONE
|
||||||
|
import android.widget.LinearLayout.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.states.ArtistOrdering
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.LibraryTypeHeaderView
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
class LibraryArtistsFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var _textMeta: TextView? = null;
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryArtistsFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView : FeedView<LibraryArtistsFragment, Artist, Artist, IPager<Artist>, ArtistViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
val libraryTypeHeader: LibraryTypeHeaderView;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryArtistsFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
libraryTypeHeader = LibraryTypeHeaderView(context);
|
||||||
|
libraryTypeHeader.setSelectedType(LibraryTypeHeaderView.SelectedType.Artists);
|
||||||
|
libraryTypeHeader.setMetadata("");
|
||||||
|
|
||||||
|
libraryTypeHeader.onSelectedChanged.subscribe {
|
||||||
|
when(it) {
|
||||||
|
LibraryTypeHeaderView.SelectedType.Albums -> fragment.navigate<LibraryAlbumsFragment>();
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_toolbarContentView.addView(libraryTypeHeader);
|
||||||
|
disableRefreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun reload() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
val intialArtists = StateLibrary.instance.getArtists(ArtistOrdering.Alphabethic);
|
||||||
|
Logger.i(TAG, "Initial album count: " + intialArtists.size);
|
||||||
|
|
||||||
|
libraryTypeHeader.setMetadata("${intialArtists.size} artists");
|
||||||
|
setPager(AdhocPager<Artist>({ listOf(); }, intialArtists));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Artist>): InsertedViewAdapterWithLoader<ArtistViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = ArtistViewHolder(viewGroup);
|
||||||
|
holder.onClick.subscribe { c ->
|
||||||
|
fragment.navigate<LibraryArtistFragment>(c)
|
||||||
|
};
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, 1)
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryArtistsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<Artist>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_artist,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<Artist>();
|
||||||
|
|
||||||
|
protected var _artist: Artist? = null;
|
||||||
|
//protected val _imageThumbnail: ImageView
|
||||||
|
protected val _textName: TextView
|
||||||
|
protected val _textMetadata: TextView
|
||||||
|
|
||||||
|
init {
|
||||||
|
//_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
|
||||||
|
_textName = _view.findViewById(R.id.text_name);
|
||||||
|
_textMetadata = _view.findViewById(R.id.text_metadata);
|
||||||
|
|
||||||
|
_view.setOnClickListener { _artist?.let { onClick.emit(it) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(artist: Artist) {
|
||||||
|
_artist = artist;
|
||||||
|
/*
|
||||||
|
_imageThumbnail?.let {
|
||||||
|
if (artist.thumbnail != null)
|
||||||
|
Glide.with(it)
|
||||||
|
.load(artist.thumbnail)
|
||||||
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(it)
|
||||||
|
else
|
||||||
|
Glide.with(it).load(R.drawable.placeholder_channel_thumbnail).into(it);
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
_textName.text = artist.name;
|
||||||
|
|
||||||
|
val metaComps = listOf(
|
||||||
|
artist.countTracks?.let { "${it} tracks" },
|
||||||
|
artist.countAlbums?.let { "${it} albums" }
|
||||||
|
).filterNotNull();
|
||||||
|
|
||||||
|
_textMetadata.text = metaComps.joinToString(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
|
||||||
|
class LibraryFilesFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryFilesFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView : FeedView<LibraryFilesFragment, FileEntry, FileEntry, IPager<FileEntry>, FileViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
val navStack = mutableListOf<FileStack>()
|
||||||
|
var buttonUp: BigButton? = null;
|
||||||
|
var buttonAdd: BigButton? = null;
|
||||||
|
|
||||||
|
private var root: FileEntry? = null;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryFilesFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown(parameter: Any? = null) {
|
||||||
|
this.root = if(parameter is FileEntry) parameter else null;
|
||||||
|
loadTop();
|
||||||
|
}
|
||||||
|
fun loadTop() {
|
||||||
|
var initialDirectories = listOf<FileEntry>();
|
||||||
|
if(root == null) {
|
||||||
|
initialDirectories = StateLibrary.instance.getFileDirectories();
|
||||||
|
if (initialDirectories.size == 0) {
|
||||||
|
setEmptyPager(true);
|
||||||
|
setPager(EmptyPager());
|
||||||
|
buttonAdd?.let {
|
||||||
|
it.isVisible = false;
|
||||||
|
}
|
||||||
|
buttonUp?.let {
|
||||||
|
it.isVisible = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else
|
||||||
|
setEmptyPager(false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buttonAdd?.let {
|
||||||
|
it.isVisible = false;
|
||||||
|
}
|
||||||
|
buttonUp?.let {
|
||||||
|
it.isVisible = false;
|
||||||
|
}
|
||||||
|
initialDirectories = root?.getSubFiles() ?: listOf();
|
||||||
|
}
|
||||||
|
navStack.clear();
|
||||||
|
val entry = FileStack("", initialDirectories);
|
||||||
|
navStack.add(entry);
|
||||||
|
openDirectory(navStack.last());
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is FilesTopBarFragment) {
|
||||||
|
it.setUpNavigate(null);
|
||||||
|
it.setTitle(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun leaveDirectory() {
|
||||||
|
if(navStack.size > 1) {
|
||||||
|
navStack.removeLast();
|
||||||
|
openDirectory(navStack.last());
|
||||||
|
}
|
||||||
|
else {}
|
||||||
|
}
|
||||||
|
fun openDirectory(stack: FileStack, addToStack: Boolean = false) {
|
||||||
|
if(addToStack)
|
||||||
|
navStack.add(stack);
|
||||||
|
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is FilesTopBarFragment) {
|
||||||
|
it.setTitle(stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonAdd?.let {
|
||||||
|
it.isVisible = navStack.size < 2
|
||||||
|
}
|
||||||
|
buttonUp?.let {
|
||||||
|
it.isVisible = navStack.size > 1;
|
||||||
|
}
|
||||||
|
setPager(AdhocPager<FileEntry>({ listOf(); }, stack.files));
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is FilesTopBarFragment) {
|
||||||
|
if(navStack.size > 1)
|
||||||
|
it.setUpNavigate{
|
||||||
|
leaveDirectory();
|
||||||
|
};
|
||||||
|
else it.setUpNavigate(null);
|
||||||
|
it.setTitle(stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBack() {
|
||||||
|
fragment.topBar?.view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEmptyPagerView(): View? {
|
||||||
|
return NoResultsView(context, "No Directories Added",
|
||||||
|
"To see files in Grayjay you have to add directories to view",
|
||||||
|
R.drawable.ic_library, listOf(
|
||||||
|
BigButton(context, "Add Directory", "Select a directory to add", R.drawable.ic_add, {
|
||||||
|
StateLibrary.instance.addFileDirectory({
|
||||||
|
loadTop();
|
||||||
|
}, true);
|
||||||
|
})
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<FileEntry>): InsertedViewAdapterWithLoader<FileViewHolder> {
|
||||||
|
/*
|
||||||
|
val buttonUp = BigButton(fragment.requireContext(), "Go up", "Go up a directory", R.drawable.ic_move_up) {
|
||||||
|
if(navStack.size > 1)
|
||||||
|
leaveDirectory();
|
||||||
|
}
|
||||||
|
val buttonAdd = BigButton(fragment.requireContext(), "Add Directory", "Select a directory to add", R.drawable.ic_add) {
|
||||||
|
StateLibrary.instance.addFileDirectory {
|
||||||
|
loadTop();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
//this.buttonUp = buttonUp;
|
||||||
|
//this.buttonAdd = buttonAdd;
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = FileViewHolder(viewGroup);
|
||||||
|
holder.onClick.subscribe { c ->
|
||||||
|
if (c != null) {
|
||||||
|
if(c.isDirectory) {
|
||||||
|
openDirectory(FileStack(c.path, c.getSubFiles()), true);
|
||||||
|
} else {
|
||||||
|
fragment.navigate<VideoDetailFragment>(c.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
holder.onDelete.subscribe { c ->
|
||||||
|
if(c != null) {
|
||||||
|
StateLibrary.instance.deleteFileDirectory(c.path);
|
||||||
|
loadTop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, 1)
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class FileStack(
|
||||||
|
val path: String,
|
||||||
|
val files: List<FileEntry>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
+297
@@ -0,0 +1,297 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.states.ArtistOrdering
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithViews
|
||||||
|
import com.futo.platformplayer.views.LibrarySection
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.ArtistTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.LocalVideoTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: FragView? = null;
|
||||||
|
|
||||||
|
private var allowedMusic = false;
|
||||||
|
private var allowedVideo = false;
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
|
||||||
|
val newView = FragView(this, allowedMusic, allowedVideo);
|
||||||
|
view = newView;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
|
||||||
|
requestPermissionMusic();
|
||||||
|
requestPermissionVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPermissionResultAudio(access: Boolean) {
|
||||||
|
allowedMusic = access;
|
||||||
|
view?.setMusicPermissions(access);
|
||||||
|
StateApp.instance.hasMediaStoreAudioPermission = (access);
|
||||||
|
}
|
||||||
|
fun setPermissionResultVideo(access: Boolean) {
|
||||||
|
allowedVideo = access;
|
||||||
|
view?.setVideoPermissions(access);
|
||||||
|
StateApp.instance.hasMediaStoreVideoPermission = (access);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestPermissionMusic() {
|
||||||
|
when {
|
||||||
|
ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.READ_MEDIA_AUDIO) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
setPermissionResultAudio(true);
|
||||||
|
}
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), android.Manifest.permission.READ_MEDIA_AUDIO) -> {
|
||||||
|
UIDialogs.showDialog(requireContext(), R.drawable.ic_library,
|
||||||
|
"Music permissions", "We require permissions to see your on-device music, denying this will hide the option to see local music.", null, 1,
|
||||||
|
UIDialogs.Action("Ok", {
|
||||||
|
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
|
UIDialogs.Action("Cancel", {
|
||||||
|
|
||||||
|
}, UIDialogs.ActionStyle.NONE));
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun requestPermissionVideo() {
|
||||||
|
when {
|
||||||
|
ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
setPermissionResultVideo(true);
|
||||||
|
}
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), android.Manifest.permission.READ_MEDIA_VIDEO) -> {
|
||||||
|
UIDialogs.showDialog(requireContext(), R.drawable.ic_library, false,
|
||||||
|
"Videos permissions", "We require permissions to see your on-device videos, denying this will hide the option to see local videos.", null, 1,
|
||||||
|
UIDialogs.Action("Ok", {
|
||||||
|
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
|
UIDialogs.Action("Cancel", {
|
||||||
|
|
||||||
|
}, UIDialogs.ActionStyle.NONE));
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val permissionReqAudio = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
||||||
|
setPermissionResultAudio(isGranted);
|
||||||
|
});
|
||||||
|
val permissionReqVideo = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
||||||
|
setPermissionResultVideo(isGranted);
|
||||||
|
});
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: LibraryFragment;
|
||||||
|
|
||||||
|
var sectionArtists: LibrarySection;
|
||||||
|
var sectionAlbums: LibrarySection;
|
||||||
|
var sectionVideos: LibrarySection;
|
||||||
|
var sectionFiles: LibrarySection;
|
||||||
|
//var buttonFiles: BigButton;
|
||||||
|
|
||||||
|
val recycler: RecyclerView;
|
||||||
|
|
||||||
|
val adapterFiles: AnyInsertedAdapterView<FileEntry, FileViewHolder>;
|
||||||
|
|
||||||
|
//var metaInfo: TextView;
|
||||||
|
|
||||||
|
var allowMusic: Boolean = false;
|
||||||
|
var allowVideo: Boolean = false;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryFragment, allowMusic: Boolean?, allowVideo: Boolean?) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.fragview_library, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
recycler = findViewById(R.id.recycler);
|
||||||
|
sectionArtists = LibrarySection(context)//findViewById<LibrarySection>(R.id.section_artists);
|
||||||
|
sectionArtists.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 140.dp(resources)).apply {
|
||||||
|
this.setMargins(0,10.dp(resources), 0, 0);
|
||||||
|
}
|
||||||
|
sectionAlbums = LibrarySection(context)//findViewById(R.id.section_albums);
|
||||||
|
sectionAlbums.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 185.dp(resources)).apply {
|
||||||
|
this.setMargins(0,0, 0, 0);
|
||||||
|
}
|
||||||
|
sectionVideos = LibrarySection(context)//findViewById(R.id.section_videos);
|
||||||
|
sectionVideos.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 170.dp(resources)).apply {
|
||||||
|
this.setMargins(0,0, 0, 0);
|
||||||
|
}
|
||||||
|
sectionFiles = LibrarySection(context)//findViewById(R.id.section_videos);
|
||||||
|
sectionFiles.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 40.dp(resources)).apply {
|
||||||
|
this.setMargins(0,0, 0, 0);
|
||||||
|
}
|
||||||
|
sectionFiles.setSection("Directories") {
|
||||||
|
StateLibrary.instance.addFileDirectory({
|
||||||
|
reloadFiles();
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
sectionFiles.setNavIcon(R.drawable.ic_add);
|
||||||
|
//buttonFiles = findViewById<BigButton>(R.id.button_files);
|
||||||
|
//metaInfo = findViewById(R.id.meta_info);
|
||||||
|
|
||||||
|
this.allowMusic = allowMusic ?: false;
|
||||||
|
this.allowVideo = allowVideo ?: false;
|
||||||
|
|
||||||
|
sectionArtists.setSection("Artists", {
|
||||||
|
if(this.allowMusic)
|
||||||
|
fragment.navigate<LibraryArtistsFragment>();
|
||||||
|
else
|
||||||
|
fragment.requestPermissionMusic();
|
||||||
|
});
|
||||||
|
val adapterArtists = sectionArtists.getAnyAdapter<Artist, ArtistTileViewHolder>({
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryArtistFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount);
|
||||||
|
adapterArtists.setData(artists);
|
||||||
|
|
||||||
|
sectionAlbums.setSection("Albums", {
|
||||||
|
if(this.allowMusic)
|
||||||
|
fragment.navigate<LibraryAlbumsFragment>();
|
||||||
|
else
|
||||||
|
fragment.requestPermissionMusic();
|
||||||
|
});
|
||||||
|
val adapterAlbums = sectionAlbums.getAnyAdapter<Album, AlbumTileViewHolder>({
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryAlbumFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
val albums = StateLibrary.instance.getAlbums();
|
||||||
|
adapterAlbums.setData(albums);
|
||||||
|
|
||||||
|
|
||||||
|
sectionVideos.setSection("Videos", {
|
||||||
|
if(this.allowVideo)
|
||||||
|
fragment.navigate<LibraryVideosFragment>();
|
||||||
|
else
|
||||||
|
fragment.requestPermissionVideo();
|
||||||
|
});
|
||||||
|
val adapterVideos = sectionVideos.getAnyAdapter<IPlatformVideo, LocalVideoTileViewHolder>({
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<VideoDetailFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
val videos = StateLibrary.instance.getRecentVideos(null, 20);
|
||||||
|
adapterVideos.setData(videos);
|
||||||
|
|
||||||
|
adapterFiles = recycler.asAnyWithViews<FileEntry, FileViewHolder>(
|
||||||
|
arrayListOf(
|
||||||
|
sectionArtists,
|
||||||
|
sectionAlbums,
|
||||||
|
sectionVideos,
|
||||||
|
sectionFiles
|
||||||
|
),
|
||||||
|
arrayListOf(View(context).apply { this.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 20.dp(resources)) }),
|
||||||
|
RecyclerView.VERTICAL, false, {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryFilesFragment>(it);
|
||||||
|
}
|
||||||
|
it.onDelete.subscribe {
|
||||||
|
if(it != null) {
|
||||||
|
StateLibrary.instance.deleteFileDirectory(it.path);
|
||||||
|
reloadFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
reloadFiles();
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
buttonFiles.onClick.subscribe {
|
||||||
|
fragment.navigate<LibraryFilesFragment>()
|
||||||
|
} */
|
||||||
|
//buttonFiles.setButtonEnabled(false);
|
||||||
|
setMusicPermissions(allowMusic ?: false);
|
||||||
|
setVideoPermissions(allowVideo ?: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadFiles() {
|
||||||
|
val files = StateLibrary.instance.getFileDirectories();
|
||||||
|
adapterFiles.setData(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun setMusicPermissions(access: Boolean) {
|
||||||
|
allowMusic = access;
|
||||||
|
sectionAlbums.setContentEmptyMessage(R.drawable.ic_library, "No mediastore permissions");
|
||||||
|
sectionArtists.setContentEmptyMessage(R.drawable.ic_library, "No mediastore permissions");
|
||||||
|
//buttonArtists.setButtonEnabled(access);
|
||||||
|
//metaInfo.text = listOf(
|
||||||
|
// if(!allowMusic) "You did not give access to local music, so these options are disabled" else null,
|
||||||
|
// if(!allowVideo) "You did not give access to local videos, so these options are disabled" else null
|
||||||
|
//).filterNotNull().joinToString("\n");
|
||||||
|
}
|
||||||
|
fun setVideoPermissions(access: Boolean) {
|
||||||
|
allowVideo = access;
|
||||||
|
sectionVideos.setContentEmptyMessage(R.drawable.ic_library, "No video permissions");
|
||||||
|
//metaInfo.text = listOf(
|
||||||
|
// if(!allowMusic) "You did not give access to local music, so these options are disabled" else null,
|
||||||
|
// if(!allowVideo) "You did not give access to local videos, so these options are disabled" else null
|
||||||
|
//).filterNotNull().joinToString("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.LinearLayout.GONE
|
||||||
|
import android.widget.LinearLayout.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.states.ArtistOrdering
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithViews
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.LibrarySection
|
||||||
|
import com.futo.platformplayer.views.LibraryTypeHeaderView
|
||||||
|
import com.futo.platformplayer.views.LibraryTypeHeaderView.SelectedType
|
||||||
|
import com.futo.platformplayer.views.PillV2
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.ArtistTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.LocalVideoTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.TrackViewHolder
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
class LibrarySearchFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibrarySearchFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: LibrarySearchFragment;
|
||||||
|
|
||||||
|
val pillArtist: PillV2;
|
||||||
|
val pillAlbums: PillV2;
|
||||||
|
val pillSongs: PillV2;
|
||||||
|
val pills: List<PillV2>;
|
||||||
|
|
||||||
|
val textMetadata: TextView;
|
||||||
|
|
||||||
|
val recycler: RecyclerView;
|
||||||
|
|
||||||
|
val adapterArtists: AnyAdapterView<Artist, LibraryArtistsFragment.ArtistViewHolder>;
|
||||||
|
val adapterSongs: AnyAdapterView<IPlatformContent, TrackViewHolder>;
|
||||||
|
val adapterAlbums: AnyAdapterView<Album, LibraryAlbumsFragment.AlbumViewHolder>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor(fragment: LibrarySearchFragment) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.fragview_library_search, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
|
||||||
|
pillArtist = findViewById(R.id.pill_artist);
|
||||||
|
pillAlbums = findViewById(R.id.pill_albums);
|
||||||
|
pillSongs = findViewById(R.id.pill_songs);
|
||||||
|
pills = listOf(pillArtist, pillAlbums, pillSongs);
|
||||||
|
|
||||||
|
textMetadata = findViewById(R.id.text_metadata);
|
||||||
|
|
||||||
|
pillArtist.onClick.subscribe {
|
||||||
|
pills.forEach { it.setIsEnabled(false) };
|
||||||
|
pillArtist.setIsEnabled(true);
|
||||||
|
loadArtists();
|
||||||
|
}
|
||||||
|
pillAlbums.onClick.subscribe {
|
||||||
|
pills.forEach { it.setIsEnabled(false) };
|
||||||
|
pillAlbums.setIsEnabled(true);
|
||||||
|
loadAlbums();
|
||||||
|
}
|
||||||
|
pillSongs.onClick.subscribe {
|
||||||
|
pills.forEach { it.setIsEnabled(false) };
|
||||||
|
pillSongs.setIsEnabled(true);
|
||||||
|
loadSongs();
|
||||||
|
}
|
||||||
|
|
||||||
|
recycler = findViewById(R.id.recycler);
|
||||||
|
adapterArtists = recycler.asAny<Artist, LibraryArtistsFragment.ArtistViewHolder>(RecyclerView.VERTICAL, false, {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryArtistFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
adapterAlbums = recycler.asAny<Album, LibraryAlbumsFragment.AlbumViewHolder>(RecyclerView.VERTICAL, false, {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryAlbumFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
adapterSongs = recycler.asAny<IPlatformContent, TrackViewHolder>(RecyclerView.VERTICAL, false, {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null && it is IPlatformVideo)
|
||||||
|
fragment.navigate<VideoDetailFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment) {
|
||||||
|
it.onSearch.subscribe {
|
||||||
|
search(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pillArtist.setIsEnabled(true);
|
||||||
|
loadArtists();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadArtists(){
|
||||||
|
recycler.adapter = adapterArtists.adapter.adapter;
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment)
|
||||||
|
search(it.getSearchText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun loadAlbums() {
|
||||||
|
recycler.adapter = adapterAlbums.adapter.adapter;
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment)
|
||||||
|
search(it.getSearchText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun loadSongs() {
|
||||||
|
recycler.adapter = adapterSongs.adapter.adapter;
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment)
|
||||||
|
search(it.getSearchText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(str: String) {
|
||||||
|
if(recycler.adapter == adapterArtists.adapter.adapter) {
|
||||||
|
val data = if(!str.isNullOrBlank())
|
||||||
|
StateLibrary.instance.searchArtists(str)
|
||||||
|
else listOf();
|
||||||
|
adapterArtists.setData(data);
|
||||||
|
textMetadata.text = "${data.size} artists";
|
||||||
|
}
|
||||||
|
else if(recycler.adapter == adapterAlbums.adapter.adapter) {
|
||||||
|
val data = if(!str.isNullOrBlank())
|
||||||
|
StateLibrary.instance.searchAlbums(str)
|
||||||
|
else listOf();
|
||||||
|
adapterAlbums.setData(data);
|
||||||
|
textMetadata.text = "${data.size} albums";
|
||||||
|
}
|
||||||
|
else if(recycler.adapter == adapterSongs.adapter.adapter) {
|
||||||
|
val data = if(!str.isNullOrBlank())
|
||||||
|
StateLibrary.instance.searchTracks(str)
|
||||||
|
else listOf();
|
||||||
|
|
||||||
|
adapterSongs.setData(data);
|
||||||
|
textMetadata.text = "${data.size} songs";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment)
|
||||||
|
it.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout.GONE
|
||||||
|
import android.widget.LinearLayout.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.allViews
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.UISlideOverlays.Companion.showOrderOverlay
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.ToggleBar
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
class LibraryVideosFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var _toggleBuckets = StateLibrary.instance.getVideoBucketNames().map { it.name }.toMutableList();
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryVideosFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView : ContentFeedView<LibraryVideosFragment> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
private var _toggleBar: ToggleBar? = null;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryVideosFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
initializeToolbarContent();
|
||||||
|
disableRefreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
val initialAlbums = StateLibrary.instance.getAlbums();
|
||||||
|
Logger.i(TAG, "Initial album count: " + initialAlbums.size);
|
||||||
|
val buckets = StateLibrary.instance.getVideoBucketNames();
|
||||||
|
setPager(StateLibrary.instance.getVideos(fragment._toggleBuckets));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val _filterLock = Object();
|
||||||
|
fun initializeToolbarContent() {
|
||||||
|
if(_toolbarContentView.allViews.any { it is ToggleBar })
|
||||||
|
_toolbarContentView.removeView(_toolbarContentView.allViews.find { it is ToggleBar });
|
||||||
|
_toggleBar = ToggleBar(context).apply {
|
||||||
|
layoutParams =
|
||||||
|
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(_filterLock) {
|
||||||
|
var buttonsPlugins: List<ToggleBar.Toggle> = listOf()
|
||||||
|
buttonsPlugins =
|
||||||
|
(StateLibrary.instance.getVideoBucketNames()
|
||||||
|
.map { bucket ->
|
||||||
|
ToggleBar.Toggle(bucket.name, null, fragment._toggleBuckets.contains(bucket.name), { view, active ->
|
||||||
|
var dontSwap = false;
|
||||||
|
if (!active) {
|
||||||
|
if (fragment._toggleBuckets.contains(bucket.name))
|
||||||
|
fragment._toggleBuckets.remove(bucket.name);
|
||||||
|
} else {
|
||||||
|
if (!fragment._toggleBuckets.contains(bucket.name)) {
|
||||||
|
val enabledClients = StatePlatform.instance.getEnabledClients();
|
||||||
|
val availableAfterDisable = enabledClients.count { !fragment._toggleBuckets.contains(it.id) && it.id != bucket.name };
|
||||||
|
if(availableAfterDisable > 0)
|
||||||
|
fragment._toggleBuckets.add(bucket.name);
|
||||||
|
else {
|
||||||
|
UIDialogs.appToast("Select atleast 1 bucket");
|
||||||
|
dontSwap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!dontSwap)
|
||||||
|
reloadForFilters();
|
||||||
|
else {
|
||||||
|
view.setToggle(active);
|
||||||
|
}
|
||||||
|
}, { view, views, enabled ->
|
||||||
|
val toDisable = views.filter { it != view && it.tag == "plugins" };
|
||||||
|
if(!view.isActive)
|
||||||
|
view.handleClick();
|
||||||
|
for(tag in toDisable) {
|
||||||
|
if(tag.isActive)
|
||||||
|
tag.handleClick();
|
||||||
|
}
|
||||||
|
}).withTag("plugins")
|
||||||
|
})
|
||||||
|
val buttons = (buttonsPlugins)
|
||||||
|
.sortedBy { it.name }.toTypedArray()
|
||||||
|
|
||||||
|
_toggleBar?.setToggles(*buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
_toolbarContentView.addView(_toggleBar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadForFilters() {
|
||||||
|
setPager(StateLibrary.instance.getVideos(fragment._toggleBuckets));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
|
||||||
|
class RecyclerFragment : MainFragment(){
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: View? = null;
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
|
||||||
|
val newView = RecyclerFragment.View(inflater.context);
|
||||||
|
view = newView;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = RecyclerFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class View: ConstraintLayout {
|
||||||
|
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
||||||
|
inflate(context, R.layout.fragview_filter_recycler, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.SettingsDev
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.assume
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
|
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
|
||||||
|
val newView = FragView(this);
|
||||||
|
view = newView;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
_currentView = view;
|
||||||
|
view?.onShown(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHide() {
|
||||||
|
super.onHide();
|
||||||
|
onClosed.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
_currentView = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = SettingsFragment().apply {}
|
||||||
|
|
||||||
|
val onClosed = Event0();
|
||||||
|
|
||||||
|
private var _currentView: FragView? = null;
|
||||||
|
val currentView: FragView?
|
||||||
|
get() = _currentView;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: SettingsFragment;
|
||||||
|
|
||||||
|
private val _form: FieldForm;
|
||||||
|
private val _buttonBack: ImageButton;
|
||||||
|
private val _loaderView: LoaderView;
|
||||||
|
|
||||||
|
private val _devSets: LinearLayout;
|
||||||
|
private val _buttonDev: MaterialButton;
|
||||||
|
|
||||||
|
private var _isFinished = false;
|
||||||
|
|
||||||
|
lateinit var overlay: FrameLayout;
|
||||||
|
|
||||||
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
|
||||||
|
constructor(fragment: SettingsFragment) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.activity_settings, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
|
||||||
|
val activity = fragment.activity;
|
||||||
|
|
||||||
|
findViewById<LinearLayout>(R.id.container_topbar).isVisible = false;
|
||||||
|
_form = findViewById(R.id.settings_form);
|
||||||
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
_buttonDev = findViewById(R.id.button_dev);
|
||||||
|
_devSets = findViewById(R.id.dev_settings);
|
||||||
|
_loaderView = findViewById(R.id.loader);
|
||||||
|
overlay = findViewById(R.id.overlay_container);
|
||||||
|
|
||||||
|
_form.onChanged.subscribe { field, _ ->
|
||||||
|
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||||
|
_form.setObjectValues();
|
||||||
|
Settings.instance.save();
|
||||||
|
|
||||||
|
if(field.descriptor?.id == "app_language") {
|
||||||
|
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
||||||
|
StateApp.instance.setLocaleSetting(context, Settings.instance.language.getAppLanguageLocaleString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(field.descriptor?.id == "background_update" && activity is MainActivity) {
|
||||||
|
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
|
||||||
|
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
|
||||||
|
val notifManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
|
if(!notifManager.areNotificationsEnabled()) {
|
||||||
|
UIDialogs.toast(context, "Notifications aren't enabled");
|
||||||
|
activity.requestNotificationPermissions("Notifications need to be enabled for background updating to function")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_buttonBack.setOnClickListener {
|
||||||
|
//finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonDev.setOnClickListener {
|
||||||
|
//startActivity(Intent(this, DeveloperActivity::class.java));
|
||||||
|
fragment.navigate<DeveloperFragment>(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//_lastActivity = this;
|
||||||
|
|
||||||
|
reloadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFirstLoad = true;
|
||||||
|
fun reloadSettings() {
|
||||||
|
val firstLoad = isFirstLoad;
|
||||||
|
isFirstLoad = false;
|
||||||
|
_form.setSearchVisible(false);
|
||||||
|
_loaderView.start();
|
||||||
|
_form.fromObject(fragment.lifecycleScope, Settings.instance) {
|
||||||
|
_loaderView.stop();
|
||||||
|
_form.setSearchVisible(true);
|
||||||
|
|
||||||
|
var devCounter = 0;
|
||||||
|
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
||||||
|
devCounter++;
|
||||||
|
if(devCounter > 5) {
|
||||||
|
devCounter = 0;
|
||||||
|
SettingsDev.instance.developerMode = true;
|
||||||
|
SettingsDev.instance.save();
|
||||||
|
updateDevMode();
|
||||||
|
UIDialogs.toast(context, fragment.getString(R.string.you_are_now_in_developer_mode));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
if(firstLoad) {
|
||||||
|
val query = intent.getStringExtra("query");
|
||||||
|
if(!query.isNullOrEmpty()) {
|
||||||
|
_form.setSearchQuery(query);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun onShown(str: Any? = null) {
|
||||||
|
updateDevMode();
|
||||||
|
if(str is String)
|
||||||
|
_form.setSearchQuery(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDevMode() {
|
||||||
|
if(SettingsDev.instance.developerMode)
|
||||||
|
_devSets.visibility = View.VISIBLE;
|
||||||
|
else
|
||||||
|
_devSets.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-1
@@ -1,5 +1,8 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -32,9 +35,11 @@ import com.futo.platformplayer.views.buttons.BigButton
|
|||||||
import com.futo.platformplayer.views.buttons.BigButtonGroup
|
import com.futo.platformplayer.views.buttons.BigButtonGroup
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.sources.SourceHeaderView
|
import com.futo.platformplayer.views.sources.SourceHeaderView
|
||||||
|
import com.google.gson.Gson
|
||||||
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 kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class SourceDetailFragment : MainFragment() {
|
class SourceDetailFragment : MainFragment() {
|
||||||
override val isMainView : Boolean = true;
|
override val isMainView : Boolean = true;
|
||||||
@@ -415,11 +420,39 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val advancedButtons = BigButtonGroup(c, "Advanced",
|
val advancedButtons = BigButtonGroup(c, "Advanced",
|
||||||
|
BigButton(c, "Reset Settings", "Resets the settings to their default (deleting existing settings)", R.drawable.ic_refresh) {
|
||||||
|
_config?.let {
|
||||||
|
StatePlugins.instance.setPluginSettings(it.id, hashMapOf());
|
||||||
|
loadConfig(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BigButton(c, "Share Settings", "Shares the settings of this plugin as json, mostly used for bug reporting", R.drawable.ic_code) {
|
||||||
|
|
||||||
|
val structure = Json { this.prettyPrint = true; this.prettyPrintIndent = " " }
|
||||||
|
.encodeToString(_settings);
|
||||||
|
fragment.startActivity(Intent.createChooser(Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND;
|
||||||
|
putExtra(Intent.EXTRA_TEXT, structure);
|
||||||
|
type = "text/plain";
|
||||||
|
}, null));
|
||||||
|
/*
|
||||||
|
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("Settings Json", structure)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
UIDialogs.toast(context, "Copied", false);
|
||||||
|
*/
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
} ,
|
||||||
|
/*
|
||||||
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
||||||
|
|
||||||
}.apply {
|
}.apply {
|
||||||
this.alpha = 0.5f;
|
this.alpha = 0.5f;
|
||||||
},
|
},*/
|
||||||
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
||||||
val embeddedConfig = StatePlugins.instance.getEmbeddedPluginConfigFromID(context, config.id);
|
val embeddedConfig = StatePlugins.instance.getEmbeddedPluginConfigFromID(context, config.id);
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -24,7 +24,6 @@ import androidx.media3.common.util.UnstableApi
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
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.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
@@ -401,9 +400,10 @@ class VideoDetailFragment() : MainFragment() {
|
|||||||
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
||||||
maximizeVideoDetail();
|
maximizeVideoDetail();
|
||||||
|
|
||||||
|
/*
|
||||||
SettingsActivity.settingsActivityClosed.subscribe(this) {
|
SettingsActivity.settingsActivityClosed.subscribe(this) {
|
||||||
updateOrientation()
|
updateOrientation()
|
||||||
}
|
} */
|
||||||
|
|
||||||
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
||||||
updateOrientation()
|
updateOrientation()
|
||||||
@@ -547,7 +547,7 @@ class VideoDetailFragment() : MainFragment() {
|
|||||||
super.onDestroyMainView();
|
super.onDestroyMainView();
|
||||||
Logger.v(TAG, "onDestroyMainView");
|
Logger.v(TAG, "onDestroyMainView");
|
||||||
|
|
||||||
SettingsActivity.settingsActivityClosed.remove(this)
|
//SettingsActivity.settingsActivityClosed.remove(this)
|
||||||
StatePlayer.instance.onRotationLockChanged.remove(this)
|
StatePlayer.instance.onRotationLockChanged.remove(this)
|
||||||
|
|
||||||
_landscapeOrientationListener?.disableListener()
|
_landscapeOrientationListener?.disableListener()
|
||||||
|
|||||||
+24
-10
@@ -20,6 +20,7 @@ import com.futo.platformplayer.UISlideOverlays
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.assume
|
import com.futo.platformplayer.assume
|
||||||
import com.futo.platformplayer.downloads.VideoDownload
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
|
import com.futo.platformplayer.images.GlideHelper
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
@@ -194,22 +195,35 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
_textMetadata.text = parts.joinToString(" • ");
|
_textMetadata.text = parts.joinToString(" • ");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean) {
|
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean, thumbnail: String? = null) {
|
||||||
if (videos != null && videos.isNotEmpty()) {
|
if(thumbnail != null) {
|
||||||
val video = videos.first();
|
|
||||||
_imagePlaylistThumbnail.let {
|
_imagePlaylistThumbnail.let {
|
||||||
Glide.with(it)
|
Glide.with(it)
|
||||||
.load(video.thumbnails.getHQThumbnail())
|
.load(thumbnail)
|
||||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(it);
|
.into(it);
|
||||||
};
|
}
|
||||||
} else {
|
|
||||||
_textMetadata.text = "0 " + context.getString(R.string.videos);
|
|
||||||
Glide.with(_imagePlaylistThumbnail)
|
|
||||||
.load(R.drawable.placeholder_video_thumbnail)
|
|
||||||
.into(_imagePlaylistThumbnail)
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if (videos != null && videos.isNotEmpty()) {
|
||||||
|
val video = videos.first();
|
||||||
|
_imagePlaylistThumbnail.let {
|
||||||
|
Glide.with(it)
|
||||||
|
.load(video.thumbnails.getHQThumbnail())
|
||||||
|
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||||
|
.crossfade()
|
||||||
|
.into(it);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
Glide.with(_imagePlaylistThumbnail)
|
||||||
|
.load(R.drawable.placeholder_video_thumbnail)
|
||||||
|
.into(_imagePlaylistThumbnail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(videos == null || videos.isEmpty())
|
||||||
|
_textMetadata.text = "0 " + context.getString(R.string.videos);
|
||||||
|
|
||||||
_loadedVideos = videos;
|
_loadedVideos = videos;
|
||||||
_loadedVideosCanEdit = canEdit;
|
_loadedVideosCanEdit = canEdit;
|
||||||
_videoListEditorView.setVideos(videos, canEdit);
|
_videoListEditorView.setVideos(videos, canEdit);
|
||||||
|
|||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.topbar
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFilesFragment
|
||||||
|
import com.futo.platformplayer.models.Playlist
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.views.casting.CastButton
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
|
class FilesTopBarFragment : TopFragment() {
|
||||||
|
private var _buttonBack: ImageButton? = null;
|
||||||
|
private var _buttonCast: CastButton? = null;
|
||||||
|
private var _textTitle: TextView? = null;
|
||||||
|
private var _menuItems: LinearLayout? = null;
|
||||||
|
|
||||||
|
private var _upHandle: (()->Unit)? = null;
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?) {
|
||||||
|
setTitle(parameter);
|
||||||
|
setMenuItems(listOf());
|
||||||
|
}
|
||||||
|
override fun onHide() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTitle(parameter: Any? = null) {
|
||||||
|
if(parameter is IPlatformChannel) {
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
} else if(parameter is PlatformAuthorLink) {
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
} else if (parameter is Playlist) {
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
} else if (parameter is String) {
|
||||||
|
_textTitle?.text = parameter;
|
||||||
|
} else if (parameter is IPlatformClient) {
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
} else if (parameter is PolycentricProfile) {
|
||||||
|
_textTitle?.text = parameter.systemState.username;
|
||||||
|
} else if(parameter is FileEntry) {
|
||||||
|
val treePrefix = "content://com.android.externalstorage.documents/tree/";
|
||||||
|
if(parameter.path.startsWith(treePrefix)) {
|
||||||
|
_textTitle?.text = parameter.path.substring(treePrefix.length - 1).replace("%3A", " ").replace("%2F", "/");
|
||||||
|
}
|
||||||
|
else if(parameter.path.isNullOrBlank())
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
else
|
||||||
|
_textTitle?.text = parameter.path;
|
||||||
|
}
|
||||||
|
else if(parameter is LibraryFilesFragment.FileStack) {
|
||||||
|
val treePrefix = "content://com.android.externalstorage.documents/tree/";
|
||||||
|
if(parameter.path.startsWith(treePrefix)) {
|
||||||
|
_textTitle?.text = parameter.path.substring(treePrefix.length - 1).replace("%3A", " ").replace("%2F", "/");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_textTitle?.text = parameter.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_files_top_bar, container, false);
|
||||||
|
|
||||||
|
val buttonBack: ImageButton = view.findViewById(R.id.button_back);
|
||||||
|
_textTitle = view.findViewById(R.id.text_title);
|
||||||
|
_menuItems = view.findViewById(R.id.menu_buttons)
|
||||||
|
|
||||||
|
buttonBack.setOnClickListener {
|
||||||
|
if(_upHandle != null)
|
||||||
|
_upHandle?.invoke();
|
||||||
|
else
|
||||||
|
closeSegment();
|
||||||
|
};
|
||||||
|
|
||||||
|
_buttonBack = buttonBack;
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUpNavigate(handle: (()->Unit)? = null) {
|
||||||
|
_upHandle = handle;
|
||||||
|
_buttonBack?.setImageResource(if(handle == null) R.drawable.ic_back_nav else R.drawable.ic_arrow_up);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
|
||||||
|
_buttonBack?.setOnClickListener(null);
|
||||||
|
_buttonBack = null;
|
||||||
|
_buttonCast?.cleanup();
|
||||||
|
_buttonCast = null;
|
||||||
|
_textTitle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMenuItems(items: List<Pair<Int, ()->Unit>>) {
|
||||||
|
_menuItems?.removeAllViews();
|
||||||
|
|
||||||
|
val dp4 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4f, resources.displayMetrics).toInt();
|
||||||
|
val dp9 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 9f, resources.displayMetrics).toInt();
|
||||||
|
|
||||||
|
for(item in items) {
|
||||||
|
val compatImageItem = AppCompatImageView(requireContext());
|
||||||
|
compatImageItem.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||||
|
compatImageItem.setImageResource(item.first);
|
||||||
|
compatImageItem.setPadding(dp4, dp9, dp4, dp9);
|
||||||
|
compatImageItem.scaleType = ImageView.ScaleType.FIT_CENTER;
|
||||||
|
compatImageItem.setOnClickListener {
|
||||||
|
item.second.invoke();
|
||||||
|
};
|
||||||
|
|
||||||
|
_menuItems?.addView(compatImageItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = FilesTopBarFragment().apply { }
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -9,6 +9,8 @@ import android.widget.ImageView
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibrarySearchFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
||||||
@@ -46,6 +48,8 @@ class GeneralTopBarFragment : TopFragment() {
|
|||||||
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.CREATOR));
|
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.CREATOR));
|
||||||
} else if (currentMain is PlaylistsFragment || currentMain is PlaylistFragment) {
|
} else if (currentMain is PlaylistsFragment || currentMain is PlaylistFragment) {
|
||||||
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.PLAYLIST));
|
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.PLAYLIST));
|
||||||
|
} else if (currentMain is LibraryFragment) {
|
||||||
|
navigate<LibrarySearchFragment>();
|
||||||
} else {
|
} else {
|
||||||
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO));
|
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO));
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -18,6 +18,7 @@ import com.futo.platformplayer.Settings
|
|||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibrarySearchFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -112,7 +113,10 @@ class SearchTopBarFragment : TopFragment() {
|
|||||||
}
|
}
|
||||||
fun clear() {
|
fun clear() {
|
||||||
_editSearch?.text?.clear();
|
_editSearch?.text?.clear();
|
||||||
if (currentMain !is SuggestionsFragment) {
|
if(currentMain is LibrarySearchFragment) {
|
||||||
|
onSearch.emit("");
|
||||||
|
}
|
||||||
|
else if (currentMain !is SuggestionsFragment) {
|
||||||
navigate<SuggestionsFragment>(SuggestionsFragmentData("", _searchType), false);
|
navigate<SuggestionsFragment>(SuggestionsFragmentData("", _searchType), false);
|
||||||
} else {
|
} else {
|
||||||
onSearch.emit("");
|
onSearch.emit("");
|
||||||
@@ -190,6 +194,12 @@ class SearchTopBarFragment : TopFragment() {
|
|||||||
_buttonFilter?.visibility = if (visible) View.VISIBLE else View.GONE;
|
_buttonFilter?.visibility = if (visible) View.VISIBLE else View.GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSearchText(): String {
|
||||||
|
return _editSearch?.let {
|
||||||
|
it.text.toString();
|
||||||
|
} ?: "";
|
||||||
|
}
|
||||||
|
|
||||||
private fun onDone() {
|
private fun onDone() {
|
||||||
val editSearch = _editSearch
|
val editSearch = _editSearch
|
||||||
if (editSearch != null) {
|
if (editSearch != null) {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class GlideHelper {
|
|||||||
req.into(this);
|
req.into(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun RequestBuilder<Drawable>.crossfade(): RequestBuilder<Drawable> {
|
fun RequestBuilder<Drawable>.crossfade(): RequestBuilder<Drawable> {
|
||||||
return this.transition(DrawableTransitionOptions.withCrossFade());
|
return this.transition(DrawableTransitionOptions.withCrossFade());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package com.futo.platformplayer.images;
|
package com.futo.platformplayer.images;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.Registry;
|
import com.bumptech.glide.Registry;
|
||||||
import com.bumptech.glide.annotation.GlideModule;
|
import com.bumptech.glide.annotation.GlideModule;
|
||||||
import com.bumptech.glide.module.AppGlideModule;
|
import com.bumptech.glide.module.AppGlideModule;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
@@ -14,5 +17,8 @@ public class GrayjayAppGlideModule extends AppGlideModule {
|
|||||||
public void registerComponents(Context context, Glide glide, Registry registry) {
|
public void registerComponents(Context context, Glide glide, Registry registry) {
|
||||||
Log.i("GrayjayAppGlideModule", "registerComponents called");
|
Log.i("GrayjayAppGlideModule", "registerComponents called");
|
||||||
registry.prepend(String.class, ByteBuffer.class, new PolycentricModelLoader.Factory());
|
registry.prepend(String.class, ByteBuffer.class, new PolycentricModelLoader.Factory());
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
registry.prepend(String.class, InputStream.class, new MediaStoreThumbnailLoader.InputStreamFactory());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.futo.platformplayer.images
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.bumptech.glide.load.Options
|
||||||
|
import com.bumptech.glide.load.data.LocalUriFetcher
|
||||||
|
import com.bumptech.glide.load.model.ModelLoader
|
||||||
|
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||||
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||||
|
import com.bumptech.glide.signature.ObjectKey
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.net.MalformedURLException
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
class MediaStoreThumbnailLoader private constructor() : ModelLoader<String, InputStream> {
|
||||||
|
|
||||||
|
override fun handles(model: String): Boolean = isMediaStoreAudioUri(model)
|
||||||
|
|
||||||
|
private fun isMediaStoreAudioUri(uri: String): Boolean {
|
||||||
|
try {
|
||||||
|
val parsed = Uri.parse(uri);
|
||||||
|
return ContentResolver.SCHEME_CONTENT == parsed.scheme
|
||||||
|
&& MediaStore.AUTHORITY == parsed.authority
|
||||||
|
&& "audio" in parsed.pathSegments
|
||||||
|
}
|
||||||
|
catch(ex: MalformedURLException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildLoadData(model: String, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
|
||||||
|
val diskCacheKey = ObjectKey(model)
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver ?: return null;
|
||||||
|
val fetcher = InputStreamFetcher(resolver, Uri.parse(model), width, height)
|
||||||
|
return ModelLoader.LoadData(diskCacheKey, fetcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
class InputStreamFactory() : ModelLoaderFactory<String, InputStream> {
|
||||||
|
|
||||||
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<String, InputStream> = MediaStoreThumbnailLoader()
|
||||||
|
|
||||||
|
override fun teardown() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InputStreamFetcher(resolver: ContentResolver, uri: Uri, private val width: Int, private val height: Int) : LocalUriFetcher<InputStream>(resolver, uri) {
|
||||||
|
|
||||||
|
override fun getDataClass(): Class<InputStream> = InputStream::class.java
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
override fun loadResource(uri: Uri, contentResolver: ContentResolver): InputStream {
|
||||||
|
val optimalSizeOptions = Bundle(1)
|
||||||
|
optimalSizeOptions.putParcelable(ContentResolver.EXTRA_SIZE, Point(width, height))
|
||||||
|
|
||||||
|
return contentResolver.openTypedAssetFile(uri, "image/*", optimalSizeOptions, null)
|
||||||
|
?.createInputStream()
|
||||||
|
?: throw FileNotFoundException("FileDescriptor is null for: $uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun close(data: InputStream) {
|
||||||
|
data.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import androidx.documentfile.provider.DocumentFile
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
|
import com.curlbind.Libcurl
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs.Action
|
import com.futo.platformplayer.UIDialogs.Action
|
||||||
@@ -28,8 +29,6 @@ import com.futo.platformplayer.UIDialogs.Companion.showDialog
|
|||||||
import com.futo.platformplayer.activities.CaptchaActivity
|
import com.futo.platformplayer.activities.CaptchaActivity
|
||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
import com.futo.platformplayer.activities.SettingsActivity.Companion.settingsActivityClosed
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
@@ -38,6 +37,7 @@ import com.futo.platformplayer.constructs.Event0
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
import com.futo.platformplayer.logging.AndroidLogConsumer
|
import com.futo.platformplayer.logging.AndroidLogConsumer
|
||||||
import com.futo.platformplayer.logging.FileLogConsumer
|
import com.futo.platformplayer.logging.FileLogConsumer
|
||||||
@@ -53,6 +53,7 @@ import com.futo.polycentric.core.toBase64Url
|
|||||||
import com.futo.platformplayer.polycentric.ModerationsManager
|
import com.futo.platformplayer.polycentric.ModerationsManager
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
@@ -80,6 +81,9 @@ class StateApp {
|
|||||||
privateModeChanged.emit(privateMode);
|
privateModeChanged.emit(privateMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasMediaStoreAudioPermission: Boolean = false;
|
||||||
|
var hasMediaStoreVideoPermission: Boolean = false;
|
||||||
|
|
||||||
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||||
if(isValidStorageUri(context, generalUri))
|
if(isValidStorageUri(context, generalUri))
|
||||||
@@ -161,6 +165,12 @@ class StateApp {
|
|||||||
?: throw IllegalStateException("Attempted to use a global context while MainActivity is no longer available");
|
?: throw IllegalStateException("Attempted to use a global context while MainActivity is no longer available");
|
||||||
return thisContext;
|
return thisContext;
|
||||||
}
|
}
|
||||||
|
val activity: MainActivity? get() {
|
||||||
|
val context = contextOrNull;
|
||||||
|
if(context is MainActivity)
|
||||||
|
return context;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private var _mainId: String? = null;
|
private var _mainId: String? = null;
|
||||||
|
|
||||||
@@ -173,6 +183,9 @@ class StateApp {
|
|||||||
private var _lastMeteredState: Boolean = false;
|
private var _lastMeteredState: Boolean = false;
|
||||||
private var _connectivityManager: ConnectivityManager? = null;
|
private var _connectivityManager: ConnectivityManager? = null;
|
||||||
private var _lastNetworkState: NetworkState = NetworkState.UNKNOWN;
|
private var _lastNetworkState: NetworkState = NetworkState.UNKNOWN;
|
||||||
|
private var _lastConnectivityChange: OffsetDateTime? = null;
|
||||||
|
val lastConnectivityChange
|
||||||
|
get() = _lastConnectivityChange;
|
||||||
|
|
||||||
//Logging
|
//Logging
|
||||||
private var _fileLogConsumer: FileLogConsumer? = null;
|
private var _fileLogConsumer: FileLogConsumer? = null;
|
||||||
@@ -276,29 +289,52 @@ class StateApp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit)
|
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit) {
|
||||||
|
return requestDirectoryAccess(activity, name, purpose, path, handle, false);
|
||||||
|
}
|
||||||
|
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit, skipDialog: Boolean = false)
|
||||||
{
|
{
|
||||||
if(activity is Context)
|
if(activity is Context)
|
||||||
{
|
{
|
||||||
UIDialogs.showDialog(activity, R.drawable.ic_security, "Directory required for\n${name}", "Please select a directory for ${name}.\n${purpose}".trim(), null, 0,
|
if(skipDialog) {
|
||||||
UIDialogs.Action("Cancel", {}),
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
UIDialogs.Action("Ok", {
|
if(path != null)
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
|
||||||
if(path != null)
|
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
|
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
.or(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.or(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||||
.or(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
.or(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
activity.launchForResult(intent, 99) {
|
||||||
|
if(it.resultCode == Activity.RESULT_OK) {
|
||||||
|
handle(it.data?.data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
UIDialogs.showDialog(activity, R.drawable.ic_security, "Directory required for\n${name}", "Please select a directory for ${name}.\n${purpose}".trim(), null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Ok", {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
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)
|
||||||
|
.or(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
.or(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||||
|
|
||||||
|
activity.launchForResult(intent, 99) {
|
||||||
|
if(it.resultCode == Activity.RESULT_OK) {
|
||||||
|
handle(it.data?.data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
|
||||||
|
};
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
|
||||||
activity.launchForResult(intent, 99) {
|
|
||||||
if(it.resultCode == Activity.RESULT_OK) {
|
|
||||||
handle(it.data?.data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
|
|
||||||
};
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,6 +418,16 @@ class StateApp {
|
|||||||
Logger.i(TAG, "MainApp Starting");
|
Logger.i(TAG, "MainApp Starting");
|
||||||
initializeFiles(true);
|
initializeFiles(true);
|
||||||
|
|
||||||
|
_scope?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val caFile = AppCaUpdater.ensureCaBundle(context)
|
||||||
|
Libcurl.setDefaultCAPath(caFile.absolutePath)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
val fallback = File(context.noBackupFilesDir, "curl-ca-bundle.pem")
|
||||||
|
if (fallback.exists()) Libcurl.setDefaultCAPath(fallback.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(Settings.instance.other.polycentricLocalCache) {
|
if(Settings.instance.other.polycentricLocalCache) {
|
||||||
Logger.i(TAG, "Initialize Polycentric Disk Cache")
|
Logger.i(TAG, "Initialize Polycentric Disk Cache")
|
||||||
_cacheDirectory?.let { ApiMethods.initCache(it) };
|
_cacheDirectory?.let { ApiMethods.initCache(it) };
|
||||||
@@ -449,7 +495,7 @@ class StateApp {
|
|||||||
StateSync.instance.start(context)
|
StateSync.instance.start(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsActivityClosed.subscribe {
|
SettingsFragment.onClosed.subscribe {
|
||||||
if (Settings.instance.synchronization.enabled) {
|
if (Settings.instance.synchronization.enabled) {
|
||||||
StateSync.instance.start(context)
|
StateSync.instance.start(context)
|
||||||
} else {
|
} else {
|
||||||
@@ -461,7 +507,7 @@ class StateApp {
|
|||||||
scopeOrNull?.launch(Dispatchers.Main) {
|
scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (!it.isNullOrEmpty()) {
|
if (!it.isNullOrEmpty()) {
|
||||||
(SettingsActivity.getActivity() ?: contextOrNull)?.let { c ->
|
(StateApp.instance.activity ?: contextOrNull)?.let { c ->
|
||||||
val okButtonAction = Action(c.getString(R.string.ok), {}, ActionStyle.PRIMARY)
|
val okButtonAction = Action(c.getString(R.string.ok), {}, ActionStyle.PRIMARY)
|
||||||
val copyButtonAction = Action(c.getString(R.string.copy), {
|
val copyButtonAction = Action(c.getString(R.string.copy), {
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
@@ -590,7 +636,9 @@ class StateApp {
|
|||||||
scheduleBackgroundWork(context, interval != 0, interval);
|
scheduleBackgroundWork(context, interval != 0, interval);
|
||||||
|
|
||||||
Logger.i(TAG, "MainApp Started: Initialize [AutoBackup]");
|
Logger.i(TAG, "MainApp Started: Initialize [AutoBackup]");
|
||||||
|
Settings.instance.backup.didAskAutoBackup = true; //Some users have issues with it
|
||||||
if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) {
|
if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) {
|
||||||
|
/*
|
||||||
StateAnnouncement.instance.registerAnnouncement("backup", "Set Automatic Backup", "Configure daily backups of your data to restore in case of catastrophic failure.", AnnouncementType.SESSION, null, null, "Configure", {
|
StateAnnouncement.instance.registerAnnouncement("backup", "Set Automatic Backup", "Configure daily backups of your data to restore in case of catastrophic failure.", AnnouncementType.SESSION, null, null, "Configure", {
|
||||||
if(context is IWithResultLauncher && !Settings.instance.storage.isStorageMainValid(context)) {
|
if(context is IWithResultLauncher && !Settings.instance.storage.isStorageMainValid(context)) {
|
||||||
UIDialogs.toast("Missing general directory");
|
UIDialogs.toast("Missing general directory");
|
||||||
@@ -607,6 +655,7 @@ class StateApp {
|
|||||||
Settings.instance.backup.didAskAutoBackup = true;
|
Settings.instance.backup.didAskAutoBackup = true;
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
else if(Settings.instance.backup.didAskAutoBackup && Settings.instance.backup.shouldAutomaticBackup() && !Settings.instance.storage.isStorageMainValid(context)) {
|
else if(Settings.instance.backup.didAskAutoBackup && Settings.instance.backup.shouldAutomaticBackup() && !Settings.instance.storage.isStorageMainValid(context)) {
|
||||||
if(context is IWithResultLauncher) {
|
if(context is IWithResultLauncher) {
|
||||||
@@ -860,8 +909,11 @@ class StateApp {
|
|||||||
val beforeMeteredState = _lastMeteredState;
|
val beforeMeteredState = _lastMeteredState;
|
||||||
_lastNetworkState = getCurrentNetworkState();
|
_lastNetworkState = getCurrentNetworkState();
|
||||||
_lastMeteredState = isCurrentMetered();
|
_lastMeteredState = isCurrentMetered();
|
||||||
if(beforeNetworkState != _lastNetworkState || beforeMeteredState != _lastMeteredState)
|
if(beforeNetworkState != _lastNetworkState || beforeMeteredState != _lastMeteredState) {
|
||||||
Logger.i(TAG, "Network capabilities changed (State: ${_lastNetworkState}, Metered: ${_lastMeteredState})");
|
Logger.i(TAG, "Network capabilities changed (State: ${_lastNetworkState}, Metered: ${_lastMeteredState})");
|
||||||
|
_lastConnectivityChange = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
} catch(ex: Throwable) {
|
} catch(ex: Throwable) {
|
||||||
Logger.w(TAG, "Failed to update network state", ex);
|
Logger.w(TAG, "Failed to update network state", ex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
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.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
@@ -157,8 +156,8 @@ class StateBackup {
|
|||||||
}
|
}
|
||||||
catch (exSec: FileNotFoundException) {
|
catch (exSec: FileNotFoundException) {
|
||||||
Logger.e(TAG, "Failed to access backup file", exSec);
|
Logger.e(TAG, "Failed to access backup file", exSec);
|
||||||
val activity = if(SettingsActivity.getActivity() != null)
|
val activity = if(StateApp.instance.activity != null)
|
||||||
SettingsActivity.getActivity();
|
StateApp.instance.activity
|
||||||
else if(StateApp.instance.isMainActive)
|
else if(StateApp.instance.isMainActive)
|
||||||
StateApp.instance.contextOrNull;
|
StateApp.instance.contextOrNull;
|
||||||
else null;
|
else null;
|
||||||
@@ -226,7 +225,7 @@ class StateBackup {
|
|||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
val uri = FileProvider.getUriForFile(it, it.resources.getString(R.string.authority), exportFile);
|
val uri = FileProvider.getUriForFile(it, it.resources.getString(R.string.authority), exportFile);
|
||||||
|
|
||||||
val activity = SettingsActivity.getActivity() ?: return@let;
|
val activity = StateApp.instance.activity ?: return@let;
|
||||||
activity.startActivity(
|
activity.startActivity(
|
||||||
ShareCompat.IntentBuilder(activity)
|
ShareCompat.IntentBuilder(activity)
|
||||||
.setType("application/zip")
|
.setType("application/zip")
|
||||||
|
|||||||
@@ -0,0 +1,676 @@
|
|||||||
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.content.Intent
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Audio.Artists
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.net.toFile
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnail
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
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.LocalVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.Playlist
|
||||||
|
import com.futo.platformplayer.states.Album.Companion.TAG
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
|
import com.futo.platformplayer.toList
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
|
|
||||||
|
class StateLibrary {
|
||||||
|
|
||||||
|
private val _files = FragmentedStorage.get<StringArrayStorage>("libraryFiles")
|
||||||
|
|
||||||
|
|
||||||
|
fun getFileDirectories(): List<FileEntry> {
|
||||||
|
val context = StateApp.instance.contextOrNull ?: return listOf();
|
||||||
|
return _files.getAllValues().map {
|
||||||
|
if(it.startsWith("content://")) {
|
||||||
|
val uri = it.toUri();
|
||||||
|
val docFile = DocumentFile.fromTreeUri(context, uri) ?: return@map null;
|
||||||
|
//val access = context.contentResolver.persistedUriPermissions.any { it.uri == uri && it.isReadPermission }
|
||||||
|
if(!docFile.isDirectory) {
|
||||||
|
_files.remove(it);
|
||||||
|
return@map null;
|
||||||
|
}
|
||||||
|
if(docFile == null)
|
||||||
|
return@map null;
|
||||||
|
return@map FileEntry.fromFile(docFile).apply { this.removable = true }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
FileEntry.fromPath(it);
|
||||||
|
}.filterNotNull();
|
||||||
|
}
|
||||||
|
fun deleteFileDirectory(path: String) {
|
||||||
|
_files.remove(path);
|
||||||
|
_files.save();
|
||||||
|
}
|
||||||
|
fun addFileDirectory(onAdded: ((entry: FileEntry) -> Unit)? = null, skipDialog: Boolean = false): Boolean {
|
||||||
|
if(!StateApp.instance.isMainActive)
|
||||||
|
return false;
|
||||||
|
val mainActivity = StateApp.instance.contextOrNull as MainActivity? ?: return false;
|
||||||
|
|
||||||
|
StateApp.instance.requestDirectoryAccess(mainActivity, "Select Directory",
|
||||||
|
"Select a directory you would like to make accessible to Grayjay", null, {
|
||||||
|
if(it != null) {
|
||||||
|
mainActivity.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION));
|
||||||
|
try {
|
||||||
|
val file = DocumentFile.fromTreeUri(mainActivity, it) ?: return@requestDirectoryAccess;
|
||||||
|
val dir = FileEntry.fromFile(file);
|
||||||
|
_files.add(dir.path);
|
||||||
|
_files.save();
|
||||||
|
onAdded?.invoke(dir);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Something went wrong converting requested directory", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, skipDialog);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun searchTracks(str: String): List<IPlatformVideo> {
|
||||||
|
if(str.isNullOrBlank())
|
||||||
|
return listOf();
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Album contentResolver not found");
|
||||||
|
return listOf();
|
||||||
|
}
|
||||||
|
val cursor = resolver?.query(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA,
|
||||||
|
"LOWER(" + MediaStore.Audio.Media.DISPLAY_NAME + ") LIKE ? ", arrayOf("%" + str.trim().lowercase() + "%"),
|
||||||
|
null) ?: return listOf();
|
||||||
|
cursor.moveToFirst();
|
||||||
|
val list = mutableListOf<IPlatformVideo>()
|
||||||
|
while(!cursor.isAfterLast) {
|
||||||
|
list.add(StateLibrary.audioFromCursor(cursor));
|
||||||
|
cursor.moveToNext();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAlbums(): List<Album> {
|
||||||
|
return Album.getAlbums();
|
||||||
|
}
|
||||||
|
fun getAlbum(str: String): Album? {
|
||||||
|
val idLong = str.toLongOrNull();
|
||||||
|
if(idLong != null)
|
||||||
|
return getAlbum(idLong);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
fun searchAlbums(str: String): List<Album> {
|
||||||
|
if(str.isNullOrBlank())
|
||||||
|
return listOf();
|
||||||
|
return Album.getAlbums("LOWER(" + MediaStore.Audio.Albums.ALBUM + ") LIKE ? ", arrayOf("%" + str.trim().lowercase() + "%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAlbum(id: Long): Album? {
|
||||||
|
return Album.getAlbum(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getArtists(ordering: ArtistOrdering): List<Artist> {
|
||||||
|
return Artist.getArtists(ordering);
|
||||||
|
}
|
||||||
|
fun getArtist(str: String): Artist? {
|
||||||
|
val idLong = str.toLongOrNull();
|
||||||
|
if(idLong != null)
|
||||||
|
return getArtist(idLong);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
fun searchArtists(str: String): List<Artist> {
|
||||||
|
if(str.isNullOrBlank())
|
||||||
|
return listOf();
|
||||||
|
return Artist.getArtists(ArtistOrdering.TrackCount, "LOWER(" + MediaStore.Audio.Artists.ARTIST + ") LIKE ? ", arrayOf("%" + str.trim().lowercase() + "%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getArtist(id: Long): Artist? {
|
||||||
|
return Artist.getArtist(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getVideos(buckets: List<String>? = null): IPager<IPlatformContent> {
|
||||||
|
var query = if(buckets != null) "${MediaStore.Video.Media.BUCKET_DISPLAY_NAME} IN " + "(" + buckets.map { "'${it}'" }.joinToString(",") + ")" else null;
|
||||||
|
val cursor = StateApp.instance.contextOrNull?.contentResolver?.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, PROJECTION_VIDEO,
|
||||||
|
query,
|
||||||
|
null,
|
||||||
|
MediaStore.Video.Media.DATE_ADDED + " DESC") ?: return EmptyPager();
|
||||||
|
cursor.moveToFirst();
|
||||||
|
val list = mutableListOf<IPlatformVideo>()
|
||||||
|
while(!cursor.isAfterLast && list.size < 10) {
|
||||||
|
list.add(videoFromCursor(cursor));
|
||||||
|
cursor.moveToNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
return AdhocPager<IPlatformContent>({
|
||||||
|
val list = mutableListOf<IPlatformContent>()
|
||||||
|
while(!cursor.isAfterLast && list.size < 10) {
|
||||||
|
list.add(videoFromCursor(cursor));
|
||||||
|
cursor.moveToNext();
|
||||||
|
}
|
||||||
|
return@AdhocPager list;
|
||||||
|
}, list);
|
||||||
|
}
|
||||||
|
fun getRecentVideos(buckets: List<String>? = null, count: Int = 20): List<IPlatformVideo> {
|
||||||
|
val videoPager = getVideos(buckets);
|
||||||
|
val items = mutableListOf<IPlatformVideo>();
|
||||||
|
while(videoPager.getResults().size > 0 && items.size < count) {
|
||||||
|
items.addAll(videoPager.getResults().filter { it is IPlatformVideo }.map { it as IPlatformVideo });
|
||||||
|
if(videoPager.hasMorePages())
|
||||||
|
videoPager.nextPage();
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _cacheBucketNames: List<Bucket>? = null;
|
||||||
|
fun getVideoBucketNames(): List<Bucket> {
|
||||||
|
if(_cacheBucketNames != null)
|
||||||
|
return _cacheBucketNames ?: listOf();
|
||||||
|
val cur: Cursor = StateApp.instance.contextOrNull?.contentResolver?.query(
|
||||||
|
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, arrayOf(
|
||||||
|
MediaStore.Video.Media.BUCKET_ID,
|
||||||
|
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
|
||||||
|
), null, null, null
|
||||||
|
) ?: return listOf();
|
||||||
|
|
||||||
|
val buckets = mutableListOf<Bucket>();
|
||||||
|
val list = HashSet<Long>();
|
||||||
|
if (cur.moveToFirst()) {
|
||||||
|
var id: Long;
|
||||||
|
var bucket: String
|
||||||
|
do {
|
||||||
|
id = cur.getLong(0);
|
||||||
|
bucket = cur.getString(1)
|
||||||
|
if(!list.contains(id)) {
|
||||||
|
list.add(id);
|
||||||
|
buckets.add(Bucket(id, bucket));
|
||||||
|
}
|
||||||
|
} while (cur.moveToNext())
|
||||||
|
}
|
||||||
|
_cacheBucketNames = buckets.toList()
|
||||||
|
return _cacheBucketNames ?: listOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val PROJECTION_VIDEO = arrayOf(
|
||||||
|
MediaStore.Video.Media._ID,
|
||||||
|
MediaStore.Video.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Video.Media.AUTHOR,
|
||||||
|
MediaStore.Video.Media.DATE_ADDED,
|
||||||
|
MediaStore.Video.Media.MIME_TYPE,
|
||||||
|
MediaStore.Video.Media.BUCKET_DISPLAY_NAME
|
||||||
|
);
|
||||||
|
val PROJECTION_MEDIA = arrayOf(
|
||||||
|
MediaStore.Audio.Media._ID, //0
|
||||||
|
MediaStore.Audio.Media.DISPLAY_NAME, //1
|
||||||
|
MediaStore.Audio.Media.ARTIST, //2
|
||||||
|
MediaStore.Audio.Media.ALBUM_ID, //3
|
||||||
|
MediaStore.Audio.Media.DURATION, //4
|
||||||
|
MediaStore.Audio.Media.DATE_ADDED, //5
|
||||||
|
MediaStore.Audio.Media.MIME_TYPE, //6
|
||||||
|
MediaStore.Audio.Media.BUCKET_DISPLAY_NAME //7
|
||||||
|
);
|
||||||
|
|
||||||
|
fun getDocumentTrack(url: String): IPlatformContentDetails? {
|
||||||
|
if(!url.contains("com.android.externalstorage.documents"))
|
||||||
|
return null;
|
||||||
|
val docFile = DocumentFile.fromSingleUri(StateApp.instance.context, url.toUri()) ?: return null;
|
||||||
|
|
||||||
|
val contentUri = docFile.uri.toString();
|
||||||
|
|
||||||
|
val mimeType = MimeTypeMap.getFileExtensionFromUrl(contentUri);
|
||||||
|
|
||||||
|
if(docFile.name != null) {
|
||||||
|
if (StateApp.instance.hasMediaStoreAudioPermission && mimeType.startsWith("audio/")) {
|
||||||
|
val aud = findAudioByName(docFile.name!!);
|
||||||
|
if (aud != null)
|
||||||
|
return aud;
|
||||||
|
}
|
||||||
|
if (StateApp.instance.hasMediaStoreVideoPermission && mimeType.startsWith("video/")) {
|
||||||
|
val vid = findVideoByName(docFile.name!!);
|
||||||
|
if (vid != null)
|
||||||
|
return vid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocalVideoDetails(
|
||||||
|
PlatformID("FILE", contentUri, null, 0, -1),
|
||||||
|
docFile.name ?: docFile.uri.toString(), Thumbnails(arrayOf(
|
||||||
|
Thumbnail(docFile.uri.toString(), 0)
|
||||||
|
)), PlatformAuthorLink.UNKNOWN, contentUri, 0, mimeType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAudioTrack(url: String): IPlatformContentDetails? {
|
||||||
|
val uri = Uri.parse(url);
|
||||||
|
val id = uri.lastPathSegment?.toLongOrNull();
|
||||||
|
if(id == null) {
|
||||||
|
return getDocumentTrack(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Album contentResolver not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
val cursor = resolver?.query(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA, "${MediaStore.Audio.Media._ID} = ?", arrayOf(id.toString()),
|
||||||
|
null) ?: return null;
|
||||||
|
cursor.moveToFirst();
|
||||||
|
if(cursor.isAfterLast)
|
||||||
|
return null;
|
||||||
|
return audioFromCursor(cursor);
|
||||||
|
}
|
||||||
|
fun findAudioByName(name: String): IPlatformContentDetails? {
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Audio contentResolver not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
val cursor = resolver?.query(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA, "${MediaStore.Audio.Media.DISPLAY_NAME} = ?", arrayOf(name),
|
||||||
|
null) ?: return null;
|
||||||
|
cursor.moveToFirst();
|
||||||
|
if(cursor.isAfterLast)
|
||||||
|
return null;
|
||||||
|
return audioFromCursor(cursor);
|
||||||
|
}
|
||||||
|
fun getVideoTrack(url: String): IPlatformContentDetails? {
|
||||||
|
val uri = Uri.parse(url);
|
||||||
|
val id = uri.lastPathSegment?.toLongOrNull();
|
||||||
|
if(id == null)
|
||||||
|
return getDocumentTrack(url);
|
||||||
|
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Album contentResolver not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
val cursor = resolver?.query(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_VIDEO, "${MediaStore.Video.Media._ID} = ?", arrayOf(id.toString()),
|
||||||
|
null) ?: return null;
|
||||||
|
cursor.moveToFirst();
|
||||||
|
if(cursor.isAfterLast)
|
||||||
|
return null;
|
||||||
|
return videoFromCursor(cursor);
|
||||||
|
}
|
||||||
|
fun findVideoByName(name: String): IPlatformContentDetails? {
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Album contentResolver not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
val cursor = resolver?.query(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_VIDEO, "${MediaStore.Video.Media.DISPLAY_NAME} = ?", arrayOf(name),
|
||||||
|
null) ?: return null;
|
||||||
|
cursor.moveToFirst();
|
||||||
|
if(cursor.isAfterLast)
|
||||||
|
return null;
|
||||||
|
return videoFromCursor(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun audioFromCursor(cursor: Cursor): IPlatformVideoDetails {
|
||||||
|
val id = cursor.getString(0);
|
||||||
|
val displayName = cursor.getString(1);
|
||||||
|
val author = cursor.getString(2);
|
||||||
|
val albumId = cursor.getLong(3);
|
||||||
|
val duration = cursor.getLong(4).let { if(it > 0) it / 1000 else 0 };
|
||||||
|
val date = cursor.getLong(5);
|
||||||
|
val contentType = cursor.getString(6);
|
||||||
|
val category = cursor.getString(7);
|
||||||
|
|
||||||
|
val idLong = id.toLongOrNull();
|
||||||
|
val contentUrl = if(idLong != null )
|
||||||
|
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, idLong).toString();
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
val albumContentUrl = if(albumId > 0)
|
||||||
|
ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId)?.toString()
|
||||||
|
else null;
|
||||||
|
|
||||||
|
val dateObj = if(date > 0)
|
||||||
|
OffsetDateTime.ofInstant(Instant.ofEpochSecond(date), ZoneOffset.UTC)
|
||||||
|
else null;
|
||||||
|
|
||||||
|
val authorObj = if(!author.isNullOrBlank())
|
||||||
|
PlatformAuthorLink(PlatformID.NONE, author, "", null, null)
|
||||||
|
else PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
|
return LocalVideoDetails(
|
||||||
|
PlatformID("FILE", contentUrl, null, 0, -1),
|
||||||
|
displayName, Thumbnails(arrayOf(
|
||||||
|
Thumbnail(albumContentUrl ?: contentUrl, 0)
|
||||||
|
)), authorObj, contentUrl, duration, contentType, dateObj);
|
||||||
|
}
|
||||||
|
fun videoFromCursor(cursor: Cursor): IPlatformVideoDetails {
|
||||||
|
val id = cursor.getString(0);
|
||||||
|
val displayName = cursor.getString(1);
|
||||||
|
val author = cursor.getString(2);
|
||||||
|
val date = cursor.getLong(3);
|
||||||
|
val contentType = cursor.getString(4);
|
||||||
|
val category = cursor.getString(5);
|
||||||
|
|
||||||
|
val idLong = id.toLongOrNull();
|
||||||
|
val contentUrl = if(idLong != null )
|
||||||
|
ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, idLong).toString();
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
val dateObj = if(date > 0)
|
||||||
|
OffsetDateTime.ofInstant(Instant.ofEpochSecond(date), ZoneOffset.UTC)
|
||||||
|
else null;
|
||||||
|
|
||||||
|
val authorObj = if(!author.isNullOrBlank())
|
||||||
|
PlatformAuthorLink(PlatformID.NONE, author, "", null, null)
|
||||||
|
else PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
|
return LocalVideoDetails(
|
||||||
|
PlatformID("FILE", contentUrl, null, 0, -1),
|
||||||
|
displayName, Thumbnails(arrayOf(
|
||||||
|
Thumbnail(contentUrl, 0)
|
||||||
|
)), authorObj, contentUrl, -1, contentType, dateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _instance : StateLibrary? = null;
|
||||||
|
val instance : StateLibrary
|
||||||
|
get(){
|
||||||
|
if(_instance == null)
|
||||||
|
_instance = StateLibrary();
|
||||||
|
return _instance!!;
|
||||||
|
};
|
||||||
|
|
||||||
|
fun finish() {
|
||||||
|
_instance?.let {
|
||||||
|
_instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bucket(val id: Long, val name: String);
|
||||||
|
|
||||||
|
|
||||||
|
enum class ArtistOrdering {
|
||||||
|
Alphabethic,
|
||||||
|
TrackCount,
|
||||||
|
AlbumCount
|
||||||
|
}
|
||||||
|
class Artist {
|
||||||
|
val id: String;
|
||||||
|
val name: String;
|
||||||
|
val countTracks: Int;
|
||||||
|
val countAlbums: Int;
|
||||||
|
val thumbnail: String?;
|
||||||
|
val contentUrl: String?;
|
||||||
|
|
||||||
|
constructor(name: String, countTracks: Int = -1, countAlbums: Int = -1, thumbnail: String? = null, id: String? = null, contentUrl: String? = null) {
|
||||||
|
this.id = id ?: ID_UNKNOWN;
|
||||||
|
this.name = name;
|
||||||
|
this.thumbnail = thumbnail;
|
||||||
|
this.countTracks = countTracks;
|
||||||
|
this.countAlbums = countAlbums;
|
||||||
|
this.contentUrl = contentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAlbums(): List<Album> {
|
||||||
|
return Album.getArtistAlbums(id.toLongOrNull() ?: return listOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toPlaylist(tracks: List<IPlatformVideo>? = null): Playlist {
|
||||||
|
return Playlist(name, tracks?.map { SerializedPlatformVideo.fromVideo(it) } ?: getAudioTracks().toList().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAudioTracks(): IPager<IPlatformContent> {
|
||||||
|
val idLong = id.toLongOrNull() ?: return EmptyPager();
|
||||||
|
return AdhocPager({ listOf() }, getTracksPager(idLong));
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ID_UNKNOWN = "UNKNOWN";
|
||||||
|
val PROJECTION: Array<String> = arrayOf(Artists._ID,
|
||||||
|
Artists.ARTIST,
|
||||||
|
Artists.NUMBER_OF_TRACKS,
|
||||||
|
Artists.NUMBER_OF_ALBUMS);
|
||||||
|
|
||||||
|
fun fromCursor(cursor: Cursor): Artist {
|
||||||
|
val id = cursor.getString(0);
|
||||||
|
val artist = cursor.getString(1);
|
||||||
|
val numTracks = cursor.getInt(2);
|
||||||
|
val numAlbums = cursor.getInt(3);
|
||||||
|
|
||||||
|
val idLong = id.toLongOrNull();
|
||||||
|
val uri = if(idLong != null) ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, idLong) else null;
|
||||||
|
|
||||||
|
return Artist(artist, numTracks, numAlbums, null, id, uri?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getArtist(id: Long): Artist? {
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Artist contentResolver not found");
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val cursor = resolver.query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
|
||||||
|
Artist.PROJECTION,
|
||||||
|
"${MediaStore.Audio.Artists._ID} = ?",
|
||||||
|
arrayOf(id.toString()), null) ?:
|
||||||
|
return null;
|
||||||
|
cursor.moveToFirst();
|
||||||
|
if(cursor.isAfterLast)
|
||||||
|
return null;
|
||||||
|
return Artist.fromCursor(cursor);
|
||||||
|
}
|
||||||
|
fun getArtists(ordering: ArtistOrdering = ArtistOrdering.Alphabethic, query: String? = null, args: Array<String>? = null): List<Artist> {
|
||||||
|
val ordering = when(ordering) {
|
||||||
|
ArtistOrdering.Alphabethic -> Artists.ARTIST + " ASC";
|
||||||
|
ArtistOrdering.AlbumCount -> Artists.NUMBER_OF_ALBUMS + " DESC";
|
||||||
|
ArtistOrdering.TrackCount -> Artists.NUMBER_OF_TRACKS + " DESC";
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val cursor = StateApp.instance.contextOrNull?.contentResolver?.query(Artists.EXTERNAL_CONTENT_URI, PROJECTION,
|
||||||
|
query,
|
||||||
|
args,
|
||||||
|
ordering) ?: return listOf();
|
||||||
|
cursor.moveToFirst();
|
||||||
|
val list = mutableListOf<Artist>()
|
||||||
|
while(!cursor.isAfterLast) {
|
||||||
|
list.add(fromCursor(cursor));
|
||||||
|
cursor.moveToNext();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTracksPager(artistId: Long): List<IPlatformVideo> {
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Album contentResolver not found");
|
||||||
|
return listOf();
|
||||||
|
}
|
||||||
|
val cursor = resolver?.query(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA, "${MediaStore.Audio.Media.ARTIST_ID} = ?", arrayOf(artistId.toString()),
|
||||||
|
null) ?: return listOf();
|
||||||
|
cursor.moveToFirst();
|
||||||
|
val list = mutableListOf<IPlatformVideo>()
|
||||||
|
while(!cursor.isAfterLast) {
|
||||||
|
list.add(StateLibrary.audioFromCursor(cursor));
|
||||||
|
cursor.moveToNext();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Album {
|
||||||
|
val id: String;
|
||||||
|
val name: String;
|
||||||
|
val artist: String?;
|
||||||
|
val countTracks: Int;
|
||||||
|
val thumbnail: String?;
|
||||||
|
|
||||||
|
constructor(name: String, countTracks: Int = -1, artist: String? = null, id: String? = null, thumbnail: String? = null) {
|
||||||
|
this.id = id ?: ID_UNKNOWN;
|
||||||
|
this.name = name;
|
||||||
|
this.artist = artist;
|
||||||
|
this.countTracks = countTracks;
|
||||||
|
this.thumbnail = thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTracks(): List<IPlatformVideo> {
|
||||||
|
return getAlbumTracks(id.toLongOrNull() ?: return listOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toPlaylist(tracks: List<IPlatformVideo>? = null): Playlist {
|
||||||
|
return Playlist(name, tracks?.map { SerializedPlatformVideo.fromVideo(it) } ?: getTracks().map { SerializedPlatformVideo.fromVideo(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = "StateLibrary";
|
||||||
|
val ID_UNKNOWN = "UNKNOWN";
|
||||||
|
val PROJECTION = arrayOf(MediaStore.Audio.Albums.ALBUM_ID,
|
||||||
|
MediaStore.Audio.Albums.ALBUM,
|
||||||
|
MediaStore.Audio.Albums.NUMBER_OF_SONGS,
|
||||||
|
MediaStore.Audio.Albums.ARTIST);
|
||||||
|
|
||||||
|
fun fromCursor(cursor: Cursor): Album {
|
||||||
|
val id = cursor.getString(0);
|
||||||
|
val album = cursor.getString(1);
|
||||||
|
val numTracks = cursor.getInt(2);
|
||||||
|
val artist = cursor.getString(3);
|
||||||
|
|
||||||
|
val idLong = id.toLongOrNull();
|
||||||
|
val uri = if(idLong != null) ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, idLong) else null;
|
||||||
|
return Album(album, numTracks, artist, id, uri?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAlbumTracks(albumId: Long): List<IPlatformVideo> {
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Album contentResolver not found");
|
||||||
|
return listOf();
|
||||||
|
}
|
||||||
|
val cursor = resolver?.query(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA, "${MediaStore.Audio.Media.ALBUM_ID} = ?", arrayOf(albumId.toString()),
|
||||||
|
null) ?: return listOf();
|
||||||
|
cursor.moveToFirst();
|
||||||
|
val list = mutableListOf<IPlatformVideo>()
|
||||||
|
while(!cursor.isAfterLast) {
|
||||||
|
list.add(StateLibrary.audioFromCursor(cursor));
|
||||||
|
cursor.moveToNext();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
fun getAlbum(id: Long): Album? {
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Album contentResolver not found");
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val cursor = resolver.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
|
||||||
|
PROJECTION,
|
||||||
|
"${MediaStore.Audio.Albums.ALBUM_ID} = ?",
|
||||||
|
arrayOf(id.toString()), null) ?:
|
||||||
|
return null;
|
||||||
|
cursor.moveToFirst();
|
||||||
|
if(cursor.isAfterLast)
|
||||||
|
return null;
|
||||||
|
return fromCursor(cursor);
|
||||||
|
}
|
||||||
|
fun getAlbums(query: String? = null, args: Array<String>? = null): List<Album> {
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Album contentResolver not found");
|
||||||
|
return listOf();
|
||||||
|
}
|
||||||
|
val cursor = resolver?.query(
|
||||||
|
MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, PROJECTION, query, args,
|
||||||
|
MediaStore.Audio.Albums.ALBUM + " ASC") ?: return listOf();
|
||||||
|
cursor.moveToFirst();
|
||||||
|
val list = mutableListOf<Album>()
|
||||||
|
while(!cursor.isAfterLast) {
|
||||||
|
list.add(fromCursor(cursor));
|
||||||
|
cursor.moveToNext();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
fun getArtistAlbums(artistId: Long): List<Album> {
|
||||||
|
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||||
|
if(resolver == null) {
|
||||||
|
Logger.w(TAG, "Album contentResolver not found");
|
||||||
|
return listOf();
|
||||||
|
}
|
||||||
|
val cursor = resolver?.query(
|
||||||
|
MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, PROJECTION, "${MediaStore.Audio.Media.ARTIST_ID} = ?", arrayOf(artistId.toString()),
|
||||||
|
MediaStore.Audio.Albums.ALBUM + " ASC") ?: return listOf();
|
||||||
|
cursor.moveToFirst();
|
||||||
|
val list = mutableListOf<Album>()
|
||||||
|
while(!cursor.isAfterLast) {
|
||||||
|
list.add(fromCursor(cursor));
|
||||||
|
cursor.moveToNext();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FileEntry(
|
||||||
|
val path: String,
|
||||||
|
val name: String,
|
||||||
|
val isDirectory: Boolean = false,
|
||||||
|
val thumbnail: String? = null,
|
||||||
|
|
||||||
|
var removable: Boolean = false
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun getSubFiles(): List<FileEntry> {
|
||||||
|
if(isDirectory) {
|
||||||
|
if(path.startsWith("content://"))
|
||||||
|
return DocumentFile.fromTreeUri(StateApp.instance.context, path.toUri())?.listFiles()
|
||||||
|
?.map { fromFile(it) } ?: return listOf();
|
||||||
|
return File(path).listFiles()
|
||||||
|
.map { fromFile(it) }
|
||||||
|
}
|
||||||
|
return listOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromPath(path: String): FileEntry {
|
||||||
|
/*
|
||||||
|
val cursor = StateApp.instance.context.contentResolver.query(path.toUri(), null, null, null, null);
|
||||||
|
cursor?.moveToFirst();
|
||||||
|
val fileName = cursor?.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||||
|
cursor?.close();
|
||||||
|
return FileEntry(path, fileName, );
|
||||||
|
*/
|
||||||
|
val file = File(path);
|
||||||
|
return FileEntry(file.path, file.name, file.isDirectory);
|
||||||
|
}
|
||||||
|
fun fromFile(file: File): FileEntry {
|
||||||
|
return FileEntry(file.path, file.name, file.isDirectory);
|
||||||
|
}
|
||||||
|
fun fromFile(file: DocumentFile): FileEntry {
|
||||||
|
return FileEntry(file.uri.toString(), file.name ?: "", file.isDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.LocalClient
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
@@ -75,6 +76,7 @@ class StatePlatform {
|
|||||||
private val _cache : LruCache<String, CachedPlatformContent> = LruCache<String, CachedPlatformContent>(VIDEO_CACHE);
|
private val _cache : LruCache<String, CachedPlatformContent> = LruCache<String, CachedPlatformContent>(VIDEO_CACHE);
|
||||||
|
|
||||||
//Clients
|
//Clients
|
||||||
|
private val _localClient = LocalClient();
|
||||||
private val _enabledClientsPersistent = FragmentedStorage.get<StringArrayStorage>("enabledClients");
|
private val _enabledClientsPersistent = FragmentedStorage.get<StringArrayStorage>("enabledClients");
|
||||||
private val _platformOrderPersistent = FragmentedStorage.get<StringArrayStorage>("platformOrder");
|
private val _platformOrderPersistent = FragmentedStorage.get<StringArrayStorage>("platformOrder");
|
||||||
private val _clientsLock = Object();
|
private val _clientsLock = Object();
|
||||||
@@ -117,6 +119,7 @@ class StatePlatform {
|
|||||||
_enabledClients.find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) }?.let {
|
_enabledClients.find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) }?.let {
|
||||||
_mainClientPool.getClientPooled(it).getContentDetails(url)
|
_mainClientPool.getClientPooled(it).getContentDetails(url)
|
||||||
}
|
}
|
||||||
|
?: (if(_localClient.isContentDetailsUrl(url)) _localClient.getContentDetails(url) else null)
|
||||||
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -124,6 +127,7 @@ class StatePlatform {
|
|||||||
_enabledClients.find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) }?.let {
|
_enabledClients.find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) }?.let {
|
||||||
_privateClientPool.getClientPooled(it).getContentDetails(url)
|
_privateClientPool.getClientPooled(it).getContentDetails(url)
|
||||||
}
|
}
|
||||||
|
?: (if(_localClient.isContentDetailsUrl(url)) _localClient.getContentDetails(url) else null)
|
||||||
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.futo.platformplayer.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
|
||||||
|
class AlbumHeaderView: ConstraintLayout {
|
||||||
|
|
||||||
|
|
||||||
|
val textName: TextView;
|
||||||
|
val textMetadata: TextView;
|
||||||
|
|
||||||
|
val imageThumbnail: ImageView;
|
||||||
|
val imageThumbnailBackground: ImageView;
|
||||||
|
|
||||||
|
val buttonPlayAll: LinearLayout;
|
||||||
|
val buttonShuffle: LinearLayout;
|
||||||
|
|
||||||
|
val onPlayAll = Event0();
|
||||||
|
val onShuffle = Event0();
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context) {
|
||||||
|
inflate(context, R.layout.view_album_header, this)
|
||||||
|
|
||||||
|
textName = findViewById(R.id.text_name);
|
||||||
|
textMetadata = findViewById(R.id.text_metadata);
|
||||||
|
imageThumbnail = findViewById(R.id.image_thumbnail);
|
||||||
|
imageThumbnailBackground = findViewById(R.id.image_thumbnail_background);
|
||||||
|
buttonPlayAll = findViewById(R.id.button_play_all);
|
||||||
|
buttonShuffle = findViewById(R.id.button_shuffle);
|
||||||
|
|
||||||
|
buttonPlayAll.setOnClickListener { onPlayAll.emit() };
|
||||||
|
buttonShuffle.setOnClickListener { onShuffle.emit() };
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setThumbnail(thumbnail: String?) {
|
||||||
|
if (thumbnail != null)
|
||||||
|
Glide.with(imageThumbnail)
|
||||||
|
.load(thumbnail)
|
||||||
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(imageThumbnail)
|
||||||
|
else
|
||||||
|
Glide.with(imageThumbnail)
|
||||||
|
.load(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(imageThumbnail);
|
||||||
|
if (thumbnail != null)
|
||||||
|
Glide.with(imageThumbnailBackground)
|
||||||
|
.load(thumbnail)
|
||||||
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(imageThumbnailBackground)
|
||||||
|
else
|
||||||
|
Glide.with(imageThumbnailBackground)
|
||||||
|
.load(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(imageThumbnailBackground);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setName(str: String){
|
||||||
|
textName.text = str;
|
||||||
|
}
|
||||||
|
fun setMetadata(str: String) {
|
||||||
|
textMetadata.text = str;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,9 +72,9 @@ class AnyInsertedAdapterView<I, T>(view: RecyclerView, adapter: BaseAnyAdapter<I
|
|||||||
= this.asAnyWithViews(arrayListOf(view), arrayListOf(), orientation, reversed, onCreate);
|
= this.asAnyWithViews(arrayListOf(view), arrayListOf(), orientation, reversed, onCreate);
|
||||||
inline fun<I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAnyWithViews(prepend: ArrayList<View> = arrayListOf(), append: ArrayList<View> = arrayListOf(), orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null) : AnyInsertedAdapterView<I, T> {
|
inline fun<I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAnyWithViews(prepend: ArrayList<View> = arrayListOf(), append: ArrayList<View> = arrayListOf(), orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null) : AnyInsertedAdapterView<I, T> {
|
||||||
for(view in prepend)
|
for(view in prepend)
|
||||||
(view.parent as ViewGroup).removeView(view);
|
(view.parent as ViewGroup?)?.removeView(view);
|
||||||
for(view in append)
|
for(view in append)
|
||||||
(view.parent as ViewGroup).removeView(view);
|
(view.parent as ViewGroup?)?.removeView(view);
|
||||||
return AnyInsertedAdapterView(this, AnyInsertedAdapter.create(prepend, append, onCreate), orientation, reversed);
|
return AnyInsertedAdapterView(this, AnyInsertedAdapter.create(prepend, append, onCreate), orientation, reversed);
|
||||||
}
|
}
|
||||||
inline fun<I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAnyWithViews(list: ArrayList<I>, prepend: ArrayList<View> = arrayListOf(), append: ArrayList<View> = arrayListOf(), orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null) : AnyInsertedAdapterView<I, T> {
|
inline fun<I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAnyWithViews(list: ArrayList<I>, prepend: ArrayList<View> = arrayListOf(), append: ArrayList<View> = arrayListOf(), orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null) : AnyInsertedAdapterView<I, T> {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.futo.platformplayer.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout.GONE
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout.inflate
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter.AnyViewHolder
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
|
||||||
|
class LibrarySection: ConstraintLayout {
|
||||||
|
val textName: TextView;
|
||||||
|
val imageNavigate: ImageView;
|
||||||
|
val recycler: RecyclerView;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(context: Context, attr: AttributeSet? = null) : super(context, attr) {
|
||||||
|
inflate(context, R.layout.view_library_section, this);
|
||||||
|
textName = findViewById(R.id.text_label)
|
||||||
|
imageNavigate = findViewById(R.id.image_nav)
|
||||||
|
recycler = findViewById(R.id.recycler_collection);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNavIcon(resId: Int) {
|
||||||
|
imageNavigate.setImageResource(resId);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContentEmptyMessage(icon: Int, msg: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
inline fun <T, reified V: AnyViewHolder<T>> getAnyAdapter(noinline onCreate: ((V)->Unit)? = null, orientation: Int = RecyclerView.HORIZONTAL): AnyAdapterView<T, V> {
|
||||||
|
return recycler.asAny<T, V>(orientation, false, onCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun setSection(title: String, crossinline onOpen: (()->Unit)) {
|
||||||
|
textName.text = title;
|
||||||
|
imageNavigate.setOnClickListener { onOpen.invoke() };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.futo.platformplayer.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
|
||||||
|
class LibraryTypeHeaderView: ConstraintLayout {
|
||||||
|
|
||||||
|
var selected: SelectedType = SelectedType.Artists;
|
||||||
|
|
||||||
|
val pillArtist: PillV2;
|
||||||
|
val pillAlbums: PillV2;
|
||||||
|
val textMetadata: TextView;
|
||||||
|
val pills: List<PillV2>
|
||||||
|
|
||||||
|
val onSelectedChanged = Event1<SelectedType>();
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context) {
|
||||||
|
inflate(context, R.layout.view_library_type_header, this)
|
||||||
|
|
||||||
|
textMetadata = findViewById(R.id.text_metadata);
|
||||||
|
pillArtist = findViewById(R.id.pill_artist);
|
||||||
|
pillAlbums = findViewById(R.id.pill_albums);
|
||||||
|
|
||||||
|
pillArtist.onClick.subscribe {
|
||||||
|
setSelectedType(SelectedType.Artists, true);
|
||||||
|
}
|
||||||
|
pillAlbums.onClick.subscribe {
|
||||||
|
setSelectedType(SelectedType.Albums, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pills = listOf(pillArtist, pillAlbums);
|
||||||
|
|
||||||
|
setSelectedType(SelectedType.Artists, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMetadata(str: String) {
|
||||||
|
textMetadata.text = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelectedType(selected: SelectedType, notify: Boolean = false){
|
||||||
|
this.selected = selected;
|
||||||
|
|
||||||
|
pills.forEach { it.setIsEnabled(false) };
|
||||||
|
|
||||||
|
when(selected) {
|
||||||
|
SelectedType.Artists -> {
|
||||||
|
pillArtist.setIsEnabled(true);
|
||||||
|
}
|
||||||
|
SelectedType.Albums -> {
|
||||||
|
pillAlbums.setIsEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(notify)
|
||||||
|
onSelectedChanged.emit(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
enum class SelectedType {
|
||||||
|
Artists,
|
||||||
|
Albums
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.futo.platformplayer.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
|
import com.futo.platformplayer.views.others.ToggleTagView
|
||||||
|
|
||||||
|
class PillV2: FrameLayout {
|
||||||
|
|
||||||
|
val root: FrameLayout;
|
||||||
|
val text: TextView;
|
||||||
|
|
||||||
|
var isToggled: Boolean = false;
|
||||||
|
|
||||||
|
val onClick = Event1<Boolean>();
|
||||||
|
|
||||||
|
constructor(context: Context, name: String, isActive: Boolean = false, action: (PillV2, Boolean)->Unit, actionLong: ((PillV2, Boolean)->Unit)? = null): super(context) {
|
||||||
|
inflate(context, R.layout.view_tag_v2, this);
|
||||||
|
root = findViewById(R.id.root);
|
||||||
|
text = findViewById(R.id.text_tag);
|
||||||
|
|
||||||
|
text.text = name;
|
||||||
|
setIsEnabled(isActive);
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
setIsEnabled(!isToggled);
|
||||||
|
onClick.emit(isToggled);
|
||||||
|
action(this, isToggled);
|
||||||
|
}
|
||||||
|
if(actionLong != null)
|
||||||
|
setOnLongClickListener {
|
||||||
|
actionLong(this, this.isToggled);
|
||||||
|
return@setOnLongClickListener true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, attr: AttributeSet? = null) : super(context, attr) {
|
||||||
|
inflate(context, R.layout.view_tag_v2, this);
|
||||||
|
root = findViewById(R.id.root);
|
||||||
|
text = findViewById(R.id.text_tag);
|
||||||
|
|
||||||
|
val attrArr = context.obtainStyledAttributes(attr, R.styleable.PillV2, 0, 0);
|
||||||
|
val attrEnabled = attrArr.getBoolean(R.styleable.PillV2_pillV2Enabled, false);
|
||||||
|
val attrText = attrArr.getText(R.styleable.PillV2_pillV2Text) ?: "";
|
||||||
|
text.text = attrText;
|
||||||
|
setIsEnabled(attrEnabled);
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
setIsEnabled(!isToggled);
|
||||||
|
onClick.emit(isToggled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun setText(text: String) {
|
||||||
|
this.text.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsEnabled(enabled: Boolean = true) {
|
||||||
|
if(enabled)
|
||||||
|
root.setBackgroundResource(R.drawable.background_2e_round_4dp)
|
||||||
|
else
|
||||||
|
root.setBackgroundResource(R.drawable.background_black_2e_round_4dp);
|
||||||
|
this.isToggled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package com.futo.platformplayer.views.adapters.viewholders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumTileViewHolder(val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<Album>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(
|
||||||
|
R.layout.list_album_tile,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<Album?>();
|
||||||
|
|
||||||
|
protected var _root: ConstraintLayout;
|
||||||
|
protected var _album: Album? = null;
|
||||||
|
protected val _imageThumbnail: ImageView
|
||||||
|
protected val _textName: TextView
|
||||||
|
protected val _textMetadata: TextView
|
||||||
|
|
||||||
|
init {
|
||||||
|
_root = _view.findViewById(R.id.root);
|
||||||
|
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
|
||||||
|
_textName = _view.findViewById(R.id.text_name);
|
||||||
|
_textMetadata = _view.findViewById(R.id.text_metadata);
|
||||||
|
|
||||||
|
_view.setOnClickListener { onClick.emit(_album) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWidth(dp: Int) {
|
||||||
|
_root.updateLayoutParams {
|
||||||
|
this.width = (dp - 12).dp(_viewGroup.context.resources);
|
||||||
|
this.height = (dp + 48).dp(_viewGroup.context.resources);
|
||||||
|
}
|
||||||
|
_imageThumbnail.updateLayoutParams {
|
||||||
|
this.width = (dp - 12).dp(_viewGroup.context.resources);
|
||||||
|
this.height = (dp - 12).dp(_viewGroup.context.resources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAutoSize(totalWidth: Float) {
|
||||||
|
val viewWidth = 98;
|
||||||
|
val dpWidth = totalWidth;
|
||||||
|
val columns = Math.max(((dpWidth) / viewWidth).toInt(), 1);
|
||||||
|
val remainder = dpWidth - columns * viewWidth;
|
||||||
|
val targetSize = viewWidth + (remainder / columns).toInt();
|
||||||
|
setWidth(targetSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(album: Album) {
|
||||||
|
_album = album;
|
||||||
|
_imageThumbnail?.let {
|
||||||
|
if (album.thumbnail != null)
|
||||||
|
Glide.with(it)
|
||||||
|
.load(album.thumbnail)
|
||||||
|
.placeholder(R.drawable.unknown_music)
|
||||||
|
.into(it)
|
||||||
|
else
|
||||||
|
Glide.with(it).load(R.drawable.unknown_music).into(it);
|
||||||
|
};
|
||||||
|
|
||||||
|
_textName.text = album.name;
|
||||||
|
_textMetadata.text = album.artist ?: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getAutoSizeColumns(totalWidth: Float): Int {
|
||||||
|
val viewWidth = 98;
|
||||||
|
val dpWidth = totalWidth;
|
||||||
|
val columns = Math.max(((dpWidth) / viewWidth).toInt(), 1);
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
package com.futo.platformplayer.views.adapters.viewholders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistTileViewHolder(val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<Artist>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(
|
||||||
|
R.layout.list_artist_tile,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<Artist?>();
|
||||||
|
|
||||||
|
protected var _artist: Artist? = null;
|
||||||
|
protected val _imageThumbnail: ImageView
|
||||||
|
protected val _textName: TextView
|
||||||
|
protected val _textMetadata: TextView
|
||||||
|
|
||||||
|
init {
|
||||||
|
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
|
||||||
|
_textName = _view.findViewById(R.id.text_name);
|
||||||
|
_textMetadata = _view.findViewById(R.id.text_metadata);
|
||||||
|
|
||||||
|
_view.setOnClickListener { onClick.emit(_artist) };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun bind(artist: Artist) {
|
||||||
|
_artist = artist;
|
||||||
|
_imageThumbnail?.let {
|
||||||
|
if (artist.thumbnail != null)
|
||||||
|
Glide.with(it)
|
||||||
|
.load(artist.thumbnail)
|
||||||
|
.placeholder(R.drawable.unknown_music)
|
||||||
|
.into(it)
|
||||||
|
else
|
||||||
|
Glide.with(it).load(R.drawable.unknown_music).into(it);
|
||||||
|
};
|
||||||
|
|
||||||
|
_textName.text = artist.name;
|
||||||
|
_textMetadata.text = ""// "${artist.countTracks} tracks";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package com.futo.platformplayer.views.adapters.viewholders
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class FileViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<FileEntry>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(
|
||||||
|
R.layout.list_file,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<FileEntry?>();
|
||||||
|
val onDelete = Event1<FileEntry?>();
|
||||||
|
|
||||||
|
protected var _file: FileEntry? = null;
|
||||||
|
protected val _imageThumbnail: ImageView
|
||||||
|
protected val _buttonDelete: ImageButton;
|
||||||
|
protected val _textName: TextView
|
||||||
|
//protected val _textMetadata: TextView
|
||||||
|
|
||||||
|
init {
|
||||||
|
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
|
||||||
|
_textName = _view.findViewById(R.id.text_name);
|
||||||
|
//_textMetadata = _view.findViewById(R.id.text_metadata);
|
||||||
|
_buttonDelete = _view.findViewById(R.id.button_delete);
|
||||||
|
|
||||||
|
_view.setOnClickListener { onClick.emit(_file) };
|
||||||
|
_buttonDelete.setOnClickListener { onDelete.emit(_file) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun bind(file: FileEntry) {
|
||||||
|
_file = file;
|
||||||
|
_imageThumbnail?.let {
|
||||||
|
if(file.isDirectory)
|
||||||
|
it.setImageResource(R.drawable.ic_library);
|
||||||
|
else {
|
||||||
|
Glide.with(it)
|
||||||
|
.load(file.thumbnail)
|
||||||
|
.placeholder(R.drawable.ic_music)
|
||||||
|
.into(it)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_buttonDelete.isVisible = file.removable;
|
||||||
|
|
||||||
|
_textName.text = file.name;
|
||||||
|
//if(file.isDirectory)
|
||||||
|
// _textMetadata.text = "Directory";
|
||||||
|
//else
|
||||||
|
// _textMetadata.text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
package com.futo.platformplayer.views.adapters.viewholders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.toHumanNowDiffString
|
||||||
|
import com.futo.platformplayer.toHumanTime
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
|
||||||
|
|
||||||
|
class LocalVideoTileViewHolder(val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<IPlatformVideo>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(
|
||||||
|
R.layout.list_video_thumbnail_tile,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<IPlatformVideo?>();
|
||||||
|
|
||||||
|
protected var _content: IPlatformVideo? = null;
|
||||||
|
protected val _imageThumbnail: ShapeableImageView
|
||||||
|
protected val _textDuration: TextView;
|
||||||
|
protected val _textName: TextView
|
||||||
|
protected val _textMetadata: TextView
|
||||||
|
|
||||||
|
init {
|
||||||
|
_imageThumbnail = _view.findViewById(R.id.image_video_thumbnail);
|
||||||
|
_textDuration = _view.findViewById(R.id.thumbnail_duration);
|
||||||
|
_textName = _view.findViewById(R.id.text_video_name);
|
||||||
|
_textMetadata = _view.findViewById(R.id.text_video_metadata);
|
||||||
|
|
||||||
|
_view.setOnClickListener { onClick.emit(_content) };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun bind(content: IPlatformVideo) {
|
||||||
|
_content = content;
|
||||||
|
_imageThumbnail?.let {
|
||||||
|
if (content.thumbnails.getHQThumbnail() != null)
|
||||||
|
Glide.with(it)
|
||||||
|
.load(content.thumbnails.getHQThumbnail())
|
||||||
|
.placeholder(R.drawable.unknown_music)
|
||||||
|
.into(it)
|
||||||
|
else
|
||||||
|
Glide.with(it).load(R.drawable.unknown_music).into(it);
|
||||||
|
};
|
||||||
|
|
||||||
|
_textName.text = content.name;
|
||||||
|
_textMetadata.text = content.datetime?.toHumanNowDiffString();
|
||||||
|
_textDuration.text = content.duration.toHumanTime(false) + " ago";
|
||||||
|
_textDuration.isVisible = content.duration > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
package com.futo.platformplayer.views.adapters.viewholders
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.toHumanDuration
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class TrackViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<IPlatformContent>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(
|
||||||
|
R.layout.list_track,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<IPlatformContent>();
|
||||||
|
val onOptions = Event1<IPlatformContent>();
|
||||||
|
|
||||||
|
protected var _content: IPlatformContent? = null;
|
||||||
|
//protected val _imageThumbnail: ImageView
|
||||||
|
protected val _textName: TextView
|
||||||
|
protected val _textMetadata: TextView
|
||||||
|
|
||||||
|
protected val _imageSettings: ImageView;
|
||||||
|
|
||||||
|
init {
|
||||||
|
//_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
|
||||||
|
_textName = _view.findViewById(R.id.text_name);
|
||||||
|
_textMetadata = _view.findViewById(R.id.text_metadata);
|
||||||
|
_imageSettings = _view.findViewById(R.id.button_options);
|
||||||
|
|
||||||
|
_view.setOnClickListener { _content?.let { onClick.emit(it) } };
|
||||||
|
_imageSettings.setOnClickListener { _content?.let { onOptions.emit(it) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(content: IPlatformContent) {
|
||||||
|
_content = content;
|
||||||
|
/*
|
||||||
|
_imageThumbnail?.let {
|
||||||
|
if (artist.thumbnail != null)
|
||||||
|
Glide.with(it)
|
||||||
|
.load(artist.thumbnail)
|
||||||
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(it)
|
||||||
|
else
|
||||||
|
Glide.with(it).load(R.drawable.placeholder_channel_thumbnail).into(it);
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
_textName.text = content.name;
|
||||||
|
|
||||||
|
val metaComps = listOf(
|
||||||
|
if(content is IPlatformVideo) "${content.duration.toHumanDuration(false)}" else null
|
||||||
|
).filterNotNull();
|
||||||
|
|
||||||
|
_textMetadata.text = metaComps.joinToString(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
|||||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
|
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
|
||||||
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback
|
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback
|
||||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||||
|
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker
|
||||||
|
import androidx.media3.exoplayer.source.BehindLiveWindowException
|
||||||
import androidx.media3.exoplayer.source.MediaSource
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||||
@@ -77,6 +79,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|||||||
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
||||||
|
import com.futo.platformplayer.getNowDiffMiliseconds
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -1008,7 +1011,19 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
protected open fun onPlayerError(error: PlaybackException) {
|
protected open fun onPlayerError(error: PlaybackException) {
|
||||||
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
|
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss, cause=${error.cause}");
|
||||||
|
|
||||||
|
if(error is BehindLiveWindowException) {
|
||||||
|
Logger.e(TAG, "BehindLiveWindowException, " + error.message);
|
||||||
|
reloadMediaSource(true, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(error != null && error.cause is HlsPlaylistTracker.PlaylistStuckException) {
|
||||||
|
Logger.e(TAG, "PlaylistStuckException");
|
||||||
|
reloadMediaSource(true, true);
|
||||||
|
UIDialogs.toast("Live playback error, reloading..");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
when (error.errorCode) {
|
when (error.errorCode) {
|
||||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
||||||
|
|||||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#2e2e2e" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#2e2e2e" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#000000" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#2e2e2e" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="428dp"
|
||||||
|
android:height="230dp"
|
||||||
|
android:viewportWidth="428"
|
||||||
|
android:viewportHeight="201">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h428v201h-428z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="214"
|
||||||
|
android:startY="0"
|
||||||
|
android:endX="214"
|
||||||
|
android:endY="201"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#AA0A0A0A"/>
|
||||||
|
<item android:offset="1" android:color="#FF000000"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</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="M480,640Q546.92,640 593.46,593.46Q640,546.92 640,480Q640,413.08 593.46,366.54Q546.92,320 480,320Q413.08,320 366.54,366.54Q320,413.08 320,480Q320,546.92 366.54,593.46Q413.08,640 480,640ZM480,520Q463,520 451.5,508.5Q440,497 440,480Q440,463 451.5,451.5Q463,440 480,440Q497,440 508.5,451.5Q520,463 520,480Q520,497 508.5,508.5Q497,520 480,520ZM480.07,860Q401.23,860 331.86,830.08Q262.49,800.16 211.18,748.87Q159.87,697.58 129.93,628.24Q100,558.9 100,480.07Q100,401.23 129.92,331.86Q159.84,262.49 211.13,211.18Q262.42,159.87 331.76,129.93Q401.1,100 479.93,100Q558.77,100 628.14,129.92Q697.51,159.84 748.82,211.13Q800.13,262.42 830.07,331.76Q860,401.1 860,479.93Q860,558.77 830.08,628.14Q800.16,697.51 748.87,748.82Q697.58,800.13 628.24,830.07Q558.9,860 480.07,860ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M450,780L450,294.92L222.15,522.77L180,480L480,180L780,480L737.85,522.77L510,294.92L510,780L450,780Z"/>
|
||||||
|
</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="M493.85,593.85Q531.61,593.85 557.73,567.73Q583.84,541.62 583.84,503.85L583.84,279.23L698.46,279.23L698.46,208.46L548.46,208.46L548.46,434.62Q537,424.23 523.35,419.04Q509.69,413.85 493.85,413.85Q456.08,413.85 429.96,439.96Q403.85,466.08 403.85,503.85Q403.85,541.62 429.96,567.73Q456.08,593.85 493.85,593.85ZM322.31,700Q292,700 271,679Q250,658 250,627.69L250,172.31Q250,142 271,121Q292,100 322.31,100L777.69,100Q808,100 829,121Q850,142 850,172.31L850,627.69Q850,658 829,679Q808,700 777.69,700L322.31,700ZM322.31,640L777.69,640Q782.31,640 786.15,636.15Q790,632.31 790,627.69L790,172.31Q790,167.69 786.15,163.85Q782.31,160 777.69,160L322.31,160Q317.69,160 313.85,163.85Q310,167.69 310,172.31L310,627.69Q310,632.31 313.85,636.15Q317.69,640 322.31,640ZM182.31,840Q152,840 131,819Q110,798 110,767.69L110,252.31L170,252.31L170,767.69Q170,772.31 173.85,776.15Q177.69,780 182.31,780L697.69,780L697.69,840L182.31,840ZM310,160L310,160Q310,160 310,163.46Q310,166.92 310,172.31L310,627.69Q310,633.08 310,636.54Q310,640 310,640L310,640Q310,640 310,636.54Q310,633.08 310,627.69L310,172.31Q310,166.92 310,163.46Q310,160 310,160Z"/>
|
||||||
|
</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="M460,561.54L711.54,400L460,238.46L460,561.54ZM322.31,700Q292,700 271,679Q250,658 250,627.69L250,172.31Q250,142 271,121Q292,100 322.31,100L777.69,100Q808,100 829,121Q850,142 850,172.31L850,627.69Q850,658 829,679Q808,700 777.69,700L322.31,700ZM322.31,640L777.69,640Q782.31,640 786.15,636.15Q790,632.31 790,627.69L790,172.31Q790,167.69 786.15,163.85Q782.31,160 777.69,160L322.31,160Q317.69,160 313.85,163.85Q310,167.69 310,172.31L310,627.69Q310,632.31 313.85,636.15Q317.69,640 322.31,640ZM182.31,840Q152,840 131,819Q110,798 110,767.69L110,252.31L170,252.31L170,767.69Q170,772.31 173.85,776.15Q177.69,780 182.31,780L697.69,780L697.69,840L182.31,840ZM310,160L310,160Q310,160 310,163.46Q310,166.92 310,172.31L310,627.69Q310,633.08 310,636.54Q310,640 310,640L310,640Q310,640 310,636.54Q310,633.08 310,627.69L310,172.31Q310,166.92 310,163.46Q310,160 310,160Z"/>
|
||||||
|
</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="M181.92,780Q151.62,780 130.62,759Q109.62,738 109.62,707.69L109.62,252.31Q109.62,222 130.62,201Q151.62,180 181.92,180L637.31,180Q667.61,180 688.61,201Q709.61,222 709.61,252.31L709.61,435.39L850.38,294.62L850.38,665.38L709.61,524.61L709.61,707.69Q709.61,738 688.61,759Q667.61,780 637.31,780L181.92,780ZM181.92,720L637.31,720Q642.69,720 646.15,716.54Q649.62,713.08 649.62,707.69L649.62,252.31Q649.62,246.92 646.15,243.46Q642.69,240 637.31,240L181.92,240Q176.54,240 173.08,243.46Q169.62,246.92 169.62,252.31L169.62,707.69Q169.62,713.08 173.08,716.54Q176.54,720 181.92,720ZM169.62,720Q169.62,720 169.62,716.54Q169.62,713.08 169.62,707.69L169.62,252.31Q169.62,246.92 169.62,243.46Q169.62,240 169.62,240L169.62,240Q169.62,240 169.62,243.46Q169.62,246.92 169.62,252.31L169.62,707.69Q169.62,713.08 169.62,716.54Q169.62,720 169.62,720Z"/>
|
||||||
|
</vector>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/container_topbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
android:background="@color/black">
|
android:background="@color/black">
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:id="@+id/container_topbar"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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="match_parent">
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
android:id="@+id/channel_coordinator"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/app_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="170dp"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
app:elevation="0dp">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
android:id="@+id/toolbar_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:contentScrim="@color/transparent"
|
||||||
|
app:layout_scrollFlags="scroll|exitUntilCollapsed"
|
||||||
|
app:toolbarId="@+id/toolbar">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginBottom="40dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/image_channel_banner"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/channel_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:background="@color/overlay">
|
||||||
|
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
android:id="@+id/creator_thumbnail"
|
||||||
|
android:background="@drawable/rounded_outline"
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="35dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_channel_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="CHANNEL NAME"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/creator_thumbnail"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/text_metadata"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_sub_settings" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/gray_ac"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="17 videos"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/creator_thumbnail"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_sub_settings"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_sub_settings"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:contentDescription="@string/cd_button_settings"
|
||||||
|
android:layout_marginTop="3dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/button_subscribe"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_subscribe"
|
||||||
|
android:src="@drawable/ic_settings" />
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
|
android:id="@+id/button_subscribe"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_button_subscribe"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:minHeight="0dp"
|
||||||
|
app:contentInsetStart="0dp"
|
||||||
|
app:contentInsetEnd="0dp"
|
||||||
|
app:layout_collapseMode="pin"
|
||||||
|
android:layout_gravity="bottom">
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tabs"
|
||||||
|
app:tabMode="scrollable"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:tabSelectedTextColor="@color/white"
|
||||||
|
app:tabTextColor="@color/gray_8c"
|
||||||
|
android:background="@drawable/tab_border"
|
||||||
|
app:tabIndicatorColor="@color/white"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:textColor="@color/gray_8c"
|
||||||
|
android:fontFamily="@font/inter_medium"
|
||||||
|
app:tabTextAppearance="@style/Theme.FutoVideo.TextAppearance.TabLayout" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
|
android:id="@+id/view_pager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/channel_loading_overlay"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:background="#77000000"
|
||||||
|
android:gravity="center">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/channel_loader_frag"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
app:srcCompat="@drawable/ic_loader_animated"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:alpha="0.7"
|
||||||
|
android:contentDescription="@string/loading" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/overlay_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
|
android:id="@+id/feed_root"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="47dp"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_back"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
app:srcCompat="@drawable/ic_back_nav" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
tools:text="FUTO"
|
||||||
|
android:maxLines="2" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/menu_buttons"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:fitsSystemWindows="false"
|
||||||
|
android:background="@drawable/bottom_menu_border"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/filter_top"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/filter_top"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:fitsSystemWindows="false"
|
||||||
|
android:background="@drawable/bottom_menu_border"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true">
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
<!--
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<com.futo.platformplayer.views.LibrarySection
|
||||||
|
android:id="@+id/section_artists"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_height="165dp" />
|
||||||
|
<com.futo.platformplayer.views.LibrarySection
|
||||||
|
android:id="@+id/section_albums"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="190dp" />
|
||||||
|
<com.futo.platformplayer.views.LibrarySection
|
||||||
|
android:id="@+id/section_videos"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:layout_height="180dp" />
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:id="@+id/button_albums"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:buttonIcon="@drawable/ic_album"
|
||||||
|
app:buttonSubText="All albums known on this phone"
|
||||||
|
app:buttonText="Albums"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:layout_marginBottom="8dp" /> -->
|
||||||
|
<!--
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:id="@+id/button_playlists"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:buttonIcon="@drawable/ic_playlist"
|
||||||
|
app:buttonText="Playlists"
|
||||||
|
app:buttonSubText="All playlists in Grayjay"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:id="@+id/button_videos"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:buttonIcon="@drawable/ic_videocam"
|
||||||
|
app:buttonText="Videos"
|
||||||
|
app:buttonSubText="All local videos on this phone"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:layout_marginBottom="8dp" /> -->
|
||||||
|
<!--
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:id="@+id/button_files"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:buttonIcon="@drawable/ic_gallery"
|
||||||
|
app:buttonText="Files"
|
||||||
|
app:buttonSubText="Browse files on your phone"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:text="Library UI is temporary, and will be replaced"
|
||||||
|
android:textColor="#FFAA00" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/meta_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:text=""
|
||||||
|
android:textColor="#AAAAAA" />
|
||||||
|
</LinearLayout>
|
||||||
|
-->
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:fitsSystemWindows="false"
|
||||||
|
android:background="@drawable/bottom_menu_border"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/container_tags"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<com.futo.platformplayer.views.PillV2
|
||||||
|
android:id="@+id/pill_songs"
|
||||||
|
app:pillV2Text="Songs"
|
||||||
|
android:layout_margin="3dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<com.futo.platformplayer.views.PillV2
|
||||||
|
android:id="@+id/pill_artist"
|
||||||
|
app:pillV2Text="Artist"
|
||||||
|
android:layout_margin="3dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<com.futo.platformplayer.views.PillV2
|
||||||
|
android:id="@+id/pill_albums"
|
||||||
|
app:pillV2Text="Albums"
|
||||||
|
android:layout_margin="3dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginTop="3dp"
|
||||||
|
android:layout_marginBottom="3dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="#D0D0D0"
|
||||||
|
android:text="0 artists"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/container_tags"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/container_tags"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_metadata"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/image_thumbnail"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:shapeAppearanceOverlay="@style/roundedCorners_4dp"
|
||||||
|
app:srcCompat="@drawable/placeholder_video_thumbnail"
|
||||||
|
android:background="@drawable/video_thumbnail_outline"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="13dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
tools:text="Legendary grant recipient: Marvin Wißfeld Very Long Title That is Long"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toLeftOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/text_metadata"
|
||||||
|
android:layout_marginStart="15dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="10dp"
|
||||||
|
android:textColor="@color/gray_e0"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
tools:text="3 videos"
|
||||||
|
android:maxLines="1"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||||
|
app:layout_constraintRight_toLeftOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginStart="15dp" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_play"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
app:srcCompat="@drawable/ic_play_white_nopad"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginEnd="10dp"/> -->
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="86dp"
|
||||||
|
android:layout_height="146dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
android:layout_marginLeft="6dp"
|
||||||
|
android:layout_marginRight="6dp"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/image_thumbnail"
|
||||||
|
android:layout_height="86dp"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintDimensionRatio="H,1,1"
|
||||||
|
app:shapeAppearanceOverlay="@style/roundedCorners_4dp"
|
||||||
|
app:srcCompat="@drawable/unknown_music"
|
||||||
|
android:background="@drawable/video_thumbnail_outline"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_medium"
|
||||||
|
tools:text="The Beetles"
|
||||||
|
android:maxLines="2"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/image_thumbnail"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/image_thumbnail"
|
||||||
|
app:layout_constraintRight_toRightOf="@id/image_thumbnail"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#75FFFFFF"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
tools:text="3 videos"
|
||||||
|
android:maxLines="2"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/text_name"
|
||||||
|
app:layout_constraintRight_toRightOf="@id/text_name" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_play"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
app:srcCompat="@drawable/ic_play_white_nopad"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginEnd="10dp"/> -->
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:layout_marginBottom="0dp"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true"
|
||||||
|
android:paddingLeft="10dp"
|
||||||
|
android:paddingRight="10dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="13dp"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
tools:text="Example Artist"
|
||||||
|
android:maxLines="1"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/text_metadata"
|
||||||
|
android:layout_marginStart="10dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="10dp"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
android:textColor="@color/gray_e0"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
tools:text="3 videos"
|
||||||
|
android:maxLines="1"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginStart="10dp" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_play"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
app:srcCompat="@drawable/ic_play_white_nopad"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginEnd="10dp"/> -->
|
||||||
|
|
||||||
|
<View
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1px"
|
||||||
|
android:background="#181818" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="71dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
android:layout_marginLeft="6dp"
|
||||||
|
android:layout_marginRight="6dp"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/image_thumbnail"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:shapeAppearanceOverlay="@style/roundedCorners_25dp"
|
||||||
|
app:srcCompat="@drawable/unknown_music"
|
||||||
|
android:background="@drawable/video_thumbnail_outline"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_medium"
|
||||||
|
android:textAlignment="center"
|
||||||
|
tools:text="The Beetles"
|
||||||
|
android:maxLines="2"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/image_thumbnail"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textColor="#75FFFFFF"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textAlignment="center"
|
||||||
|
tools:text="3 videos"
|
||||||
|
android:maxLines="2"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/text_name"
|
||||||
|
app:layout_constraintRight_toRightOf="@id/text_name" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_play"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
app:srcCompat="@drawable/ic_play_white_nopad"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginEnd="10dp"/> -->
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="68dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:background="@drawable/background_16_round_4dp"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/image_thumbnail_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:background="@drawable/background_1b_round_6dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_thumbnail"
|
||||||
|
android:alpha="0.4"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="15dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
tools:text="Legendary grant recipient: Marvin Wißfeld Very Long Title That is Long"
|
||||||
|
android:maxLines="2"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/image_thumbnail_container"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_delete"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="10dp"
|
||||||
|
android:textColor="@color/gray_e0"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
tools:text="3 videos"
|
||||||
|
android:maxLines="1"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_delete"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginStart="12dp" />-->
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_delete"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
app:srcCompat="@drawable/ic_trash"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginEnd="10dp"/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
|
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintBottom_toTopOf="@id/text_metadata"
|
app:layout_constraintBottom_toTopOf="@id/text_metadata"
|
||||||
android:layout_marginStart="10dp" />
|
android:layout_marginStart="10dp" />
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_name"
|
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||||
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
|
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
|
||||||
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:layout_marginStart="10dp" />
|
android:layout_marginStart="10dp" />
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:layout_marginEnd="10dp"/> -->
|
android:layout_marginEnd="10dp"/> -->
|
||||||
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_trash"
|
android:id="@+id/button_trash"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:layout_marginBottom="0dp"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true"
|
||||||
|
android:paddingLeft="10dp"
|
||||||
|
android:paddingRight="10dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="13dp"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
tools:text="Example Artist"
|
||||||
|
android:maxLines="1"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_options"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/text_metadata"
|
||||||
|
android:layout_marginStart="10dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="10dp"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:textColor="#595959"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
tools:text="3 videos"
|
||||||
|
android:maxLines="1"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_options"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginStart="10dp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/button_options"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:src="@drawable/ic_settings"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_play"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
app:srcCompat="@drawable/ic_play_white_nopad"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginEnd="10dp"/> -->
|
||||||
|
|
||||||
|
<View
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1px"
|
||||||
|
android:background="#181818" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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="120dp"
|
||||||
|
android:layout_height="160dp"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_marginRight="5dp"
|
||||||
|
android:layout_marginTop="3dp"
|
||||||
|
android:layout_marginBottom="3dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/player_container"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="66dp">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/image_video_thumbnail"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="66dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/thumbnail"
|
||||||
|
app:shapeAppearanceOverlay="@style/roundedCorners_4dp"
|
||||||
|
app:srcCompat="@drawable/placeholder_video_thumbnail"
|
||||||
|
android:background="@drawable/video_thumbnail_outline" />
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/thumbnail_duration_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:paddingStart="2dp"
|
||||||
|
android:paddingEnd="2dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:paddingTop="0dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="@drawable/background_thumbnail_duration">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/thumbnail_duration"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:paddingLeft="2dp"
|
||||||
|
android:paddingRight="2dp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="12dp"
|
||||||
|
tools:text="0:00"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:textStyle="normal" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/player_container"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/player_container"
|
||||||
|
app:layout_constraintRight_toRightOf="@id/player_container"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginEnd="6dp">
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_video_name"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="11dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
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" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_video_metadata"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="10dp"
|
||||||
|
android:textColor="@color/gray_e0"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
tools:text="57K views • 1 day ago"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/text_video_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_video_name"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:layout_marginStart="0dp"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/root">
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/app_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:elevation="0dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="0dp"
|
||||||
|
app:layout_scrollFlags="scroll"
|
||||||
|
app:contentInsetStart="0dp"
|
||||||
|
app:contentInsetEnd="0dp">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="230dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_thumbnail_background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:srcCompat="@drawable/background_thumbnail_live"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:srcCompat="@drawable/bottom_gradient_dark"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="-220dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_thumbnail"
|
||||||
|
android:layout_width="108dp"
|
||||||
|
android:layout_height="108dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
app:srcCompat="@drawable/background_thumbnail_live"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_medium"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="18dp"
|
||||||
|
tools:text="Playlist name"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/image_thumbnail"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/container_buttons"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/text_metadata"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
android:textColor="#595959"
|
||||||
|
android:textSize="14dp"
|
||||||
|
tools:text="15 tracks - 1h 15 minutes"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/container_buttons"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/container_buttons"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_buttons"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_play_all"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="@drawable/background_button_primary_round"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="14dp"
|
||||||
|
android:layout_height="14dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:srcCompat="@drawable/ic_play_white_nopad"
|
||||||
|
android:layout_marginEnd="10dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16dp"
|
||||||
|
android:text="@string/play_all" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_shuffle"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="@drawable/background_button_round"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:srcCompat="@drawable/ic_shuffle"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
app:tint="@color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16dp"
|
||||||
|
android:text="@string/shuffle" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
<com.futo.platformplayer.views.SearchView
|
||||||
|
android:id="@+id/search_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="-10dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/container_buttons"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_label"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:text="Albums" />
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_nav"
|
||||||
|
android:layout_width="25dp"
|
||||||
|
android:layout_height="25dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/text_label"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/text_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
|
||||||
|
android:src="@drawable/ic_arrow_right" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_collection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_label"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:orientation="horizontal" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_play"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
app:srcCompat="@drawable/ic_play_white_nopad"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginEnd="10dp"/> -->
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/root">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_tags"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.PillV2
|
||||||
|
android:id="@+id/pill_artist"
|
||||||
|
app:pillV2Text="Artist"
|
||||||
|
android:layout_margin="3dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<com.futo.platformplayer.views.PillV2
|
||||||
|
android:id="@+id/pill_albums"
|
||||||
|
app:pillV2Text="Albums"
|
||||||
|
android:layout_margin="3dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_metadata"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="3dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="#D0D0D0"
|
||||||
|
android:text="0 artists"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/container_tags"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/container_tags"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginLeft="3dp"
|
||||||
|
android:layout_marginRight="3dp"
|
||||||
|
android:layout_marginTop="3dp"
|
||||||
|
android:layout_marginBottom="3dp"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:background="@drawable/background_black_2e_round_4dp"
|
||||||
|
android:id="@+id/root">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_tag"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="11dp"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
tools:text="Artist" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="PillV2">
|
||||||
|
<attr name="pillV2Enabled" format="boolean" />
|
||||||
|
<attr name="pillV2Text" format="string" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
||||||
@@ -73,6 +73,7 @@
|
|||||||
<string name="share">Share</string>
|
<string name="share">Share</string>
|
||||||
<string name="view_all">View all</string>
|
<string name="view_all">View all</string>
|
||||||
<string name="creators">Creators</string>
|
<string name="creators">Creators</string>
|
||||||
|
<string name="library">Library</string>
|
||||||
<string name="enabled">Enabled</string>
|
<string name="enabled">Enabled</string>
|
||||||
<string name="keep_screen_on">Keep screen on</string>
|
<string name="keep_screen_on">Keep screen on</string>
|
||||||
<string name="keep_screen_on_while_casting">Keep screen on while casting</string>
|
<string name="keep_screen_on_while_casting">Keep screen on while casting</string>
|
||||||
|
|||||||
@@ -23,6 +23,18 @@
|
|||||||
<item name="cornerFamily">rounded</item>
|
<item name="cornerFamily">rounded</item>
|
||||||
<item name="cornerSize">16dp</item>
|
<item name="cornerSize">16dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
<style name="roundedCorners_30dp" parent="">
|
||||||
|
<item name="cornerFamily">rounded</item>
|
||||||
|
<item name="cornerSize">30dp</item>
|
||||||
|
</style>
|
||||||
|
<style name="roundedCorners_33dp" parent="">
|
||||||
|
<item name="cornerFamily">rounded</item>
|
||||||
|
<item name="cornerSize">33dp</item>
|
||||||
|
</style>
|
||||||
|
<style name="roundedCorners_25dp" parent="">
|
||||||
|
<item name="cornerFamily">rounded</item>
|
||||||
|
<item name="cornerSize">25dp</item>
|
||||||
|
</style>
|
||||||
<style name="roundedCorners_40dp" parent="">
|
<style name="roundedCorners_40dp" parent="">
|
||||||
<item name="cornerFamily">rounded</item>
|
<item name="cornerFamily">rounded</item>
|
||||||
<item name="cornerSize">40dp</item>
|
<item name="cornerSize">40dp</item>
|
||||||
|
|||||||
Submodule app/src/stable/assets/sources/apple-podcasts updated: 8cff240ca7...9c65475be1
Submodule app/src/stable/assets/sources/bilibili updated: f636e9713d...17d7aef314
Submodule app/src/stable/assets/sources/dailymotion updated: 850eb8122d...d95df7dca2
Submodule app/src/stable/assets/sources/kick updated: 4ff0b02700...9b3c7ea213
Submodule app/src/stable/assets/sources/rumble updated: 3a7087ccb0...2864a541e6
Submodule app/src/stable/assets/sources/twitch updated: 8de3ab18f5...e4cdb5a32e
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user