mirror of
https://github.com/jellyfin/jellyfin-android.git
synced 2026-09-03 05:10:27 +03:00
Merge pull request #50 from jellyfin/extract-connection-handling
Split up web app and connection handling, use view binding for setup layout
This commit is contained in:
@@ -31,6 +31,10 @@ android {
|
||||
aaptOptions.cruncherEnabled = false // Disable png crunching
|
||||
}
|
||||
}
|
||||
@Suppress("UnstableApiUsage")
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
@@ -7,24 +7,10 @@ import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.webkit.*
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.mobile.bridge.Commands.triggerInputManagerAction
|
||||
import org.jellyfin.mobile.bridge.NativeInterface
|
||||
@@ -32,6 +18,7 @@ import org.jellyfin.mobile.bridge.NativePlayer
|
||||
import org.jellyfin.mobile.cast.Chromecast
|
||||
import org.jellyfin.mobile.utils.*
|
||||
import org.jellyfin.mobile.utils.Constants.INPUT_MANAGER_COMMAND_BACK
|
||||
import org.jellyfin.mobile.webapp.ConnectionHelper
|
||||
import timber.log.Timber
|
||||
import java.io.Reader
|
||||
|
||||
@@ -40,6 +27,10 @@ class WebappActivity : AppCompatActivity(), WebViewController {
|
||||
val appPreferences: AppPreferences by lazy { AppPreferences(this) }
|
||||
val httpClient = OkHttpClient()
|
||||
val chromecast = Chromecast()
|
||||
private val connectionHelper = ConnectionHelper(this)
|
||||
|
||||
val rootView: FrameLayout by lazyView(R.id.root_view)
|
||||
val webView: WebView by lazyView(R.id.web_view)
|
||||
|
||||
var serviceBinder: RemotePlayerService.ServiceBinder? = null
|
||||
private set
|
||||
@@ -54,15 +45,6 @@ class WebappActivity : AppCompatActivity(), WebViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var cachedInstanceUrl: HttpUrl? = null
|
||||
private var connected = false
|
||||
|
||||
private val rootView: FrameLayout by lazyView(R.id.root_view)
|
||||
private val webView: WebView by lazyView(R.id.web_view)
|
||||
private val serverSetupLayout: View by lazy { layoutInflater.inflate(R.layout.connect_server, rootView, false) }
|
||||
private val hostInput: EditText by lazy { serverSetupLayout.findViewById<EditText>(R.id.host_input) }
|
||||
private val connectButton: Button by lazy { serverSetupLayout.findViewById<Button>(R.id.connect_button) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -74,8 +56,7 @@ class WebappActivity : AppCompatActivity(), WebViewController {
|
||||
webView.initialize()
|
||||
|
||||
// Load content
|
||||
cachedInstanceUrl = appPreferences.instanceUrl?.toHttpUrlOrNull()
|
||||
loadOrShowSetup()
|
||||
connectionHelper.initialize()
|
||||
|
||||
chromecast.initializePlugin(this)
|
||||
}
|
||||
@@ -91,16 +72,16 @@ class WebappActivity : AppCompatActivity(), WebViewController {
|
||||
path.endsWith(Constants.INDEX_PATH) -> {
|
||||
val patchedIndex = loadPatchedIndex(httpClient, url.toString())
|
||||
if (patchedIndex != null) {
|
||||
runOnUiThread { onConnectedToWebapp() }
|
||||
runOnUiThread { connectionHelper.onConnectedToWebapp() }
|
||||
patchedIndex
|
||||
} else {
|
||||
runOnUiThread { onErrorReceived() }
|
||||
runOnUiThread { connectionHelper.onErrorReceived() }
|
||||
emptyResponse
|
||||
}
|
||||
}
|
||||
path.contains("native") -> loadAsset("native/${url.lastPathSegment}")
|
||||
path.endsWith("web/selectserver.html") -> {
|
||||
runOnUiThread { onSelectServer() }
|
||||
runOnUiThread { connectionHelper.onSelectServer() }
|
||||
emptyResponse
|
||||
}
|
||||
else -> null
|
||||
@@ -111,7 +92,7 @@ class WebappActivity : AppCompatActivity(), WebViewController {
|
||||
val errorMessage = errorResponse.data?.run { bufferedReader().use(Reader::readText) }
|
||||
Timber.e("Received WebView HTTP %d error: %s", errorResponse.statusCode, errorMessage)
|
||||
if (request.url.path?.endsWith(Constants.INDEX_PATH) != false)
|
||||
runOnUiThread { onErrorReceived() }
|
||||
runOnUiThread { connectionHelper.onErrorReceived() }
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceError) {
|
||||
@@ -127,78 +108,8 @@ class WebappActivity : AppCompatActivity(), WebViewController {
|
||||
addJavascriptInterface(NativePlayer(this@WebappActivity), "NativePlayer")
|
||||
}
|
||||
|
||||
private fun loadOrShowSetup() {
|
||||
cachedInstanceUrl.let { url ->
|
||||
if (url != null) {
|
||||
webView.isVisible = true
|
||||
webView.loadUrl(url.resolve(Constants.INDEX_PATH).toString())
|
||||
} else {
|
||||
webView.isVisible = false
|
||||
showServerSetup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showServerSetup() {
|
||||
rootView.addView(serverSetupLayout)
|
||||
hostInput.setText(appPreferences.instanceUrl)
|
||||
hostInput.setSelection(hostInput.length())
|
||||
hostInput.setOnEditorActionListener { _, action, event ->
|
||||
when {
|
||||
action == EditorInfo.IME_ACTION_DONE || event.keyCode == KeyEvent.KEYCODE_ENTER -> {
|
||||
connect()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
connectButton.setOnClickListener {
|
||||
connect()
|
||||
}
|
||||
|
||||
// Show keyboard
|
||||
serverSetupLayout.doOnNextLayout {
|
||||
hostInput.postDelayed(25) {
|
||||
hostInput.requestFocus()
|
||||
getSystemService<InputMethodManager>()?.showSoftInput(hostInput, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect() {
|
||||
hostInput.isEnabled = false
|
||||
connectButton.isEnabled = false
|
||||
lifecycleScope.launch {
|
||||
val httpUrl = checkServerUrlAndConnection(hostInput.text.toString())
|
||||
if (httpUrl != null) {
|
||||
appPreferences.instanceUrl = httpUrl.toString()
|
||||
cachedInstanceUrl = httpUrl
|
||||
rootView.removeView(serverSetupLayout)
|
||||
loadOrShowSetup()
|
||||
}
|
||||
hostInput.isEnabled = true
|
||||
connectButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun onConnectedToWebapp() {
|
||||
connected = true
|
||||
requestNoBatteryOptimizations()
|
||||
}
|
||||
|
||||
private fun onSelectServer() {
|
||||
cachedInstanceUrl = null
|
||||
loadOrShowSetup()
|
||||
}
|
||||
|
||||
private fun onErrorReceived() {
|
||||
connected = false
|
||||
appPreferences.instanceUrl = null
|
||||
onSelectServer()
|
||||
}
|
||||
|
||||
override fun loadUrl(url: String) {
|
||||
if (connected) webView.loadUrl(url)
|
||||
if (connectionHelper.connected) webView.loadUrl(url)
|
||||
}
|
||||
|
||||
fun updateRemoteVolumeLevel(value: Int) {
|
||||
@@ -207,13 +118,8 @@ class WebappActivity : AppCompatActivity(), WebViewController {
|
||||
|
||||
override fun onBackPressed() {
|
||||
when {
|
||||
!connected -> super.onBackPressed()
|
||||
serverSetupLayout.isAttachedToWindow -> {
|
||||
rootView.removeView(serverSetupLayout)
|
||||
cachedInstanceUrl = appPreferences.instanceUrl?.toHttpUrlOrNull()
|
||||
webView.isVisible = true
|
||||
}
|
||||
else -> triggerInputManagerAction(INPUT_MANAGER_COMMAND_BACK)
|
||||
!connectionHelper.connected -> super.onBackPressed()
|
||||
!connectionHelper.onBackPressed() -> triggerInputManagerAction(INPUT_MANAGER_COMMAND_BACK)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package org.jellyfin.mobile.utils
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.WebappActivity
|
||||
import org.jellyfin.mobile.utils.Constants.SERVER_INFO_PATH
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
suspend fun WebappActivity.checkServerUrlAndConnection(enteredUrl: String): HttpUrl? {
|
||||
val normalizedUrl = enteredUrl.run {
|
||||
if (lastOrNull() == '/') this
|
||||
else "$this/"
|
||||
}
|
||||
|
||||
val urls = when {
|
||||
normalizedUrl.startsWith("http") -> listOf(normalizedUrl)
|
||||
else -> listOf("https://$normalizedUrl", "http://$normalizedUrl")
|
||||
}
|
||||
|
||||
var httpUrl: HttpUrl? = null
|
||||
var serverInfoResponse: String? = null
|
||||
loop@ for (url in urls) {
|
||||
httpUrl = url.toHttpUrlOrNull()
|
||||
|
||||
if (httpUrl == null) {
|
||||
toast(R.string.toast_error_invalid_format)
|
||||
return null // Format is invalid, don't try any other variants
|
||||
}
|
||||
|
||||
serverInfoResponse = fetchServerInfo(httpClient, httpUrl)
|
||||
if (serverInfoResponse != null)
|
||||
break@loop
|
||||
}
|
||||
|
||||
if (httpUrl == null || serverInfoResponse == null) {
|
||||
toast(getString(R.string.toast_error_cannot_connect_host, normalizedUrl))
|
||||
return null
|
||||
}
|
||||
|
||||
val isValidInstance = try {
|
||||
val serverInfo = JSONObject(serverInfoResponse)
|
||||
val version = serverInfo.getString("Version")
|
||||
.split('.')
|
||||
.mapNotNull(String::toIntOrNull)
|
||||
when {
|
||||
version.size != 3 -> false
|
||||
version[0] == 10 && version[1] < 3 -> true // Valid old version
|
||||
else -> serverInfo.getString("ProductName") == "Jellyfin Server"
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
Timber.e(e, "Cannot get server info")
|
||||
false
|
||||
}
|
||||
|
||||
return if (isValidInstance) httpUrl else {
|
||||
toast(getString(R.string.toast_error_cannot_connect_host, normalizedUrl))
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchServerInfo(httpClient: OkHttpClient, url: HttpUrl): String? {
|
||||
val serverInfoUrl = url.resolve(SERVER_INFO_PATH) ?: return null
|
||||
val request = httpClient.newCall(Request.Builder().url(serverInfoUrl).build())
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
request.execute().use { it.body?.string() }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "Cannot connect to server")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package org.jellyfin.mobile.webapp
|
||||
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.webkit.WebView
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jellyfin.mobile.AppPreferences
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.WebappActivity
|
||||
import org.jellyfin.mobile.databinding.ConnectServerBinding
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.Constants.SERVER_INFO_PATH
|
||||
import org.jellyfin.mobile.utils.requestNoBatteryOptimizations
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
class ConnectionHelper(private val activity: WebappActivity) {
|
||||
private val appPreferences: AppPreferences get() = activity.appPreferences
|
||||
private val rootView: FrameLayout get() = activity.rootView
|
||||
private val webView: WebView get() = activity.webView
|
||||
|
||||
private var cachedInstanceUrl: HttpUrl? = null
|
||||
var connected = false
|
||||
private set
|
||||
|
||||
private val connectServerBinding: ConnectServerBinding by lazy {
|
||||
ConnectServerBinding.inflate(activity.layoutInflater, rootView, false)
|
||||
}
|
||||
private val serverSetupLayout: View get() = connectServerBinding.root
|
||||
private val hostInput: EditText get() = connectServerBinding.hostInput
|
||||
private val connectionErrorText: TextView get() = connectServerBinding.connectionErrorText
|
||||
private val connectButton: Button get() = connectServerBinding.connectButton
|
||||
|
||||
fun initialize() {
|
||||
cachedInstanceUrl = appPreferences.instanceUrl?.toHttpUrlOrNull()
|
||||
loadOrShowSetup()
|
||||
}
|
||||
|
||||
fun onConnectedToWebapp() {
|
||||
connected = true
|
||||
activity.requestNoBatteryOptimizations()
|
||||
}
|
||||
|
||||
fun onSelectServer() {
|
||||
cachedInstanceUrl = null
|
||||
loadOrShowSetup()
|
||||
}
|
||||
|
||||
fun onErrorReceived() {
|
||||
connected = false
|
||||
showConnectionError()
|
||||
onSelectServer()
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
if (serverSetupLayout.isAttachedToWindow) {
|
||||
rootView.removeView(serverSetupLayout)
|
||||
cachedInstanceUrl = appPreferences.instanceUrl?.toHttpUrlOrNull()
|
||||
webView.isVisible = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun loadOrShowSetup() {
|
||||
cachedInstanceUrl.let { url ->
|
||||
if (url != null) {
|
||||
webView.isVisible = true
|
||||
webView.loadUrl(url.resolve(Constants.INDEX_PATH).toString())
|
||||
} else {
|
||||
webView.isVisible = false
|
||||
showServerSetup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showServerSetup() {
|
||||
rootView.addView(serverSetupLayout)
|
||||
hostInput.setText(appPreferences.instanceUrl)
|
||||
hostInput.setSelection(hostInput.length())
|
||||
hostInput.setOnEditorActionListener { _, action, event ->
|
||||
when {
|
||||
action == EditorInfo.IME_ACTION_DONE || event.keyCode == KeyEvent.KEYCODE_ENTER -> {
|
||||
connect()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
connectButton.setOnClickListener {
|
||||
connect()
|
||||
}
|
||||
|
||||
// Show keyboard
|
||||
serverSetupLayout.doOnNextLayout {
|
||||
hostInput.postDelayed(25) {
|
||||
hostInput.requestFocus()
|
||||
activity.getSystemService<InputMethodManager>()?.showSoftInput(hostInput, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect() {
|
||||
hostInput.isEnabled = false
|
||||
connectButton.isEnabled = false
|
||||
clearConnectionError()
|
||||
activity.lifecycleScope.launch {
|
||||
val httpUrl = checkServerUrlAndConnection(hostInput.text.toString())
|
||||
if (httpUrl != null) {
|
||||
appPreferences.instanceUrl = httpUrl.toString()
|
||||
cachedInstanceUrl = httpUrl
|
||||
rootView.removeView(serverSetupLayout)
|
||||
loadOrShowSetup()
|
||||
}
|
||||
hostInput.isEnabled = true
|
||||
connectButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun showConnectionError(@StringRes errorString: Int = R.string.connection_error_cannot_connect) {
|
||||
connectionErrorText.setText(errorString)
|
||||
connectionErrorText.isVisible = true
|
||||
}
|
||||
|
||||
private fun clearConnectionError() {
|
||||
connectionErrorText.isVisible = false
|
||||
}
|
||||
|
||||
private suspend fun checkServerUrlAndConnection(enteredUrl: String): HttpUrl? {
|
||||
val normalizedUrl = enteredUrl.run {
|
||||
if (lastOrNull() == '/') this
|
||||
else "$this/"
|
||||
}
|
||||
|
||||
val urls = when {
|
||||
normalizedUrl.startsWith("http") -> listOf(normalizedUrl)
|
||||
else -> listOf("https://$normalizedUrl", "http://$normalizedUrl")
|
||||
}
|
||||
|
||||
var httpUrl: HttpUrl? = null
|
||||
var serverInfoResponse: String? = null
|
||||
loop@ for (url in urls) {
|
||||
httpUrl = url.toHttpUrlOrNull()
|
||||
|
||||
if (httpUrl == null) {
|
||||
showConnectionError(R.string.connection_error_invalid_format)
|
||||
return null // Format is invalid, don't try any other variants
|
||||
}
|
||||
|
||||
serverInfoResponse = fetchServerInfo(activity.httpClient, httpUrl)
|
||||
if (serverInfoResponse != null)
|
||||
break@loop
|
||||
}
|
||||
|
||||
if (httpUrl == null || serverInfoResponse == null) {
|
||||
showConnectionError()
|
||||
return null
|
||||
}
|
||||
|
||||
val isValidInstance = try {
|
||||
val serverInfo = JSONObject(serverInfoResponse)
|
||||
val version = serverInfo.getString("Version")
|
||||
.split('.')
|
||||
.mapNotNull(String::toIntOrNull)
|
||||
when {
|
||||
version.size != 3 -> false
|
||||
version[0] == 10 && version[1] < 3 -> true // Valid old version
|
||||
else -> serverInfo.getString("ProductName") == "Jellyfin Server"
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
Timber.e(e, "Cannot get server info")
|
||||
false
|
||||
}
|
||||
|
||||
return if (isValidInstance) httpUrl else null
|
||||
}
|
||||
|
||||
private suspend fun fetchServerInfo(httpClient: OkHttpClient, url: HttpUrl): String? {
|
||||
val serverInfoUrl = url.resolve(SERVER_INFO_PATH) ?: return null
|
||||
val request = httpClient.newCall(Request.Builder().url(serverInfoUrl).build())
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
request.execute().use { it.body?.string() }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "Cannot connect to server")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
android:id="@+id/player_view"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?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:layout_height="match_parent"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
@@ -18,4 +20,4 @@
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<?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:id="@+id/root_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:windowBackground">
|
||||
android:background="?android:windowBackground"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/web_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?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:id="@+id/connect_server_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -60,6 +61,22 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/connect_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connection_error_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textColor="@color/error_text_color"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/host_input"
|
||||
tools:text="@string/connection_error_cannot_connect"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/connect_button"
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
@@ -70,5 +87,5 @@
|
||||
android:textColor="@android:color/white"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/host_input" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_text" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
android:id="@+id/player_controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/playback_controls_background">
|
||||
android:background="@color/playback_controls_background"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/track_title"
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
<string name="battery_optimizations_message">Por favor desactiva las optimizaciones de batería para la reproducción de medios mientras la pantalla este apagada.</string>
|
||||
<string name="toast_error_cannot_connect_host">No se pudo establecer la conexión a %s.
|
||||
\nPor favor revisa el nombre de host y las configuaciones de tu red.</string>
|
||||
<string name="toast_error_invalid_format">Host tiene un formato inválido</string>
|
||||
<string name="connection_error_invalid_format">Host tiene un formato inválido</string>
|
||||
<string name="toast_error_missing_scheme">Faltó el scheme (http o https)</string>
|
||||
<string name="connect_button_text">Conectar</string>
|
||||
<string name="host_input_hint">Host</string>
|
||||
<string name="connect_to_server_title">Conectar a un Servidor</string>
|
||||
<string name="app_name">Jellyfin</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<string name="battery_optimizations_title">Vypnout systémovou správu baterie</string>
|
||||
<string name="toast_error_cannot_connect_host">Připojení k %s se nezdařilo.
|
||||
\nZkontrolujte název hostitele a připojení k síti.</string>
|
||||
<string name="toast_error_invalid_format">Formát hostitele je neplatný</string>
|
||||
<string name="connection_error_invalid_format">Formát hostitele je neplatný</string>
|
||||
<string name="toast_error_missing_scheme">Chybí protokol (https nebo http)</string>
|
||||
<string name="connect_button_text">Připojit</string>
|
||||
<string name="host_input_hint">Hostitel</string>
|
||||
@@ -27,4 +27,4 @@
|
||||
<string name="playback_info_audio_streams">Audio proudy</string>
|
||||
<string name="playback_info_video_streams">Video proudy</string>
|
||||
<string name="playback_info_transcoding">Překódování: %b</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<string name="connect_to_server_title">Serververbindung herstellen</string>
|
||||
<string name="toast_error_cannot_connect_host">Verbindung zu %s konnte nicht hergestellt werden.
|
||||
\nPrüfe deine Netzwerkverbindung.</string>
|
||||
<string name="toast_error_invalid_format">Adressformat ungültig</string>
|
||||
<string name="connection_error_invalid_format">Adressformat ungültig</string>
|
||||
<string name="toast_error_missing_scheme">Protokoll fehlt (https oder http)</string>
|
||||
<string name="pref_music_notification_always_dismissible_title">Musik Player Benachrichtigung immer verwerfbar</string>
|
||||
<string name="pref_enable_exoplayer_summary">Aktiviere den experimentellen ExoPlayer Video Player, welcher mehr Videoformate und -Codecs unterstützt und besser ins Betriebssystem integriert ist</string>
|
||||
@@ -27,4 +27,4 @@
|
||||
<string name="playback_info_video_streams">Video-Streams</string>
|
||||
<string name="playback_info_and_x_more">…und %d weitere</string>
|
||||
<string name="playback_info_transcoding">Transkodiert: %b</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<string name="app_name">Jellyfin</string>
|
||||
<string name="toast_error_cannot_connect_host">No se puede establecer la conexión a %s.
|
||||
\nCompruebe el nombre de host y su conexión de red.</string>
|
||||
<string name="toast_error_invalid_format">El host tiene un formato no válido</string>
|
||||
<string name="connection_error_invalid_format">El host tiene un formato no válido</string>
|
||||
<string name="toast_error_missing_scheme">Esquema faltante (https o http)</string>
|
||||
<string name="connect_button_text">Conectar</string>
|
||||
<string name="host_input_hint">Host</string>
|
||||
@@ -23,4 +23,4 @@
|
||||
<string name="pref_category_music_player">Reproductor de Música</string>
|
||||
<string name="activity_name_settings">Configuración de Jellyfin</string>
|
||||
<string name="menu_item_none">Ninguno</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<string name="battery_optimizations_message">Por favor deshabilita la optimización de la batería para la reproducción de medios mientras la pantalla está apagada.</string>
|
||||
<string name="toast_error_cannot_connect_host">No se puede establecer la conexión a %s.
|
||||
\nCompruebe el nombre de host y su conexión de red.</string>
|
||||
<string name="toast_error_invalid_format">El host tiene un formato no válido</string>
|
||||
<string name="connection_error_invalid_format">El host tiene un formato no válido</string>
|
||||
<string name="toast_error_missing_scheme">Esquema faltante (https o http)</string>
|
||||
<string name="connect_button_text">Conectado</string>
|
||||
<string name="connect_to_server_title">Conectarse al Servidor</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
<string name="pref_music_notification_always_dismissible_title">Aseta musiikkisoittimen ilmoitukset piilotetuiksi</string>
|
||||
<string name="pref_category_music_player">Musiikkisoitin</string>
|
||||
<string name="activity_name_settings">Jellyfin asetukset</string>
|
||||
<string name="toast_error_invalid_format">Isäntäpalvelimen muoto on virheellinen</string>
|
||||
<string name="connection_error_invalid_format">Isäntäpalvelimen muoto on virheellinen</string>
|
||||
<string name="host_input_hint">Isäntäpalvelin</string>
|
||||
<string name="toast_error_cannot_connect_host">Yhteyttä %s ei pystytä muodostamaan.
|
||||
\nTarkista isäntäpalvelimen nimi ja verkkoyhteytesi.</string>
|
||||
<string name="connect_button_text">Yhdistä</string>
|
||||
<string name="connect_to_server_title">Yhdistä palvelimeen</string>
|
||||
<string name="app_name">Jellyfin</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<string name="battery_optimizations_title">Désactiver les optimisations de la batterie</string>
|
||||
<string name="toast_error_cannot_connect_host">La connexion à %s ne peut être établie.
|
||||
\nVeuillez vérifier le nom d\'hôte et votre connexion au réseau.</string>
|
||||
<string name="toast_error_invalid_format">L\'hôte a un format invalide</string>
|
||||
<string name="connection_error_invalid_format">L\'hôte a un format invalide</string>
|
||||
<string name="toast_error_missing_scheme">Schéma manquant (https ou http)</string>
|
||||
<string name="connect_button_text">Se connecter</string>
|
||||
<string name="host_input_hint">Hôte</string>
|
||||
@@ -23,4 +23,4 @@
|
||||
<string name="menu_item_none">Aucun</string>
|
||||
<string name="pref_category_music_player">Lecteur de musique</string>
|
||||
<string name="pref_music_notification_always_dismissible_title">Rendre rejetable la notification du lecteur audio</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<string name="network_title">Engedélyezett hálózat típusok</string>
|
||||
<string name="battery_optimizations_message">Ahhoz, hogy a lejátszás kikapcsolt képernyő mellett is működhessen, kapcsold ki az akkumulátor optimalizálást.</string>
|
||||
<string name="battery_optimizations_title">Akkumulátor optimalizálás letiltása</string>
|
||||
<string name="toast_error_invalid_format">A hoszt formátuma helytelen</string>
|
||||
<string name="connection_error_invalid_format">A hoszt formátuma helytelen</string>
|
||||
<string name="toast_error_missing_scheme">Hiányzó séma (https vagy http)</string>
|
||||
<string name="connect_button_text">Kapcsolódás</string>
|
||||
<string name="host_input_hint">Hoszt</string>
|
||||
@@ -27,4 +27,4 @@
|
||||
<string name="playback_info_audio_streams">Audio sávok</string>
|
||||
<string name="playback_info_video_streams">Video sávok</string>
|
||||
<string name="playback_info_transcoding">Transzkódolás: %b</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<string name="battery_optimizations_title">Nonaktifkan Pengoptimalan Baterai</string>
|
||||
<string name="toast_error_cannot_connect_host">Koneksi ke %s tidak dapat dibuat.
|
||||
\nSilakan periksa nama host dan koneksi jaringan Anda.</string>
|
||||
<string name="toast_error_invalid_format">Host memiliki format yang tidak valid</string>
|
||||
<string name="connection_error_invalid_format">Host memiliki format yang tidak valid</string>
|
||||
<string name="downloading">Mengunduh</string>
|
||||
<string name="playback_info_and_x_more">… Dan %d lainnya</string>
|
||||
<string name="playback_info_audio_streams">Aliran audio</string>
|
||||
<string name="playback_info_video_streams">Aliran video</string>
|
||||
<string name="playback_info_transcoding">Transcoding : %b</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
<string name="activity_name_settings">Impostazioni Jellyfin</string>
|
||||
<string name="toast_error_cannot_connect_host">La connessione verso %s non può essere stabilita.
|
||||
\nControllare il nome Host e la connessione di rete.</string>
|
||||
<string name="toast_error_invalid_format">L\'Host non ha un formato valido</string>
|
||||
<string name="connection_error_invalid_format">L\'Host non ha un formato valido</string>
|
||||
<string name="toast_error_missing_scheme">Schema mancante (https o http)</string>
|
||||
<string name="connect_button_text">Connetti</string>
|
||||
<string name="host_input_hint">Host</string>
|
||||
<string name="connect_to_server_title">Connetti al Server</string>
|
||||
<string name="app_name">Jellyfin</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
<string name="battery_optimizations_title">배터리 최적화 비활성화</string>
|
||||
<string name="toast_error_cannot_connect_host">%s으로의 연결이 성립되지 않았습니다.
|
||||
\nhostname과 네트워크 연결 상태를 확인해주십시오.</string>
|
||||
<string name="toast_error_invalid_format">호스트가 올바르지 않은 포맷을 사용합니다</string>
|
||||
<string name="connection_error_invalid_format">호스트가 올바르지 않은 포맷을 사용합니다</string>
|
||||
<string name="toast_error_missing_scheme">스킴이 존재하지 않음 (https 혹은 http)</string>
|
||||
<string name="connect_button_text">연결</string>
|
||||
<string name="host_input_hint">호스트</string>
|
||||
<string name="connect_to_server_title">서버에 연결</string>
|
||||
<string name="app_name">젤리핀</string>
|
||||
<string name="battery_optimizations_message">화면이 꺼졌을 때의 미디어 재생을 위해 배터리 최적화 비활성화 해주십시오.</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<string name="battery_optimizations_title">Deaktiver batterioptimaliseringer</string>
|
||||
<string name="toast_error_cannot_connect_host">Tilkobling til %s kunne ikke etableres.
|
||||
\nVennligst sjekk tjenernavnet og nettverksforbindelsen din.</string>
|
||||
<string name="toast_error_invalid_format">Tjeneren har et ugyldig format</string>
|
||||
<string name="connection_error_invalid_format">Tjeneren har et ugyldig format</string>
|
||||
<string name="toast_error_missing_scheme">Mangler skjema (https eller http)</string>
|
||||
<string name="connect_button_text">Koble til</string>
|
||||
<string name="host_input_hint">Tjener</string>
|
||||
@@ -19,4 +19,4 @@
|
||||
<string name="playback_info_audio_streams">Lyd strømninh</string>
|
||||
<string name="playback_info_video_streams">Video strømning</string>
|
||||
<string name="menu_item_none">Ingen</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<string name="battery_optimizations_message">Zet batterijoptimalisatie uit om media te spelen wanneer het scherm uit is.</string>
|
||||
<string name="battery_optimizations_title">Batterijoptimalisaties uitschakelen</string>
|
||||
<string name="app_name">Jellyfin</string>
|
||||
<string name="toast_error_invalid_format">Host heeft een ongeldig formaat</string>
|
||||
<string name="connection_error_invalid_format">Host heeft een ongeldig formaat</string>
|
||||
<string name="host_input_hint">Host</string>
|
||||
<string name="toast_error_missing_scheme">Geen schema opgegeven (https of http)</string>
|
||||
<string name="toast_error_cannot_connect_host">Verbinding met %s kan niet worden opgezet.
|
||||
@@ -23,4 +23,4 @@
|
||||
<string name="pref_category_music_player">Muziek speler</string>
|
||||
<string name="activity_name_settings">Jellyfin Instellingen</string>
|
||||
<string name="menu_item_none">Geen</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<string name="network_title">Tipuri de rețea permise</string>
|
||||
<string name="battery_optimizations_message">Vă rugăm să dezactivați optimizările bateriei pentru redarea media cu ecranul stins.</string>
|
||||
<string name="battery_optimizations_title">Dezactivați optimizările bateriei</string>
|
||||
<string name="toast_error_invalid_format">Gazda are un format invalid</string>
|
||||
<string name="connection_error_invalid_format">Gazda are un format invalid</string>
|
||||
<string name="toast_error_missing_scheme">Lipsește protocolul (https sau http)</string>
|
||||
<string name="connect_button_text">Conectare</string>
|
||||
<string name="host_input_hint">Gazdă</string>
|
||||
@@ -16,4 +16,4 @@
|
||||
<string name="app_name">Jellyfin</string>
|
||||
<string name="toast_error_cannot_connect_host">Conectarea la %s nu poate realizată.
|
||||
\nVerificați numele gazdei și conexiunea de rețea.</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<string name="menu_item_none">Нет</string>
|
||||
<string name="toast_error_cannot_connect_host">Соединение с %s не установлено.
|
||||
\nПроверьте имя сервера и ваше подключение к сети.</string>
|
||||
<string name="toast_error_invalid_format">Неправильный формат имени сервера</string>
|
||||
<string name="connection_error_invalid_format">Неправильный формат имени сервера</string>
|
||||
<string name="connect_button_text">Соединение</string>
|
||||
<string name="host_input_hint">Сервер</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<string name="battery_optimizations_title">Zakázať optimalizáciu batérie</string>
|
||||
<string name="toast_error_cannot_connect_host">Pripojenie k %s sa nepodarilo.
|
||||
\nSkontrolujte názov hostiteľa a pripojenie k sieti.</string>
|
||||
<string name="toast_error_invalid_format">Formát hostiteľa je neplatný</string>
|
||||
<string name="connection_error_invalid_format">Formát hostiteľa je neplatný</string>
|
||||
<string name="toast_error_missing_scheme">Chýba protokol (https alebo http)</string>
|
||||
<string name="connect_button_text">Pripojiť</string>
|
||||
<string name="host_input_hint">Hostiteľ</string>
|
||||
@@ -27,4 +27,4 @@
|
||||
<string name="playback_info_audio_streams">Audio streamy</string>
|
||||
<string name="playback_info_video_streams">Video streamy</string>
|
||||
<string name="playback_info_transcoding">Transkódovanie: %b</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<string name="battery_optimizations_title">Onemogoči optimizacijo akumulatorja</string>
|
||||
<string name="toast_error_cannot_connect_host">Povezave s %s ni mogoče vzpostaviti.
|
||||
\nPreverite naslov in internetno povezavo.</string>
|
||||
<string name="toast_error_invalid_format">Napačen format naslova</string>
|
||||
<string name="connection_error_invalid_format">Napačen format naslova</string>
|
||||
<string name="toast_error_missing_scheme">Manjka protokol (https ali http)</string>
|
||||
<string name="connect_button_text">Poveži</string>
|
||||
<string name="host_input_hint">Naslov</string>
|
||||
@@ -23,4 +23,4 @@
|
||||
<string name="pref_category_music_player">Predvajalnik glasbe</string>
|
||||
<string name="activity_name_settings">Nastavitve Jellyfin</string>
|
||||
<string name="menu_item_none">Nič</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<string name="battery_optimizations_title">பேட்டரி உகப்பாக்கங்களை முடக்கு</string>
|
||||
<string name="toast_error_cannot_connect_host">%s உடன் இணைப்பை நிறுவ முடியாது.
|
||||
\nஹோஸ்ட்பெயர் மற்றும் உங்கள் பிணைய இணைப்பை சரிபார்க்கவும்.</string>
|
||||
<string name="toast_error_invalid_format">ஹோஸ்ட் தவறான வடிவத்தைக் கொண்டுள்ளது</string>
|
||||
<string name="connection_error_invalid_format">ஹோஸ்ட் தவறான வடிவத்தைக் கொண்டுள்ளது</string>
|
||||
<string name="toast_error_missing_scheme">விடுபட்ட திட்டம் (https அல்லது http)</string>
|
||||
<string name="connect_button_text">இணை</string>
|
||||
<string name="host_input_hint">ஹோஸ்ட்</string>
|
||||
@@ -27,4 +27,4 @@
|
||||
<string name="playback_info_audio_streams">ஆடியோ ஸ்ட்ரீம்கள்</string>
|
||||
<string name="playback_info_video_streams">வீடியோ ஸ்ட்ரீம்கள்</string>
|
||||
<string name="playback_info_transcoding">டிரான்ஸ்கோடிங்: %b</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
<string name="battery_optimizations_message">Відключіть оптимізацію акумулятора для відтворення медіа, коли екран вимкнено.</string>
|
||||
<string name="toast_error_cannot_connect_host">Підключення до %s неможливо встановити.
|
||||
\nПеревірте ім\'я сервера та мережеве з\'єднання.</string>
|
||||
<string name="toast_error_invalid_format">Невірний формат сервера</string>
|
||||
<string name="connection_error_invalid_format">Невірний формат сервера</string>
|
||||
<string name="toast_error_missing_scheme">Відсутня схема (https або http)</string>
|
||||
<string name="connect_button_text">Підключитись</string>
|
||||
<string name="host_input_hint">Сервер</string>
|
||||
<string name="connect_to_server_title">Підключення до сервера</string>
|
||||
<string name="app_name">Jellyfin</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<string name="app_name">Jellyfin</string>
|
||||
<string name="toast_error_cannot_connect_host">无法建立到 %s 的连接。
|
||||
\n请检查主机名和你的网络连接。</string>
|
||||
<string name="toast_error_invalid_format">主机格式无效</string>
|
||||
<string name="connection_error_invalid_format">主机格式无效</string>
|
||||
<string name="toast_error_missing_scheme">缺少协议类型 (https 或 http)</string>
|
||||
<string name="connect_button_text">连接</string>
|
||||
<string name="host_input_hint">主机</string>
|
||||
<string name="connect_to_server_title">连接到服务器</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<!-- App colors -->
|
||||
<color name="theme_background">#101010</color>
|
||||
<color name="logo_text_color">#fafafa</color>
|
||||
<color name="error_text_color">#b00020</color>
|
||||
<color name="playback_controls_background">#60000000</color>
|
||||
<color name="playback_info_background">#cc000000</color>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
|
||||
<string name="connect_to_server_title">Connect to Server</string>
|
||||
<string name="host_input_hint">Host</string>
|
||||
<string name="connection_error_invalid_format">Host has an invalid format</string>
|
||||
<string name="connection_error_cannot_connect">Connection cannot be established.\nPlease check the hostname and your network connection.</string>
|
||||
<string name="connect_button_text">Connect</string>
|
||||
|
||||
<string name="toast_error_invalid_format">Host has an invalid format</string>
|
||||
<string name="toast_error_cannot_connect_host">Connection to %s cannot be established.\nPlease check the hostname and your network connection.</string>
|
||||
|
||||
<string name="battery_optimizations_title">Disable Battery Optimizations</string>
|
||||
<string name="battery_optimizations_message">Please disable battery optimizations for media playback while the screen is off.</string>
|
||||
<string name="network_title">Allowed Network Types</string>
|
||||
@@ -29,4 +28,4 @@
|
||||
<string name="pref_category_video_player">Native video player</string>
|
||||
<string name="pref_enable_exoplayer_title">Enable video player integration</string>
|
||||
<string name="pref_enable_exoplayer_summary">Enable the experimental ExoPlayer video player which supports more video formats and codecs, and is more integrated into the OS</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user