mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 11:35:46 +00:00
Language setting, Preview setting, Background audio switch setting, No error on comments failed
This commit is contained in:
parent
d7f4dd65e8
commit
ba64153f1d
19 changed files with 144 additions and 31 deletions
|
@ -4,9 +4,7 @@ import android.content.ActivityNotFoundException
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.webkit.CookieManager
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.activities.*
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
|
@ -30,6 +28,7 @@ import kotlinx.serialization.*
|
|||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||
|
@ -46,7 +45,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
|
||||
@FormField(
|
||||
R.string.manage_polycentric_identity, FieldForm.BUTTON,
|
||||
R.string.manage_your_polycentric_identity, -4
|
||||
R.string.manage_your_polycentric_identity, -5
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_person)
|
||||
fun managePolycentricIdentity() {
|
||||
|
@ -61,7 +60,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
|
||||
@FormField(
|
||||
R.string.show_faq, FieldForm.BUTTON,
|
||||
R.string.get_answers_to_common_questions, -3
|
||||
R.string.get_answers_to_common_questions, -4
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_quiz)
|
||||
fun openFAQ() {
|
||||
|
@ -74,7 +73,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
}
|
||||
@FormField(
|
||||
R.string.show_issues, FieldForm.BUTTON,
|
||||
R.string.a_list_of_user_reported_and_self_reported_issues, -2
|
||||
R.string.a_list_of_user_reported_and_self_reported_issues, -3
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_data_alert)
|
||||
fun openIssues() {
|
||||
|
@ -109,7 +108,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
|
||||
@FormField(
|
||||
R.string.manage_tabs, FieldForm.BUTTON,
|
||||
R.string.change_tabs_visible_on_the_home_screen, -1
|
||||
R.string.change_tabs_visible_on_the_home_screen, -2
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_tabs)
|
||||
fun manageTabs() {
|
||||
|
@ -122,11 +121,39 @@ class Settings : FragmentedStorageFileJson() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 0)
|
||||
var language = LanguageSettings();
|
||||
@Serializable
|
||||
class LanguageSettings {
|
||||
@FormField(R.string.app_language, FieldForm.DROPDOWN, R.string.may_require_restart, 5, "app_language")
|
||||
@DropdownFieldOptionsId(R.array.app_languages)
|
||||
var appLanguage: Int = 0;
|
||||
|
||||
fun getAppLanguageLocaleString(): String? {
|
||||
return when(appLanguage) {
|
||||
0 -> null
|
||||
1 -> "en";
|
||||
2 -> "de";
|
||||
3 -> "es";
|
||||
4 -> "pt";
|
||||
5 -> "fr"
|
||||
6 -> "ja";
|
||||
7 -> "ko";
|
||||
8 -> "zh";
|
||||
9 -> "ru";
|
||||
10 -> "ar";
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 1)
|
||||
var home = HomeSettings();
|
||||
@Serializable
|
||||
class HomeSettings {
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 5)
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
|
||||
@DropdownFieldOptionsId(R.array.feed_style)
|
||||
var homeFeedStyle: Int = 1;
|
||||
|
||||
|
@ -136,21 +163,28 @@ class Settings : FragmentedStorageFileJson() {
|
|||
else
|
||||
return FeedStyle.THUMBNAIL;
|
||||
}
|
||||
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||
var previewFeedItems: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.search, "group", -1, 2)
|
||||
var search = SearchSettings();
|
||||
@Serializable
|
||||
class SearchSettings {
|
||||
@FormField(R.string.search_history, FieldForm.TOGGLE, -1, 4)
|
||||
@FormField(R.string.search_history, FieldForm.TOGGLE, R.string.may_require_restart, 3)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var searchHistory: Boolean = true;
|
||||
|
||||
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 5)
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 4)
|
||||
@DropdownFieldOptionsId(R.array.feed_style)
|
||||
var searchFeedStyle: Int = 1;
|
||||
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||
var previewFeedItems: Boolean = true;
|
||||
|
||||
|
||||
|
||||
fun getSearchFeedStyle(): FeedStyle {
|
||||
if(searchFeedStyle == 0)
|
||||
|
@ -164,7 +198,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
var subscriptions = SubscriptionsSettings();
|
||||
@Serializable
|
||||
class SubscriptionsSettings {
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 5)
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 4)
|
||||
@DropdownFieldOptionsId(R.array.feed_style)
|
||||
var subscriptionsFeedStyle: Int = 1;
|
||||
|
||||
|
@ -175,6 +209,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||
return FeedStyle.THUMBNAIL;
|
||||
}
|
||||
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||
var previewFeedItems: Boolean = true;
|
||||
|
||||
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 6)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var fetchOnAppBoot: Boolean = true;
|
||||
|
@ -208,6 +245,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
|
||||
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 10)
|
||||
var allowPlaytimeTracking: Boolean = true;
|
||||
|
||||
}
|
||||
|
||||
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 4)
|
||||
|
@ -215,10 +253,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||
@Serializable
|
||||
class PlaybackSettings {
|
||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
||||
@DropdownFieldOptionsId(R.array.languages)
|
||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||
var primaryLanguage: Int = 0;
|
||||
|
||||
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.languages)[primaryLanguage];
|
||||
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||
|
||||
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||
|
@ -277,10 +315,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
||||
var resumeAfterPreview: Int = 1;
|
||||
|
||||
|
||||
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 8)
|
||||
var useLiveChatWindow: Boolean = true;
|
||||
|
||||
fun shouldResumePreview(previewedPosition: Long): Boolean{
|
||||
if(resumeAfterPreview == 2)
|
||||
return true;
|
||||
|
@ -288,6 +322,14 @@ class Settings : FragmentedStorageFileJson() {
|
|||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 8)
|
||||
var useLiveChatWindow: Boolean = true;
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 8)
|
||||
var backgroundSwitchToAudio: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 5)
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.content.pm.ActivityInfo
|
|||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
|
@ -154,6 +155,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
Logger.i(TAG, "MainActivity.attachBaseContext")
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||
StateApp.instance.mainAppStarting(this);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
|
@ -13,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.Loader
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||
|
@ -28,6 +30,10 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||
|
||||
private var _isFinished = false;
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
@ -43,6 +49,11 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||
_form.setObjectValues();
|
||||
Settings.instance.save();
|
||||
|
||||
if(field.descriptor?.id == "app_language") {
|
||||
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
||||
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
|
||||
}
|
||||
};
|
||||
_buttonBack.setOnClickListener {
|
||||
finish();
|
||||
|
|
|
@ -63,7 +63,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||
}
|
||||
|
||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||
_view?.setPreviewsEnabled(previewsEnabled);
|
||||
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.search.previewFeedItems);
|
||||
}
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
|
@ -93,6 +93,8 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||
Logger.w(TAG, "Failed to load results.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||
}
|
||||
|
||||
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
|
|
|
@ -78,7 +78,7 @@ class HomeFragment : MainFragment() {
|
|||
}
|
||||
|
||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||
_view?.setPreviewsEnabled(previewsEnabled);
|
||||
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems);
|
||||
}
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
|
@ -122,6 +122,8 @@ class HomeFragment : MainFragment() {
|
|||
setLoading(false);
|
||||
};
|
||||
};
|
||||
|
||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||
}
|
||||
|
||||
fun onShown() {
|
||||
|
|
|
@ -81,7 +81,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||
}
|
||||
|
||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||
_view?.setPreviewsEnabled(previewsEnabled);
|
||||
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.subscriptions.previewFeedItems);
|
||||
}
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
|
@ -108,6 +108,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||
};
|
||||
|
||||
initializeToolbarContent();
|
||||
|
||||
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
|
||||
}
|
||||
|
||||
fun onShown() {
|
||||
|
|
|
@ -812,7 +812,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
when (Settings.instance.playback.backgroundPlay) {
|
||||
0 -> handlePause();
|
||||
1 -> {
|
||||
if(!(video?.isLive ?: false))
|
||||
if(!(video?.isLive ?: false) && Settings.instance.playback.backgroundSwitchToAudio)
|
||||
_player.switchToAudioMode();
|
||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||
}
|
||||
|
|
|
@ -731,6 +731,34 @@ class StateApp {
|
|||
}
|
||||
}
|
||||
|
||||
fun getLocaleContext(baseContext: Context?): Context? {
|
||||
val locale = getLocaleSetting(baseContext);
|
||||
try {
|
||||
|
||||
if (baseContext != null && locale != null) {
|
||||
val config = baseContext.resources.configuration;
|
||||
config.setLocale(locale);
|
||||
return baseContext.createConfigurationContext(config);
|
||||
}
|
||||
return baseContext;
|
||||
}
|
||||
catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to load locale", ex);
|
||||
return baseContext;
|
||||
}
|
||||
}
|
||||
fun getLocaleSetting(context: Context?): Locale? {
|
||||
return context?.getSharedPreferences("language", Context.MODE_PRIVATE)
|
||||
?.getString("language", null)
|
||||
?.let { Locale(it) };
|
||||
}
|
||||
fun setLocaleSetting(context: Context?, locale: String?) {
|
||||
context?.getSharedPreferences("language", Context.MODE_PRIVATE)
|
||||
?.edit()
|
||||
?.putString("language", locale)
|
||||
?.apply();
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "StateApp";
|
||||
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
|
||||
|
|
|
@ -34,7 +34,8 @@ class CommentsList : ConstraintLayout {
|
|||
}
|
||||
.exception<Throwable> {
|
||||
Logger.e(TAG, "Failed to load comments.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_comments) + (it.message ?: ""), it, ::fetchComments);
|
||||
UIDialogs.toast(context, context.getString(R.string.failed_to_load_comments) + "\n" + (it.message ?: ""));
|
||||
//UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_comments) + (it.message ?: ""), it, ::fetchComments);
|
||||
setLoading(false);
|
||||
} else TaskHandler(IPlatformVideoDetails::class.java, StateApp.instance.scopeGetter);
|
||||
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
<item>استئناف بعد 10 ثوان</item>
|
||||
<item>استئناف دائم</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>الإنجليزية</item>
|
||||
<item>الإسبانية</item>
|
||||
<item>الفرنسية</item>
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
<item>Nach 10 Sekunden fortsetzen</item>
|
||||
<item>Immer fortsetzen</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Englisch</item>
|
||||
<item>Spanisch</item>
|
||||
<item>Französisch</item>
|
||||
|
|
|
@ -713,7 +713,7 @@
|
|||
<item>Reanudar Después de 10s</item>
|
||||
<item>Siempre Reanudar</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Inglés</item>
|
||||
<item>Español</item>
|
||||
<item>Francés</item>
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
<item>Reprendre après 10s</item>
|
||||
<item>Toujours reprendre</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Anglais</item>
|
||||
<item>Espagnol</item>
|
||||
<item>Français</item>
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
<item>10秒後から再開</item>
|
||||
<item>常に再開</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>英語</item>
|
||||
<item>スペイン語</item>
|
||||
<item>フランス語</item>
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
<item>10초 후에 이어서</item>
|
||||
<item>항상 이어서</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>영어</item>
|
||||
<item>스페인어</item>
|
||||
<item>프랑스어</item>
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
<item>Continuar Após 10s</item>
|
||||
<item>Sempre Continuar</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Inglês</item>
|
||||
<item>Espanhol</item>
|
||||
<item>Francês</item>
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
<item>Продолжить после 10 секунд</item>
|
||||
<item>Всегда продолжать</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>Английский</item>
|
||||
<item>Испанский</item>
|
||||
<item>Французский</item>
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
<item>预览后10秒继续</item>
|
||||
<item>始终继续</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>英语</item>
|
||||
<item>西班牙语</item>
|
||||
<item>法语</item>
|
||||
|
|
|
@ -307,11 +307,17 @@
|
|||
<string name="import_data_description">Select a file to import, support various files (alternative to opening directly)</string>
|
||||
<string name="external_storage">External Storage</string>
|
||||
<string name="feed_style">Feed Style</string>
|
||||
<string name="app_language">App Language</string>
|
||||
<string name="may_require_restart">May require restart</string>
|
||||
<string name="fetch_on_app_boot">Fetch on app boot</string>
|
||||
<string name="get_answers_to_common_questions">Get answers to common questions</string>
|
||||
<string name="give_feedback_on_the_application">Give feedback on the application</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="live_chat_webview">Live Chat Webview</string>
|
||||
<string name="background_switch_audio">Switch to Audio in Background</string>
|
||||
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
|
||||
<string name="preview_feed_items">Preview Feed Items</string>
|
||||
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
|
||||
<string name="log_level">Log Level</string>
|
||||
<string name="logging">Logging</string>
|
||||
<string name="manage_polycentric_identity">Manage Polycentric identity</string>
|
||||
|
@ -724,6 +730,19 @@
|
|||
<item>Preview</item>
|
||||
<item>List</item>
|
||||
</string-array>
|
||||
<string-array name="app_languages">
|
||||
<item>System</item>
|
||||
<item>English (EN)</item>
|
||||
<item>German (DE)</item>
|
||||
<item>Spanish (ES)</item>
|
||||
<item>Portuguese (PT)</item>
|
||||
<item>French (FR)</item>
|
||||
<item>Japanese (JA)</item>
|
||||
<item>Korean (KO)</item>
|
||||
<item>Chinese (ZH)</item>
|
||||
<item>Russian (RU)</item>
|
||||
<item>Arabic (AR)</item>
|
||||
</string-array>
|
||||
<string-array name="player_background_behavior">
|
||||
<item>None</item>
|
||||
<item>Keep Playing</item>
|
||||
|
@ -734,7 +753,7 @@
|
|||
<item>Resume After 10s</item>
|
||||
<item>Always Resume</item>
|
||||
</string-array>
|
||||
<string-array name="languages">
|
||||
<string-array name="audio_languages">
|
||||
<item>English</item>
|
||||
<item>Spanish</item>
|
||||
<item>French</item>
|
||||
|
|
Loading…
Add table
Reference in a new issue