mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 12:11:49 +01:00
Remove update library option + Library updater now uses a queue system
Also reordered group notifaction so it always sends first
This commit is contained in:
parent
0aca015171
commit
adc3a522cd
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
@ -14,7 +15,6 @@ import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
@ -41,13 +42,17 @@ import eu.kanade.tachiyomi.util.notification
|
|||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -94,6 +99,8 @@ class LibraryUpdateService(
|
|||||||
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var job:Job? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cached progress notification to avoid creating a lot.
|
* Cached progress notification to avoid creating a lot.
|
||||||
*/
|
*/
|
||||||
@ -123,6 +130,8 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
const val KEY_CATEGORY = "category"
|
const val KEY_CATEGORY = "category"
|
||||||
|
|
||||||
|
private val mangaToUpdate = mutableListOf<LibraryManga>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key that defines what should be updated.
|
* Key that defines what should be updated.
|
||||||
*/
|
*/
|
||||||
@ -158,6 +167,21 @@ class LibraryUpdateService(
|
|||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if (target == Target.CHAPTERS) category?.id?.let {
|
||||||
|
val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||||
|
addManga(getMangaToUpdate(it, target).sortedWith(
|
||||||
|
rankingScheme[selectedScheme]
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addManga(mangaToAdd: List<LibraryManga>) {
|
||||||
|
for (manga in mangaToAdd) {
|
||||||
|
if (mangaToUpdate.none { it.id == manga.id }) mangaToUpdate.add(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -169,6 +193,38 @@ class LibraryUpdateService(
|
|||||||
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of manga to be updated.
|
||||||
|
*
|
||||||
|
* @param intent the update intent.
|
||||||
|
* @param target the target to update.
|
||||||
|
* @return a list of manga to update
|
||||||
|
*/
|
||||||
|
private fun getMangaToUpdate(categoryId: Int, target: Target): List<LibraryManga> {
|
||||||
|
val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
val db: DatabaseHelper = Injekt.get()
|
||||||
|
var listToUpdate = if (categoryId != -1)
|
||||||
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
|
else {
|
||||||
|
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
|
||||||
|
if (categoriesToUpdate.isNotEmpty())
|
||||||
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
|
.filter { it.category in categoriesToUpdate }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
else
|
||||||
|
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
|
}
|
||||||
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
|
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||||
|
}
|
||||||
|
|
||||||
|
return listToUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
||||||
|
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||||
|
return getMangaToUpdate(categoryId, target)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,12 +239,18 @@ class LibraryUpdateService(
|
|||||||
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun stopService(name: Intent?): Boolean {
|
||||||
|
job?.cancel()
|
||||||
|
return super.stopService(name)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service is destroyed. It destroys subscriptions and releases the wake
|
* Method called when the service is destroyed. It destroys subscriptions and releases the wake
|
||||||
* lock.
|
* lock.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
|
mangaToUpdate.clear()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@ -212,77 +274,56 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return START_NOT_STICKY
|
if (intent == null) return START_NOT_STICKY
|
||||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
val target = intent.getSerializableExtra(KEY_TARGET) as? Target ?: return START_NOT_STICKY
|
||||||
?: return START_NOT_STICKY
|
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
|
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
// Update either chapter list or manga details.
|
|
||||||
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||||
|
if (target == Target.CHAPTERS) {
|
||||||
|
updateChapters(
|
||||||
|
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme]), startId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Update either chapter list or manga details.
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
val mangaList = getMangaToUpdate(intent, target)
|
val mangaList =
|
||||||
.sortedWith(rankingScheme[selectedScheme])
|
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme])
|
||||||
subscription = Observable.defer {
|
subscription = Observable.defer {
|
||||||
when (target) {
|
when (target) {
|
||||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
|
||||||
Target.DETAILS -> updateDetails(mangaList)
|
Target.DETAILS -> updateDetails(mangaList)
|
||||||
else -> updateTrackings(mangaList)
|
else -> updateTrackings(mangaList)
|
||||||
}
|
}
|
||||||
}.subscribeOn(Schedulers.io()).subscribe({}, {
|
}.subscribeOn(Schedulers.io()).subscribe({}, {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}, {
|
}, {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun updateChapters(mangaToAdd: List<LibraryManga>, startId: Int) {
|
||||||
* Returns the list of manga to be updated.
|
addManga(mangaToAdd)
|
||||||
*
|
|
||||||
* @param intent the update intent.
|
|
||||||
* @param target the target to update.
|
|
||||||
* @return a list of manga to update
|
|
||||||
*/
|
|
||||||
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
|
||||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
|
||||||
|
|
||||||
var listToUpdate = if (categoryId != -1)
|
if (job == null) {
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) {
|
||||||
else {
|
updateChaptersJob()
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
|
mangaToUpdate.clear()
|
||||||
if (categoriesToUpdate.isNotEmpty())
|
stopSelf(startId)
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
}
|
||||||
.filter { it.category in categoriesToUpdate }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
else
|
|
||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
|
||||||
}
|
}
|
||||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
|
||||||
}
|
|
||||||
|
|
||||||
return listToUpdate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun updateChaptersJob() {
|
||||||
* Method that updates the given list of manga. It's called in a background thread, so it's safe
|
|
||||||
* to do heavy operations or network calls here.
|
|
||||||
* For each manga it calls [updateManga] and updates the notification showing the current
|
|
||||||
* progress.
|
|
||||||
*
|
|
||||||
* @param mangaToUpdate the list to update
|
|
||||||
* @return an observable delivering the progress of each update.
|
|
||||||
*/
|
|
||||||
fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
|
||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
var count = 0
|
||||||
// List containing new updates
|
// List containing new updates
|
||||||
val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
// list containing failed updates
|
// list containing failed updates
|
||||||
@ -294,64 +335,49 @@ class LibraryUpdateService(
|
|||||||
// Boolean to determine if DownloadManager has downloads
|
// Boolean to determine if DownloadManager has downloads
|
||||||
var hasDownloads = false
|
var hasDownloads = false
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
while (count < mangaToUpdate.size && count + 1 >= mangaToUpdate.size) {
|
||||||
return Observable.from(mangaToUpdate)
|
if (job?.isCancelled == true || job == null) break
|
||||||
// Notify manga that will update.
|
val manga = mangaToUpdate[count]
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
showProgressNotification(manga, count++, mangaToUpdate.size)
|
||||||
// Update the chapters of the manga.
|
val source = sourceManager.get(manga.source) as? HttpSource ?: continue
|
||||||
.concatMap { manga ->
|
val fetchedChapters = try { source.fetchChapterList(manga).toBlocking().single() }
|
||||||
updateManga(manga)
|
catch(e: java.lang.Exception) {
|
||||||
// If there's any error, return empty update and continue.
|
failedUpdates.add(manga)
|
||||||
.onErrorReturn {
|
emptyList<SChapter>() }
|
||||||
failedUpdates.add(manga)
|
if (fetchedChapters.isNotEmpty()) {
|
||||||
Pair(emptyList(), emptyList())
|
val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source).first
|
||||||
}
|
if (newChapters.isNotEmpty()) {
|
||||||
// Filter out mangas without new chapters (or failed).
|
if (downloadNew && (categoriesToDownload.isEmpty() || manga.category in categoriesToDownload)) {
|
||||||
.filter { pair -> pair.first.isNotEmpty() }
|
downloadChapters(manga, newChapters)
|
||||||
.doOnNext {
|
hasDownloads = true
|
||||||
if (downloadNew && (categoriesToDownload.isEmpty() ||
|
}
|
||||||
manga.category in categoriesToDownload)) {
|
newUpdates.add(manga to newChapters.toTypedArray())
|
||||||
|
|
||||||
downloadChapters(manga, it.first)
|
|
||||||
hasDownloads = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { Pair(manga, (it.first.sortedByDescending { ch -> ch
|
|
||||||
.source_order }.toTypedArray())) }
|
|
||||||
}
|
}
|
||||||
// Add manga with new chapters to the list.
|
}
|
||||||
.doOnNext { manga ->
|
}
|
||||||
// Add to the list
|
if (newUpdates.isNotEmpty()) {
|
||||||
newUpdates.add(manga)
|
showResultNotification(newUpdates)
|
||||||
}
|
|
||||||
// Notify result of the overall update.
|
|
||||||
.doOnCompleted {
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
|
||||||
showResultNotification(newUpdates)
|
|
||||||
|
|
||||||
if (preferences.refreshCoversToo().getOrDefault()) {
|
if (preferences.refreshCoversToo().getOrDefault()) {
|
||||||
updateDetails(newUpdates.map { it.first }).observeOn(Schedulers.io())
|
updateDetails(newUpdates.map { it.first }).observeOn(Schedulers.io())
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
cancelProgressNotification()
|
cancelProgressNotification()
|
||||||
if (downloadNew && hasDownloads) {
|
if (downloadNew && hasDownloads) {
|
||||||
DownloadService.start(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io()).subscribe {}
|
|
||||||
}
|
|
||||||
else if (downloadNew && hasDownloads) {
|
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.subscribeOn(Schedulers.io()).subscribe {}
|
||||||
|
}
|
||||||
|
else if (downloadNew && hasDownloads) {
|
||||||
|
DownloadService.start(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (failedUpdates.isNotEmpty()) {
|
if (failedUpdates.isNotEmpty()) {
|
||||||
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
|
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelProgressNotification()
|
cancelProgressNotification()
|
||||||
}
|
|
||||||
.map { manga -> manga.first }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanupDownloads() {
|
private fun cleanupDownloads() {
|
||||||
@ -520,9 +546,6 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
NotificationManagerCompat.from(this).apply {
|
NotificationManagerCompat.from(this).apply {
|
||||||
notifications.forEach {
|
|
||||||
notify(it.second, it.first)
|
|
||||||
}
|
|
||||||
|
|
||||||
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
@ -542,10 +565,15 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
priority = NotificationCompat.PRIORITY_HIGH
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||||
|
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
||||||
setGroupSummary(true)
|
setGroupSummary(true)
|
||||||
setContentIntent(getNotificationIntent())
|
setContentIntent(getNotificationIntent())
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
notifications.forEach {
|
||||||
|
notify(it.second, it.first)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,12 +119,9 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
// Double the distance required to trigger sync
|
// Double the distance required to trigger sync
|
||||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||||
swipe_refresh.setOnRefreshListener {
|
swipe_refresh.setOnRefreshListener {
|
||||||
if (!LibraryUpdateService.isRunning(context)) {
|
LibraryUpdateService.start(context, category)
|
||||||
LibraryUpdateService.start(context, category)
|
controller.snack?.dismiss()
|
||||||
controller.snack?.dismiss()
|
controller.snack = swipe_refresh.snack(R.string.updating_category)
|
||||||
controller.snack = swipe_refresh.snack(R.string.updating_category)
|
|
||||||
}
|
|
||||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
|
||||||
swipe_refresh.isRefreshing = false
|
swipe_refresh.isRefreshing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -476,10 +476,6 @@ class LibraryController(
|
|||||||
R.id.action_filter -> {
|
R.id.action_filter -> {
|
||||||
navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) }
|
navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) }
|
||||||
}
|
}
|
||||||
R.id.action_update_library -> {
|
|
||||||
activity?.let { LibraryUpdateService.start(it) }
|
|
||||||
snack = view?.snack(R.string.updating_library)
|
|
||||||
}
|
|
||||||
R.id.action_edit_categories -> {
|
R.id.action_edit_categories -> {
|
||||||
router.pushController(CategoryController().withFadeTransaction())
|
router.pushController(CategoryController().withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,6 @@
|
|||||||
android:title="@string/action_filter"
|
android:title="@string/action_filter"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_update_library"
|
|
||||||
android:icon="@drawable/ic_refresh_white_24dp"
|
|
||||||
android:title="@string/action_update_library"
|
|
||||||
app:showAsAction="ifRoom"/>
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_edit_categories"
|
android:id="@+id/action_edit_categories"
|
||||||
android:title="@string/action_edit_categories"
|
android:title="@string/action_edit_categories"
|
||||||
|
Loading…
Reference in New Issue
Block a user