Made synchronization server address configurable
This commit is contained in:
@@ -95,6 +95,7 @@
|
||||
|
||||
<data android:scheme="kotatsu" />
|
||||
<data android:host="about" />
|
||||
<data android:host="sync-settings" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
|
||||
@@ -388,6 +388,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_EXIT_CONFIRM = "exit_confirm"
|
||||
const val KEY_INCOGNITO_MODE = "incognito"
|
||||
const val KEY_SYNC = "sync"
|
||||
const val KEY_SYNC_SETTINGS = "sync_settings"
|
||||
const val KEY_READER_BAR = "reader_bar"
|
||||
const val KEY_READER_SLIDER = "reader_slider"
|
||||
const val KEY_SHORTCUTS = "dynamic_shortcuts"
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.internal.toCanonicalHost
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.EditTextValidator
|
||||
|
||||
class DomainValidator : EditTextValidator() {
|
||||
|
||||
private val urlBuilder = HttpUrl.Builder()
|
||||
|
||||
override fun validate(text: String): ValidationResult {
|
||||
val trimmed = text.trim()
|
||||
if (trimmed.isEmpty()) {
|
||||
return ValidationResult.Success
|
||||
}
|
||||
return if (!checkCharacters(trimmed) || trimmed.toCanonicalHost() == null) {
|
||||
return if (!checkCharacters(trimmed)) {
|
||||
ValidationResult.Failed(context.getString(R.string.invalid_domain_message))
|
||||
} else {
|
||||
ValidationResult.Success
|
||||
@@ -22,6 +19,12 @@ class DomainValidator : EditTextValidator() {
|
||||
}
|
||||
|
||||
private fun checkCharacters(value: String): Boolean = runCatching {
|
||||
urlBuilder.host(value)
|
||||
val parts = value.split(':')
|
||||
require(parts.size <= 2)
|
||||
val urlBuilder = HttpUrl.Builder()
|
||||
urlBuilder.host(parts.first())
|
||||
if (parts.size == 2) {
|
||||
urlBuilder.port(parts[1].toInt())
|
||||
}
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) {
|
||||
else -> getString(R.string.disabled)
|
||||
}
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_SYNC_SETTINGS)?.isEnabled = account != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ class SettingsActivity :
|
||||
Intent.ACTION_VIEW -> {
|
||||
when (intent.data?.host) {
|
||||
HOST_ABOUT -> AboutSettingsFragment()
|
||||
HOST_SYNC_SETTINGS -> SyncSettingsFragment()
|
||||
else -> SettingsHeadersFragment()
|
||||
}
|
||||
}
|
||||
@@ -159,6 +160,7 @@ class SettingsActivity :
|
||||
private const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST"
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
private const val HOST_ABOUT = "about"
|
||||
private const val HOST_SYNC_SETTINGS = "sync-settings"
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java)
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.FragmentResultListener
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.sync.data.SyncSettings
|
||||
import org.koitharu.kotatsu.sync.ui.SyncHostDialogFragment
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SyncSettingsFragment : BasePreferenceFragment(R.string.sync_settings), FragmentResultListener {
|
||||
|
||||
@Inject
|
||||
lateinit var syncSettings: SyncSettings
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_sync)
|
||||
bindHostSummary()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
childFragmentManager.setFragmentResultListener(SyncHostDialogFragment.REQUEST_KEY, viewLifecycleOwner, this)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
SyncSettings.KEY_HOST -> {
|
||||
SyncHostDialogFragment.show(childFragmentManager)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFragmentResult(requestKey: String, result: Bundle) {
|
||||
bindHostSummary()
|
||||
}
|
||||
|
||||
private fun bindHostSummary() {
|
||||
val preference = findPreference<Preference>(SyncSettings.KEY_HOST) ?: return
|
||||
preference.summary = syncSettings.host
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import android.widget.EditText
|
||||
import androidx.annotation.ArrayRes
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.preference.EditTextPreference
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
@@ -25,6 +26,12 @@ class AutoCompleteTextViewPreference @JvmOverloads constructor(
|
||||
|
||||
init {
|
||||
super.setOnBindEditTextListener(autoCompleteBindListener)
|
||||
context.withStyledAttributes(attrs, R.styleable.AutoCompleteTextViewPreference, defStyleAttr, defStyleRes) {
|
||||
val entriesId = getResourceId(R.styleable.AutoCompleteTextViewPreference_android_entries, 0)
|
||||
if (entriesId != 0) {
|
||||
setEntries(entriesId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setEntries(@ArrayRes arrayResId: Int) {
|
||||
@@ -55,4 +62,4 @@ class AutoCompleteTextViewPreference @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package org.koitharu.kotatsu.sync.data
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.Reusable
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.SyncApiException
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
@@ -13,19 +11,17 @@ import org.koitharu.kotatsu.parsers.util.removeSurrounding
|
||||
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class SyncAuthApi @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
) {
|
||||
|
||||
private val baseUrl = context.getString(R.string.url_sync_server)
|
||||
|
||||
suspend fun authenticate(email: String, password: String): String {
|
||||
suspend fun authenticate(host: String, email: String, password: String): String {
|
||||
val body = JSONObject(
|
||||
mapOf("email" to email, "password" to password),
|
||||
).toRequestBody()
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/auth")
|
||||
.url("http://$host/auth")
|
||||
.post(body)
|
||||
.build()
|
||||
val response = okHttpClient.newCall(request).await()
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.R
|
||||
class SyncAuthenticator(
|
||||
context: Context,
|
||||
private val account: Account,
|
||||
private val syncSettings: SyncSettings,
|
||||
private val authApi: SyncAuthApi,
|
||||
) : Authenticator {
|
||||
|
||||
@@ -30,6 +31,7 @@ class SyncAuthenticator(
|
||||
private fun tryRefreshToken() = runCatching {
|
||||
runBlocking {
|
||||
authApi.authenticate(
|
||||
syncSettings.host,
|
||||
account.name,
|
||||
accountManager.getPassword(account),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.sync.data
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class SyncSettings(
|
||||
context: Context,
|
||||
private val account: Account?,
|
||||
) {
|
||||
|
||||
@Inject
|
||||
constructor(@ApplicationContext context: Context) : this(
|
||||
context,
|
||||
AccountManager.get(context)
|
||||
.getAccountsByType(context.getString(R.string.account_type_sync))
|
||||
.firstOrNull(),
|
||||
)
|
||||
|
||||
private val accountManager = AccountManager.get(context)
|
||||
private val defaultHost = context.getString(R.string.sync_host_default)
|
||||
|
||||
@get:WorkerThread
|
||||
@set:WorkerThread
|
||||
var host: String
|
||||
get() = account?.let {
|
||||
accountManager.getUserData(it, KEY_HOST)
|
||||
}.ifNullOrEmpty { defaultHost }
|
||||
set(value) {
|
||||
account?.let {
|
||||
accountManager.setUserData(it, KEY_HOST, value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val KEY_HOST = "host"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.sync.domain
|
||||
|
||||
class SyncAuthResult(
|
||||
val host: String,
|
||||
val email: String,
|
||||
val password: String,
|
||||
val token: String,
|
||||
@@ -12,17 +13,17 @@ class SyncAuthResult(
|
||||
|
||||
other as SyncAuthResult
|
||||
|
||||
if (host != other.host) return false
|
||||
if (email != other.email) return false
|
||||
if (password != other.password) return false
|
||||
if (token != other.token) return false
|
||||
|
||||
return true
|
||||
return token == other.token
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = email.hashCode()
|
||||
var result = host.hashCode()
|
||||
result = 31 * result + email.hashCode()
|
||||
result = 31 * result + password.hashCode()
|
||||
result = 31 * result + token.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
|
||||
import org.koitharu.kotatsu.sync.data.SyncAuthApi
|
||||
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
|
||||
import org.koitharu.kotatsu.sync.data.SyncInterceptor
|
||||
import org.koitharu.kotatsu.sync.data.SyncSettings
|
||||
import org.koitharu.kotatsu.utils.GZipInterceptor
|
||||
import org.koitharu.kotatsu.utils.ext.parseJsonOrNull
|
||||
import org.koitharu.kotatsu.utils.ext.toContentValues
|
||||
@@ -41,18 +42,20 @@ private const val FIELD_TIMESTAMP = "timestamp"
|
||||
@WorkerThread
|
||||
class SyncHelper(
|
||||
context: Context,
|
||||
account: Account,
|
||||
private val account: Account,
|
||||
private val provider: ContentProviderClient,
|
||||
) {
|
||||
|
||||
private val authorityHistory = context.getString(R.string.sync_authority_history)
|
||||
private val authorityFavourites = context.getString(R.string.sync_authority_favourites)
|
||||
private val settings = SyncSettings(context, account)
|
||||
private val httpClient = OkHttpClient.Builder()
|
||||
.authenticator(SyncAuthenticator(context, account, SyncAuthApi(context, OkHttpClient())))
|
||||
.authenticator(SyncAuthenticator(context, account, settings, SyncAuthApi(OkHttpClient())))
|
||||
.addInterceptor(SyncInterceptor(context, account))
|
||||
.addInterceptor(GZipInterceptor())
|
||||
.build()
|
||||
private val baseUrl = context.getString(R.string.url_sync_server)
|
||||
private val baseUrl: String
|
||||
get() = "http://${settings.host}"
|
||||
private val defaultGcPeriod: Long // gc period if sync enabled
|
||||
get() = TimeUnit.DAYS.toMillis(4)
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import android.widget.Button
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentResultListener
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -20,12 +20,13 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.databinding.ActivitySyncAuthBinding
|
||||
import org.koitharu.kotatsu.sync.data.SyncSettings
|
||||
import org.koitharu.kotatsu.sync.domain.SyncAuthResult
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickListener {
|
||||
class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickListener, FragmentResultListener {
|
||||
|
||||
private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null
|
||||
private var resultBundle: Bundle? = null
|
||||
@@ -43,6 +44,8 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
binding.buttonNext.setOnClickListener(this)
|
||||
binding.buttonBack.setOnClickListener(this)
|
||||
binding.buttonDone.setOnClickListener(this)
|
||||
binding.layoutProgress.setOnClickListener(this)
|
||||
binding.buttonSettings.setOnClickListener(this)
|
||||
binding.editEmail.addTextChangedListener(EmailTextWatcher(binding.buttonNext))
|
||||
binding.editPassword.addTextChangedListener(PasswordTextWatcher(binding.buttonDone))
|
||||
|
||||
@@ -52,6 +55,7 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
viewModel.onError.observe(this, ::onError)
|
||||
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
|
||||
|
||||
supportFragmentManager.setFragmentResultListener(SyncHostDialogFragment.REQUEST_KEY, this, this)
|
||||
pageBackCallback.update()
|
||||
}
|
||||
|
||||
@@ -73,12 +77,14 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
}
|
||||
|
||||
R.id.button_next -> {
|
||||
binding.switcher.showNext()
|
||||
binding.groupLogin.isVisible = false
|
||||
binding.groupPassword.isVisible = true
|
||||
pageBackCallback.update()
|
||||
}
|
||||
|
||||
R.id.button_back -> {
|
||||
binding.switcher.showPrevious()
|
||||
binding.groupPassword.isVisible = false
|
||||
binding.groupLogin.isVisible = true
|
||||
pageBackCallback.update()
|
||||
}
|
||||
|
||||
@@ -88,9 +94,18 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
password = binding.editPassword.text.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.button_settings -> {
|
||||
SyncHostDialogFragment.show(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFragmentResult(requestKey: String, result: Bundle) {
|
||||
val host = result.getString(SyncHostDialogFragment.KEY_HOST) ?: return
|
||||
viewModel.host.value = host
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
accountAuthenticatorResponse?.let { response ->
|
||||
resultBundle?.also {
|
||||
@@ -105,7 +120,6 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
return
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(binding.root, Fade())
|
||||
binding.switcher.isGone = isLoading
|
||||
binding.layoutProgress.isVisible = isLoading
|
||||
pageBackCallback.update()
|
||||
}
|
||||
@@ -121,8 +135,10 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
private fun onTokenReceived(authResult: SyncAuthResult) {
|
||||
val am = AccountManager.get(this)
|
||||
val account = Account(authResult.email, getString(R.string.account_type_sync))
|
||||
val userdata = Bundle(1)
|
||||
userdata.putString(SyncSettings.KEY_HOST, authResult.host)
|
||||
val result = Bundle()
|
||||
if (am.addAccountExplicitly(account, authResult.password, Bundle())) {
|
||||
if (am.addAccountExplicitly(account, authResult.password, userdata)) {
|
||||
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name)
|
||||
result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
|
||||
result.putString(AccountManager.KEY_AUTHTOKEN, authResult.token)
|
||||
@@ -168,12 +184,13 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
private inner class PageBackCallback : OnBackPressedCallback(false) {
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
binding.switcher.showPrevious()
|
||||
binding.groupLogin.isVisible = true
|
||||
binding.groupPassword.isVisible = false
|
||||
update()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
isEnabled = binding.switcher.isVisible && binding.switcher.displayedChild > 0
|
||||
isEnabled = !binding.layoutProgress.isVisible && binding.groupPassword.isVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
package org.koitharu.kotatsu.sync.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.sync.data.SyncAuthApi
|
||||
import org.koitharu.kotatsu.sync.domain.SyncAuthResult
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SyncAuthViewModel @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
private val api: SyncAuthApi,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onTokenObtained = SingleLiveEvent<SyncAuthResult>()
|
||||
val host = MutableLiveData("")
|
||||
|
||||
private val defaultHost = context.getString(R.string.sync_host_default)
|
||||
|
||||
fun obtainToken(email: String, password: String) {
|
||||
val hostValue = host.value.ifNullOrEmpty { defaultHost }
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val token = api.authenticate(email, password)
|
||||
val result = SyncAuthResult(email, password, token)
|
||||
val token = api.authenticate(hostValue, email, password)
|
||||
val result = SyncAuthResult(host.value.orEmpty(), email, password, token)
|
||||
onTokenObtained.emitCall(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.koitharu.kotatsu.sync.ui
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.databinding.PreferenceDialogAutocompletetextviewBinding
|
||||
import org.koitharu.kotatsu.settings.DomainValidator
|
||||
import org.koitharu.kotatsu.sync.data.SyncSettings
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SyncHostDialogFragment : AlertDialogFragment<PreferenceDialogAutocompletetextviewBinding>(),
|
||||
DialogInterface.OnClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var syncSettings: SyncSettings
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
) = PreferenceDialogAutocompletetextviewBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
return super.onBuildDialog(builder)
|
||||
.setPositiveButton(android.R.string.ok, this)
|
||||
.setNegativeButton(android.R.string.cancel, this)
|
||||
.setCancelable(false)
|
||||
.setTitle(R.string.server_address)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.message.updateLayoutParams<MarginLayoutParams> {
|
||||
topMargin = view.resources.getDimensionPixelOffset(R.dimen.screen_padding)
|
||||
bottomMargin = topMargin
|
||||
}
|
||||
binding.message.setText(R.string.sync_host_description)
|
||||
val entries = view.resources.getStringArray(R.array.sync_host_list)
|
||||
val editText = binding.edit
|
||||
editText.setText(syncSettings.host)
|
||||
editText.threshold = 0
|
||||
editText.setAdapter(ArrayAdapter(view.context, android.R.layout.simple_spinner_dropdown_item, entries))
|
||||
binding.dropdown.setOnClickListener {
|
||||
editText.showDropDown()
|
||||
}
|
||||
DomainValidator().attachToEditText(editText)
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
val result = binding.edit.text?.toString().orEmpty()
|
||||
syncSettings.host = result
|
||||
parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(KEY_HOST to result))
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "SyncHostDialogFragment"
|
||||
const val REQUEST_KEY = "sync_host"
|
||||
const val KEY_HOST = "host"
|
||||
|
||||
fun show(fm: FragmentManager) = SyncHostDialogFragment().show(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<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"
|
||||
@@ -10,9 +10,8 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:drawablePadding="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/sync_title"
|
||||
@@ -23,150 +22,157 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ViewSwitcher
|
||||
android:id="@+id/switcher"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ImageButton
|
||||
android:id="@+id/button_settings"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_settings" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/page_email"
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/email_enter_hint"
|
||||
android:textAppearance="?textAppearanceSubtitle1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_email"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
app:errorIconDrawable="@null"
|
||||
app:helperText="@string/sync_auth_hint"
|
||||
app:hintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_subtitle">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="emailAddress"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textEmailAddress"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
tools:text="test@mail.com" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/email_enter_hint"
|
||||
android:textAppearance="?textAppearanceSubtitle1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_email"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/textView_subtitle"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginTop="30dp"
|
||||
app:errorIconDrawable="@null"
|
||||
app:helperText="@string/sync_auth_hint"
|
||||
app:hintEnabled="false">
|
||||
<Button
|
||||
android:id="@+id/button_cancel"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@android:string/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="emailAddress"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textEmailAddress"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
tools:text="test@mail.com" />
|
||||
<Button
|
||||
android:id="@+id/button_next"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:text="@string/next"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/enter_password"
|
||||
android:textAppearance="?textAppearanceSubtitle1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:text="@android:string/cancel" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_password"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
app:endIconMode="password_toggle"
|
||||
app:errorIconDrawable="@null"
|
||||
app:hintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_subtitle_2">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_next"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:enabled="false"
|
||||
android:text="@string/next"
|
||||
tools:ignore="RelativeOverlap" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/page_password"
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:maxLength="24"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
tools:text="qwerty" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/enter_password"
|
||||
android:textAppearance="?textAppearanceSubtitle1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_password"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/textView_subtitle_2"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginTop="30dp"
|
||||
app:endIconMode="password_toggle"
|
||||
app:errorIconDrawable="@null"
|
||||
app:hintEnabled="false">
|
||||
<Button
|
||||
android:id="@+id/button_back"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/back"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:maxLength="24"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
tools:text="qwerty" />
|
||||
<Button
|
||||
android:id="@+id/button_done"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:text="@string/done"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/group_login"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="textView_subtitle,button_cancel,button_next,layout_email" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_back"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:text="@string/back" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_done"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:enabled="false"
|
||||
android:text="@string/done"
|
||||
tools:ignore="RelativeOverlap" />
|
||||
|
||||
</RelativeLayout>
|
||||
</ViewSwitcher>
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/group_password"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="textView_subtitle_2,button_back,button_done,layout_password" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?android:windowBackground"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/circularProgressIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
@@ -174,4 +180,4 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -103,4 +103,8 @@
|
||||
<attr name="cornerSizeBottomRight" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="AutoCompleteTextViewPreference">
|
||||
<attr name="android:entries" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<string name="url_weblate" translatable="false">https://hosted.weblate.org/engage/kotatsu</string>
|
||||
<string name="url_error_report" translatable="false">https://acra.kotatsu.app/report</string>
|
||||
<string name="account_type_sync" translatable="false">org.kotatsu.sync</string>
|
||||
<string name="url_sync_server" translatable="false">https://sync.kotatsu.app</string>
|
||||
<string name="sync_host_default" translatable="false">sync.kotatsu.app</string>
|
||||
<string name="shikimori_clientId" translatable="false">Mw6F0tPEOgyV7F9U9Twg50Q8SndMY7hzIOfXg0AX_XU</string>
|
||||
<string name="shikimori_clientSecret" translatable="false">euBMt1GGRSDpVIFQVPxZrO7Kh6X4gWyv0dABuj4B-M8</string>
|
||||
<string name="anilist_clientId" translatable="false">9887</string>
|
||||
@@ -41,9 +41,13 @@
|
||||
<item>block_nsfw</item>
|
||||
<item>block_all</item>
|
||||
</string-array>
|
||||
<string-array name="values_network_policy">
|
||||
<string-array name="values_network_policy" translatable="false">
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>0</item>
|
||||
</string-array>
|
||||
<string-array name="sync_host_list" translatable="false">
|
||||
<item>@string/sync_host_default</item>
|
||||
<item>86.57.183.214:8081</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -435,4 +435,7 @@
|
||||
<string name="show_on_shelf">Show on the Shelf</string>
|
||||
<string name="sync_auth_hint">You can sign in into an existing account or create a new one</string>
|
||||
<string name="find_similar">Find similar</string>
|
||||
<string name="sync_settings">Synchronization settings</string>
|
||||
<string name="server_address">Server address</string>
|
||||
<string name="sync_host_description">You can use a self-hosted synchronization server or a default one. Don\'t change this if you\'re not sure what you\'re doing.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<account-authenticator
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountPreferences="@xml/pref_sync"
|
||||
android:accountPreferences="@xml/pref_sync_header"
|
||||
android:accountType="@string/account_type_sync"
|
||||
android:icon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
android:summary="@string/sync_title"
|
||||
android:title="@string/sync" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:enabled="false"
|
||||
android:fragment="org.koitharu.kotatsu.settings.SyncSettingsFragment"
|
||||
android:key="sync_settings"
|
||||
android:title="@string/sync_settings" />
|
||||
|
||||
<PreferenceCategory android:title="@string/tracking">
|
||||
|
||||
<Preference
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen />
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<Preference
|
||||
android:key="host"
|
||||
android:title="@string/server_address" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
12
app/src/main/res/xml/pref_sync_header.xml
Normal file
12
app/src/main/res/xml/pref_sync_header.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<Preference android:title="@string/sync_settings">
|
||||
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="kotatsu://sync-settings" />
|
||||
</Preference>
|
||||
|
||||
</PreferenceScreen>
|
||||
Reference in New Issue
Block a user