Sync on demand

This commit is contained in:
Koitharu
2022-05-14 18:29:38 +03:00
parent 1be8760c00
commit d31d302896
18 changed files with 178 additions and 55 deletions

View File

@@ -13,6 +13,9 @@
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" /> <uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<application <application
android:name="org.koitharu.kotatsu.KotatsuApp" android:name="org.koitharu.kotatsu.KotatsuApp"

View File

@@ -30,7 +30,6 @@ import org.koitharu.kotatsu.settings.settingsModule
import org.koitharu.kotatsu.suggestions.suggestionsModule import org.koitharu.kotatsu.suggestions.suggestionsModule
import org.koitharu.kotatsu.sync.syncModule import org.koitharu.kotatsu.sync.syncModule
import org.koitharu.kotatsu.tracker.trackerModule import org.koitharu.kotatsu.tracker.trackerModule
import org.koitharu.kotatsu.widget.WidgetUpdater
import org.koitharu.kotatsu.widget.appWidgetModule import org.koitharu.kotatsu.widget.appWidgetModule
class KotatsuApp : Application() { class KotatsuApp : Application() {
@@ -44,9 +43,6 @@ class KotatsuApp : Application() {
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext)) Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme) AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
registerActivityLifecycleCallbacks(get<AppProtectHelper>()) registerActivityLifecycleCallbacks(get<AppProtectHelper>())
val widgetUpdater = WidgetUpdater(applicationContext)
widgetUpdater.subscribeToFavourites(get())
widgetUpdater.subscribeToHistory(get())
} }
private fun initKoin() { private fun initKoin() {

View File

@@ -1,9 +1,16 @@
package org.koitharu.kotatsu.core.db package org.koitharu.kotatsu.core.db
import androidx.room.InvalidationTracker
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val databaseModule val databaseModule
get() = module { get() = module {
single { MangaDatabase(androidContext()) } single {
MangaDatabase(androidContext()).also { db ->
getAll<InvalidationTracker.Observer>().forEach {
db.invalidationTracker.addObserver(it)
}
}
}
} }

View File

@@ -27,6 +27,7 @@ import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -56,6 +57,7 @@ import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.ui.FeedFragment
import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.VoiceInputContract import org.koitharu.kotatsu.utils.VoiceInputContract
@@ -440,6 +442,8 @@ class MainActivity :
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager) !settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager) settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
} }
yield()
get<SyncController>().requestFullSync()
} }
} }

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.accounts.AccountManager
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
@@ -7,7 +9,10 @@ import android.provider.Settings
import android.view.View import android.view.View
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
@@ -17,6 +22,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.util.names import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.settings.utils.SliderPreference
import org.koitharu.kotatsu.sync.domain.AUTHORITY_FAVOURITES
import org.koitharu.kotatsu.sync.domain.AUTHORITY_HISTORY
import org.koitharu.kotatsu.utils.ext.getStorageName import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@@ -60,6 +67,11 @@ class ContentSettingsFragment :
settings.subscribe(this) settings.subscribe(this)
} }
override fun onResume() {
super.onResume()
bindSyncSummary()
}
override fun onDestroyView() { override fun onDestroyView() {
settings.unsubscribe(this) settings.unsubscribe(this)
super.onDestroyView() super.onDestroyView()
@@ -78,10 +90,6 @@ class ContentSettingsFragment :
AppSettings.KEY_SOURCES_HIDDEN -> { AppSettings.KEY_SOURCES_HIDDEN -> {
bindRemoteSourcesSummary() bindRemoteSourcesSummary()
} }
AppSettings.KEY_SYNC -> {
val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
startActivity(intent)
}
} }
} }
@@ -96,6 +104,23 @@ class ContentSettingsFragment :
.show() .show()
true true
} }
AppSettings.KEY_SYNC -> {
val am = AccountManager.get(requireContext())
val accountType = getString(R.string.account_type_sync)
if (am.getAccountsByType(accountType).firstOrNull() == null) {
am.addAccount(accountType, accountType, null, null, requireActivity(), null, null)
} else {
val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf(accountType))
intent.putExtra(Settings.EXTRA_AUTHORITIES, arrayOf(AUTHORITY_HISTORY, AUTHORITY_FAVOURITES))
try {
startActivity(intent)
} catch (_: ActivityNotFoundException) {
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
}
}
true
}
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
} }
} }
@@ -119,4 +144,16 @@ class ContentSettingsFragment :
) )
} }
} }
private fun bindSyncSummary() {
viewLifecycleScope.launch {
val account = withContext(Dispatchers.Default) {
val type = getString(R.string.account_type_sync)
AccountManager.get(requireContext()).getAccountsByType(type).firstOrNull()
}
findPreference<Preference>(AppSettings.KEY_SYNC)?.run {
summary = account?.name ?: getString(R.string.sync_title)
}
}
}
} }

