From f0e56c4b6a3c41f29671b697e6434c67e1beb18f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 12 May 2021 20:10:47 +0300 Subject: [PATCH] Suggestions --- .idea/misc.xml | 3 + .../java/org/koitharu/kotatsu/KotatsuApp.kt | 4 +- .../koitharu/kotatsu/core/db/MangaDatabase.kt | 4 ++ .../koitharu/kotatsu/core/prefs/AppSection.kt | 2 +- .../kotatsu/history/data/HistoryDao.kt | 4 ++ .../history/domain/HistoryRepository.kt | 5 ++ .../koitharu/kotatsu/main/ui/MainActivity.kt | 11 +++ .../kotatsu/suggestions/SuggestionsModule.kt | 14 ++++ .../kotatsu/suggestions/data/SuggestionDao.kt | 28 ++++++++ .../data}/SuggestionEntity.kt | 5 +- .../suggestions/data/SuggestionWithManga.kt | 23 +++++++ .../suggestions/domain/MangaSuggestion.kt | 10 +++ .../domain/SuggestionRepository.kt | 38 +++++++++++ .../suggestions/ui/SuggestionsFragment.kt | 28 ++++++++ .../suggestions/ui/SuggestionsViewModel.kt | 53 +++++++++++++++ .../suggestions/ui/SuggestionsWorker.kt | 68 +++++++++++++++++++ app/src/main/res/drawable/ic_suggestion.xml | 12 ++++ app/src/main/res/menu/nav_drawer.xml | 4 ++ app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 20 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/SuggestionsModule.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt rename app/src/main/java/org/koitharu/kotatsu/{core/db/entity => suggestions/data}/SuggestionEntity.kt (77%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt create mode 100644 app/src/main/res/drawable/ic_suggestion.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 7f598c7c9..44afccb0c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -9,6 +9,8 @@ + + @@ -33,6 +35,7 @@ + diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 311a9bdb6..85e7adf5d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.reader.readerModule import org.koitharu.kotatsu.remotelist.remoteListModule import org.koitharu.kotatsu.search.searchModule import org.koitharu.kotatsu.settings.settingsModule +import org.koitharu.kotatsu.suggestions.suggestionsModule import org.koitharu.kotatsu.tracker.trackerModule import org.koitharu.kotatsu.widget.WidgetUpdater import org.koitharu.kotatsu.widget.appWidgetModule @@ -65,7 +66,8 @@ class KotatsuApp : Application() { trackerModule, settingsModule, readerModule, - appWidgetModule + appWidgetModule, + suggestionsModule, ) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index f0844571e..9c40edaae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -10,6 +10,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.FavouritesDao import org.koitharu.kotatsu.history.data.HistoryDao import org.koitharu.kotatsu.history.data.HistoryEntity +import org.koitharu.kotatsu.suggestions.data.SuggestionDao +import org.koitharu.kotatsu.suggestions.data.SuggestionEntity @Database( entities = [ @@ -35,4 +37,6 @@ abstract class MangaDatabase : RoomDatabase() { abstract val tracksDao: TracksDao abstract val trackLogsDao: TrackLogsDao + + abstract val suggestionDao: SuggestionDao } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt index 64ce67264..0efa45c92 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt @@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.prefs enum class AppSection { - LOCAL, FAVOURITES, HISTORY, FEED + LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt index 9ee2642fb..0b973aa64 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.history.data import androidx.room.* import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity @Dao @@ -22,6 +23,9 @@ abstract class HistoryDao { @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)") abstract suspend fun findAllManga(): List + @Query("SELECT * FROM tags WHERE tag_id IN (SELECT tag_id FROM manga_tags WHERE manga_id IN (SELECT manga_id FROM history))") + abstract suspend fun findAllTags(): List + @Query("SELECT * FROM history WHERE manga_id = :id") abstract suspend fun find(id: Long): HistoryEntity? diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt index cece71003..ade87172b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt @@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.mapItems @@ -84,4 +85,8 @@ class HistoryRepository( db.historyDao.delete(manga.id) } } + + suspend fun getAllTags(): List { + return db.historyDao.findAllTags().map { x -> x.toMangaTag() } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 443ab118f..6abc5fae4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -32,6 +32,8 @@ import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.search.ui.SearchHelper import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment +import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -75,6 +77,7 @@ class MainActivity : BaseActivity(), } if (savedInstanceState == null) { TrackWorker.setup(applicationContext) + SuggestionsWorker.setup(applicationContext) AppUpdateChecker(this).launchIfNeeded() } @@ -144,6 +147,10 @@ class MainActivity : BaseActivity(), viewModel.defaultSection = AppSection.LOCAL setPrimaryFragment(LocalListFragment.newInstance()) } + R.id.nav_suggestions -> { + viewModel.defaultSection = AppSection.SUGGESTIONS + setPrimaryFragment(SuggestionsFragment.newInstance()) + } R.id.nav_feed -> { viewModel.defaultSection = AppSection.FEED setPrimaryFragment(FeedFragment.newInstance()) @@ -229,6 +236,10 @@ class MainActivity : BaseActivity(), binding.navigationView.setCheckedItem(R.id.nav_feed) setPrimaryFragment(FeedFragment.newInstance()) } + AppSection.SUGGESTIONS -> { + binding.navigationView.setCheckedItem(R.id.nav_suggestions) + setPrimaryFragment(SuggestionsFragment.newInstance()) + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/SuggestionsModule.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/SuggestionsModule.kt new file mode 100644 index 000000000..df0a2c870 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/SuggestionsModule.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.suggestions + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module +import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository +import org.koitharu.kotatsu.suggestions.ui.SuggestionsViewModel + +val suggestionsModule + get() = module { + + factory { SuggestionRepository(get()) } + + viewModel { SuggestionsViewModel(get(), get()) } + } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt new file mode 100644 index 000000000..bf5f2f055 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.suggestions.data + +import androidx.room.* +import kotlinx.coroutines.flow.Flow + +@Dao +abstract class SuggestionDao { + + @Transaction + @Query("SELECT * FROM suggestions ORDER BY relevance DESC") + abstract fun observeAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract suspend fun insert(entity: SuggestionEntity): Long + + @Update + abstract suspend fun update(entity: SuggestionEntity): Int + + @Query("DELETE FROM suggestions") + abstract suspend fun deleteAll() + + @Transaction + open suspend fun upsert(entity: SuggestionEntity) { + if (update(entity) == 0) { + insert(entity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/SuggestionEntity.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/SuggestionEntity.kt rename to app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt index 896732e15..174810bc2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/SuggestionEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt @@ -1,9 +1,11 @@ -package org.koitharu.kotatsu.core.db.entity +package org.koitharu.kotatsu.suggestions.data +import androidx.annotation.FloatRange import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.koitharu.kotatsu.core.db.entity.MangaEntity @Entity( tableName = "suggestions", @@ -19,6 +21,7 @@ import androidx.room.PrimaryKey data class SuggestionEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "manga_id", index = true) val mangaId: Long, + @FloatRange(from = 0.0, to = 1.0) @ColumnInfo(name = "relevance") val relevance: Float, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), ) diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt new file mode 100644 index 000000000..13aa11bec --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.suggestions.data + +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity + +data class SuggestionWithManga( + @Embedded val suggestion: SuggestionEntity, + @Relation( + parentColumn = "manga_id", + entityColumn = "manga_id" + ) + val manga: MangaEntity, + @Relation( + parentColumn = "manga_id", + entityColumn = "tag_id", + associateBy = Junction(MangaTagsEntity::class) + ) + val tags: List +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt new file mode 100644 index 000000000..689d8276a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.suggestions.domain + +import androidx.annotation.FloatRange +import org.koitharu.kotatsu.core.model.Manga + +data class MangaSuggestion( + val manga: Manga, + @FloatRange(from = 0.0, to = 1.0) + val relevance: Float, +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt new file mode 100644 index 000000000..4ea272ba5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt @@ -0,0 +1,38 @@ +package org.koitharu.kotatsu.suggestions.domain + +import androidx.room.withTransaction +import kotlinx.coroutines.flow.Flow +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.suggestions.data.SuggestionEntity +import org.koitharu.kotatsu.utils.ext.mapItems +import org.koitharu.kotatsu.utils.ext.mapToSet + +class SuggestionRepository( + private val db: MangaDatabase, +) { + + fun observeAll(): Flow> { + return db.suggestionDao.observeAll().mapItems { + it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) + } + } + + suspend fun replace(suggestions: Iterable) { + db.withTransaction { + db.suggestionDao.deleteAll() + suggestions.forEach { x -> + db.mangaDao.upsert(MangaEntity.from(x.manga)) + db.suggestionDao.upsert( + SuggestionEntity( + mangaId = x.manga.id, + relevance = x.relevance, + createdAt = System.currentTimeMillis(), + ) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt new file mode 100644 index 000000000..c53b705e5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.suggestions.ui + +import android.os.Bundle +import android.view.View +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.list.ui.MangaListFragment + +class SuggestionsFragment : MangaListFragment() { + + override val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) + override val isSwipeRefreshEnabled = false + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + } + + override fun onScrolledToEnd() = Unit + + override fun getTitle(): CharSequence? { + return context?.getString(R.string.suggestions) + } + + companion object { + + fun newInstance() = SuggestionsFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt new file mode 100644 index 000000000..cc3e52d2f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt @@ -0,0 +1,53 @@ +package org.koitharu.kotatsu.suggestions.ui + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.list.ui.model.* +import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository +import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.onFirst + +class SuggestionsViewModel( + repository: SuggestionRepository, + settings: AppSettings, +) : MangaListViewModel(settings) { + + override val content = combine( + repository.observeAll(), + createListModeFlow() + ) { list, mode -> + when { + list.isEmpty() -> listOf(EmptyState(R.string.text_suggestion_holder)) + else -> mapList(list, mode) + } + }.onFirst { + isLoading.postValue(false) + }.catch { + it.toErrorState(canRetry = false) + }.asLiveDataDistinct( + viewModelScope.coroutineContext + Dispatchers.Default, + listOf(LoadingState) + ) + + override fun onRefresh() = Unit + + override fun onRetry() = Unit + + private fun mapList( + list: List, + mode: ListMode, + ): List = list.map { manga -> + when (mode) { + ListMode.LIST -> manga.toListModel() + ListMode.DETAILED_LIST -> manga.toListDetailedModel() + ListMode.GRID -> manga.toGridModel() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt new file mode 100644 index 000000000..895508592 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -0,0 +1,68 @@ +package org.koitharu.kotatsu.suggestions.ui + +import android.content.Context +import androidx.work.* +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.SortOrder +import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion +import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository +import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf +import java.util.concurrent.TimeUnit +import kotlin.math.pow + +class SuggestionsWorker(appContext: Context, params: WorkerParameters) : + CoroutineWorker(appContext, params), KoinComponent { + + private val suggestionRepository by inject() + private val historyRepository by inject() + + override suspend fun doWork(): Result { + val rawResults = ArrayList() + val allTags = historyRepository.getAllTags() + val tagsBySources = allTags.groupBy { x -> x.source } + for ((source, tags) in tagsBySources) { + val repo = mangaRepositoryOf(source) + tags.flatMapTo(rawResults) { tag -> + repo.getList( + offset = 0, + sortOrder = SortOrder.UPDATED, + tag = tag, + ) + } + } + suggestionRepository.replace( + rawResults.distinctBy { manga -> + manga.id + }.map { manga -> + val jointTags = manga.tags intersect allTags + MangaSuggestion( + manga = manga, + relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(), + ) + } + ) + return Result.success() + } + + companion object { + + private const val TAG = "suggestions" + + fun setup(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresBatteryNotLow(true) + .build() + val request = PeriodicWorkRequestBuilder(6, TimeUnit.HOURS) + .setConstraints(constraints) + .addTag(TAG) + .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) + .build() + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_suggestion.xml b/app/src/main/res/drawable/ic_suggestion.xml new file mode 100644 index 000000000..a93a75799 --- /dev/null +++ b/app/src/main/res/drawable/ic_suggestion.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer.xml b/app/src/main/res/menu/nav_drawer.xml index 75869c7a7..1e0db493c 100644 --- a/app/src/main/res/menu/nav_drawer.xml +++ b/app/src/main/res/menu/nav_drawer.xml @@ -14,6 +14,10 @@ android:id="@+id/nav_history" android:icon="@drawable/ic_history" android:title="@string/history" /> + Enter password that will be required when the application starts Confirm Password must be at least 4 characters + Suggestions + Start reading manga and you will get personalized suggestions \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 161e209c5..fce999bca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -207,4 +207,6 @@ Enter password that will be required when the application starts Confirm Password must be at least 4 characters + Suggestions + Start reading manga and you will get personalized suggestions \ No newline at end of file