View File

@@ -1,14 +1,19 @@
package org.koitharu.kotatsu.sync package org.koitharu.kotatsu.sync
import androidx.room.InvalidationTracker
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.sync.data.SyncAuthApi import org.koitharu.kotatsu.sync.data.SyncAuthApi
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.ui.SyncAuthViewModel import org.koitharu.kotatsu.sync.ui.SyncAuthViewModel
val syncModule val syncModule
get() = module { get() = module {
single { SyncController(androidContext()) } bind InvalidationTracker.Observer::class
factory { SyncAuthApi(androidContext(), get()) } factory { SyncAuthApi(androidContext(), get()) }
viewModel { SyncAuthViewModel(get()) } viewModel { SyncAuthViewModel(get()) }

View File

@@ -0,0 +1,71 @@
package org.koitharu.kotatsu.sync.domain
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentResolver
import android.content.Context
import android.os.Bundle
import androidx.room.InvalidationTracker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
class SyncController(
context: Context,
) : InvalidationTracker.Observer(arrayOf(TABLE_HISTORY, TABLE_FAVOURITES, TABLE_FAVOURITE_CATEGORIES)) {
private val am = AccountManager.get(context)
private val accountType = context.getString(R.string.account_type_sync)
override fun onInvalidated(tables: MutableSet<String>) {
requestSync(
favourites = TABLE_FAVOURITES in tables || TABLE_FAVOURITE_CATEGORIES in tables,
history = TABLE_HISTORY in tables,
)
}
suspend fun requestFullSync() = withContext(Dispatchers.Default) {
requestSyncImpl(favourites = true, history = true)
}
private fun requestSync(favourites: Boolean, history: Boolean) = processLifecycleScope.launch(Dispatchers.Default) {
requestSyncImpl(favourites, history)
}
@Synchronized
private fun requestSyncImpl(favourites: Boolean, history: Boolean) {
if (!favourites && !history) {
return
}
val account = peekAccount() ?: return
if (!ContentResolver.getMasterSyncAutomatically()) {
return
}
// TODO limit frequency
if (favourites) {
requestSyncForAuthority(account, AUTHORITY_FAVOURITES)
}
if (history) {
requestSyncForAuthority(account, AUTHORITY_HISTORY)
}
}
private fun requestSyncForAuthority(account: Account, authority: String) {
if (
ContentResolver.getSyncAutomatically(account, AUTHORITY_FAVOURITES) &&
!ContentResolver.isSyncActive(account, authority) &&
!ContentResolver.isSyncPending(account, authority)
) {
ContentResolver.requestSync(account, authority, Bundle.EMPTY)
}
}
private fun peekAccount(): Account? {
return am.getAccountsByType(accountType).firstOrNull()
}
}

View File

@@ -13,17 +13,17 @@ import org.json.JSONObject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.* import org.koitharu.kotatsu.core.db.*
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.sync.data.SyncAuthApi import org.koitharu.kotatsu.sync.data.SyncAuthApi
import org.koitharu.kotatsu.sync.data.SyncAuthenticator import org.koitharu.kotatsu.sync.data.SyncAuthenticator
import org.koitharu.kotatsu.sync.data.SyncInterceptor import org.koitharu.kotatsu.sync.data.SyncInterceptor
import org.koitharu.kotatsu.utils.GZipInterceptor import org.koitharu.kotatsu.utils.GZipInterceptor
import org.koitharu.kotatsu.utils.ext.parseJsonOrNull
import org.koitharu.kotatsu.utils.ext.toContentValues import org.koitharu.kotatsu.utils.ext.toContentValues
import org.koitharu.kotatsu.utils.ext.toJson import org.koitharu.kotatsu.utils.ext.toJson
import org.koitharu.kotatsu.utils.ext.toRequestBody import org.koitharu.kotatsu.utils.ext.toRequestBody
private const val AUTHORITY_HISTORY = "org.koitharu.kotatsu.history" const val AUTHORITY_HISTORY = "org.koitharu.kotatsu.history"
private const val AUTHORITY_FAVOURITES = "org.koitharu.kotatsu.favourites" const val AUTHORITY_FAVOURITES = "org.koitharu.kotatsu.favourites"
private const val FIELD_TIMESTAMP = "timestamp" private const val FIELD_TIMESTAMP = "timestamp"
@@ -53,7 +53,7 @@ class SyncHelper(
.url("$baseUrl/resource/$TABLE_FAVOURITES") .url("$baseUrl/resource/$TABLE_FAVOURITES")
.post(data.toRequestBody()) .post(data.toRequestBody())
.build() .build()
val response = httpClient.newCall(request).execute().parseJson() val response = httpClient.newCall(request).execute().parseJsonOrNull() ?: return
val timestamp = response.getLong(FIELD_TIMESTAMP) val timestamp = response.getLong(FIELD_TIMESTAMP)
val categoriesResult = upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES), timestamp) val categoriesResult = upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES), timestamp)
syncResult.stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L syncResult.stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L
@@ -71,7 +71,7 @@ class SyncHelper(
.url("$baseUrl/resource/$TABLE_HISTORY") .url("$baseUrl/resource/$TABLE_HISTORY")
.post(data.toRequestBody()) .post(data.toRequestBody())
.build() .build()
val response = httpClient.newCall(request).execute().parseJson() val response = httpClient.newCall(request).execute().parseJsonOrNull() ?: return
val result = upsertHistory( val result = upsertHistory(
json = response.getJSONArray(TABLE_HISTORY), json = response.getJSONArray(TABLE_HISTORY),
timestamp = response.getLong(FIELD_TIMESTAMP), timestamp = response.getLong(FIELD_TIMESTAMP),

View File

@@ -9,7 +9,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
class SyncAuthenticator(private val context: Context) : AbstractAccountAuthenticator(context) { class SyncAccountAuthenticator(private val context: Context) : AbstractAccountAuthenticator(context) {
override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?): Bundle? = null override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?): Bundle? = null
@@ -52,7 +52,6 @@ class SyncAuthenticator(private val context: Context) : AbstractAccountAuthentic
} else { } else {
val intent = Intent(context, SyncAuthActivity::class.java) val intent = Intent(context, SyncAuthActivity::class.java)
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
// intent.putExtra(SyncAuthActivity.EXTRA_TOKEN_TYPE, authTokenType)
val bundle = Bundle() val bundle = Bundle()
if (options != null) { if (options != null) {
bundle.putAll(options) bundle.putAll(options)

View File

@@ -6,11 +6,11 @@ import android.os.IBinder
class SyncAuthenticatorService : Service() { class SyncAuthenticatorService : Service() {
private lateinit var authenticator: SyncAuthenticator private lateinit var authenticator: SyncAccountAuthenticator
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
authenticator = SyncAuthenticator(this) authenticator = SyncAccountAuthenticator(this)
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {

View File

@@ -2,8 +2,19 @@ package org.koitharu.kotatsu.utils.ext
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.parsers.util.parseJson
import java.net.HttpURLConnection
private val TYPE_JSON = "application/json".toMediaType() private val TYPE_JSON = "application/json".toMediaType()
fun JSONObject.toRequestBody() = toString().toRequestBody(TYPE_JSON) fun JSONObject.toRequestBody() = toString().toRequestBody(TYPE_JSON)
fun Response.parseJsonOrNull(): JSONObject? {
return if (code == HttpURLConnection.HTTP_NO_CONTENT) {
null
} else {
parseJson()
}
}

View File

@@ -1,10 +1,15 @@
package org.koitharu.kotatsu.widget package org.koitharu.kotatsu.widget
import androidx.room.InvalidationTracker
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.widget.shelf.ShelfConfigViewModel import org.koitharu.kotatsu.widget.shelf.ShelfConfigViewModel
val appWidgetModule val appWidgetModule
get() = module { get() = module {
single<InvalidationTracker.Observer> { WidgetUpdater(androidContext()) }
viewModel { ShelfConfigViewModel(get()) } viewModel { ShelfConfigViewModel(get()) }
} }

View File

@@ -4,36 +4,26 @@ import android.appwidget.AppWidgetManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import kotlinx.coroutines.CancellationException import androidx.room.InvalidationTracker
import kotlinx.coroutines.Dispatchers import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import kotlinx.coroutines.flow.launchIn import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.retry
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.widget.recent.RecentWidgetProvider import org.koitharu.kotatsu.widget.recent.RecentWidgetProvider
import org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider import org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider
class WidgetUpdater(private val context: Context) { class WidgetUpdater(
private val context: Context
) : InvalidationTracker.Observer(TABLE_HISTORY, TABLE_FAVOURITES) {
fun subscribeToFavourites(repository: FavouritesRepository) { override fun onInvalidated(tables: MutableSet<String>) {
repository.observeAll(SortOrder.NEWEST) if (TABLE_HISTORY in tables) {
.onEach { updateWidget(ShelfWidgetProvider::class.java) } updateWidgets(RecentWidgetProvider::class.java)
.retry { error -> error !is CancellationException } }
.launchIn(processLifecycleScope + Dispatchers.Default) if (TABLE_FAVOURITES in tables) {
updateWidgets(ShelfWidgetProvider::class.java)
}
} }
fun subscribeToHistory(repository: HistoryRepository) { private fun updateWidgets(cls: Class<*>) {
repository.observeAll()
.onEach { updateWidget(RecentWidgetProvider::class.java) }
.retry { error -> error !is CancellationException }
.launchIn(processLifecycleScope + Dispatchers.Default)
}
private fun updateWidget(cls: Class<*>) {
val intent = Intent(context, cls) val intent = Intent(context, cls)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(context) val ids = AppWidgetManager.getInstance(context)

View File

@@ -107,7 +107,7 @@
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:text="Enter your email to continue" android:text="@string/enter_email_text"
android:textAppearance="?textAppearanceSubtitle1" /> android:textAppearance="?textAppearanceSubtitle1" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
@@ -119,6 +119,7 @@
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
app:endIconMode="password_toggle"
app:errorIconDrawable="@null" app:errorIconDrawable="@null"
app:helperText="You can sign in into an existing account or create a new one" app:helperText="You can sign in into an existing account or create a new one"
app:hintEnabled="false"> app:hintEnabled="false">

View File

@@ -5,7 +5,7 @@
<string name="url_forpda" translatable="false">https://4pda.to/forum/index.php?showtopic=697669</string> <string name="url_forpda" translatable="false">https://4pda.to/forum/index.php?showtopic=697669</string>
<string name="url_weblate" translatable="false">https://hosted.weblate.org/engage/kotatsu</string> <string name="url_weblate" translatable="false">https://hosted.weblate.org/engage/kotatsu</string>
<string name="account_type_sync" translatable="false">org.kotatsu.sync</string> <string name="account_type_sync" translatable="false">org.kotatsu.sync</string>
<string name="url_sync_server" translatable="false">http://95.216.215.49:8080</string> <string name="url_sync_server" translatable="false">http://95.216.215.49:8055</string>
<string-array name="values_theme" translatable="false"> <string-array name="values_theme" translatable="false">
<item>-1</item> <item>-1</item>
<item>1</item> <item>1</item>

View File

@@ -303,4 +303,5 @@
<string name="default_mode">Default mode</string> <string name="default_mode">Default mode</string>
<string name="detect_reader_mode">Autodetect reader mode</string> <string name="detect_reader_mode">Autodetect reader mode</string>
<string name="detect_reader_mode_summary">Automatically detect if manga is webtoon</string> <string name="detect_reader_mode_summary">Automatically detect if manga is webtoon</string>
<string name="enter_email_text">Enter your email to continue</string>
</resources> </resources>

View File

@@ -44,7 +44,8 @@
android:key="sync" android:key="sync"
android:persistent="false" android:persistent="false"
android:summary="@string/sync_title" android:summary="@string/sync_title"
android:title="@string/sync" /> android:title="@string/sync"
app:allowDividerAbove="true" />
<PreferenceScreen <PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment" android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment"

View File

@@ -1,10 +1,2 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen />
xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:persistent="false"
android:summary="Preference stub"
android:title="TODO" />
</PreferenceScreen>