music: clean up loader implementation
Clean up the music loader implementation, removing pre-sorting to make it a bit more efficent. Instead, sorting is done on indiviual components.
This commit is contained in:
parent
cde3a99f4d
commit
637bcccd51
12 changed files with 155 additions and 191 deletions
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.Album
|
|||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
|
@ -76,7 +77,10 @@ class ArtistImageFetcher private constructor(
|
|||
private val artist: Artist,
|
||||
) : AuxioFetcher() {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
val results = artist.albums.mapAtMost(4) { album ->
|
||||
val albums = Sort.ByName(true)
|
||||
.sortAlbums(artist.albums)
|
||||
|
||||
val results = albums.mapAtMost(4) { album ->
|
||||
fetchArt(context, album)
|
||||
}
|
||||
|
||||
|
@ -100,6 +104,7 @@ class GenreImageFetcher private constructor(
|
|||
private val genre: Genre,
|
||||
) : AuxioFetcher() {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
// We don't need to sort here, as the way we
|
||||
val albums = genre.songs.groupBy { it.album }.keys
|
||||
val results = albums.mapAtMost(4) { album ->
|
||||
fetchArt(context, album)
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
package org.oxycblt.auxio.excluded
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
|
@ -32,14 +31,13 @@ import androidx.core.view.isVisible
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.MainActivity
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogExcludedBinding
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.LifecycleDialog
|
||||
import org.oxycblt.auxio.util.hardRestart
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Dialog that manages the currently excluded directories.
|
||||
|
@ -150,25 +148,12 @@ class ExcludedDialog : LifecycleDialog() {
|
|||
|
||||
private fun saveAndRestart() {
|
||||
excludedModel.save {
|
||||
playbackModel.savePlaybackState(requireContext(), ::hardRestart)
|
||||
playbackModel.savePlaybackState(requireContext()) {
|
||||
requireContext().hardRestart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hardRestart() {
|
||||
logD("Performing hard restart.")
|
||||
|
||||
// Instead of having to do a ton of cleanup and horrible code changes
|
||||
// to restart this application non-destructively, I just restart the UI task [There is only
|
||||
// one, after all] and then kill the application using exitProcess. Works well enough.
|
||||
val intent = Intent(requireContext().applicationContext, MainActivity::class.java).setFlags(
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
|
||||
startActivity(intent)
|
||||
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get *just* the root path, nothing else is really needed.
|
||||
*/
|
||||
|
|
|
@ -1,37 +1,14 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Auxio Project
|
||||
* MusicLoader.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.Audio.Genres
|
||||
import android.provider.MediaStore.Audio.Media
|
||||
import androidx.core.database.getStringOrNull
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.excluded.ExcludedDatabase
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* This class does pretty much all the black magic required to get a remotely sensible music
|
||||
* indexing system while still optimizing for time. I would recommend you leave this file now
|
||||
* This class acts as the base for most the black magic required to get a remotely sensible music
|
||||
* indexing system while still optimizing for time. I would recommend you leave this module now
|
||||
* before you lose your sanity trying to understand the hoops I had to jump through for this
|
||||
* system, but if you really want to stay, here's a debrief on why this code is so awful.
|
||||
*
|
||||
|
@ -90,111 +67,72 @@ import org.oxycblt.auxio.util.logD
|
|||
*
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class MusicLoader(private val context: Context) {
|
||||
var genres = mutableListOf<Genre>()
|
||||
var albums = mutableListOf<Album>()
|
||||
var artists = mutableListOf<Artist>()
|
||||
var songs = mutableListOf<Song>()
|
||||
class MusicLoader {
|
||||
data class Library(
|
||||
val genres: List<Genre>,
|
||||
val artists: List<Artist>,
|
||||
val albums: List<Album>,
|
||||
val songs: List<Song>
|
||||
)
|
||||
|
||||
private val resolver = context.contentResolver
|
||||
fun load(context: Context): Library? {
|
||||
val songs = loadSongs(context)
|
||||
if (songs.isEmpty()) return null
|
||||
|
||||
private var selector = "${Media.IS_MUSIC}=1"
|
||||
private var args = arrayOf<String>()
|
||||
val albums = buildAlbums(songs)
|
||||
val artists = buildArtists(context, albums)
|
||||
val genres = readGenres(context, songs)
|
||||
|
||||
/**
|
||||
* Begin the loading process.
|
||||
* Resulting models are pushed to [genres], [artists], [albums], and [songs].
|
||||
*/
|
||||
fun load() {
|
||||
buildSelector()
|
||||
|
||||
loadGenres()
|
||||
loadSongs()
|
||||
linkGenres()
|
||||
|
||||
buildAlbums()
|
||||
buildArtists()
|
||||
return Library(
|
||||
genres,
|
||||
artists,
|
||||
albums,
|
||||
songs
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun buildSelector() {
|
||||
private fun loadSongs(context: Context): List<Song> {
|
||||
var songs = mutableListOf<Song>()
|
||||
val blacklistDatabase = ExcludedDatabase.getInstance(context)
|
||||
|
||||
val paths = blacklistDatabase.readPaths()
|
||||
|
||||
// DATA was deprecated on Android Q, but is set to be un-deprecated in Android 12L
|
||||
var selector = "${MediaStore.Audio.Media.IS_MUSIC}=1"
|
||||
val args = mutableListOf<String>()
|
||||
|
||||
// DATA was deprecated on Android 10, but is set to be un-deprecated in Android 12L.
|
||||
// The only reason we'd want to change this is to add external partitions support, but
|
||||
// that's less efficent and there's no demand for that right now.
|
||||
for (path in paths) {
|
||||
selector += " AND ${Media.DATA} NOT LIKE ?"
|
||||
selector += " AND ${MediaStore.Audio.Media.DATA} NOT LIKE ?"
|
||||
args += "$path%" // Append % so that the selector properly detects children
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadGenres() {
|
||||
logD("Starting genre search...")
|
||||
|
||||
// First, get a cursor for every genre in the android system
|
||||
val genreCursor = resolver.query(
|
||||
Genres.EXTERNAL_CONTENT_URI,
|
||||
context.contentResolver.query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
arrayOf(
|
||||
Genres._ID, // 0
|
||||
Genres.NAME // 1
|
||||
MediaStore.Audio.Media._ID,
|
||||
MediaStore.Audio.Media.TITLE,
|
||||
MediaStore.Audio.Media.DISPLAY_NAME,
|
||||
MediaStore.Audio.Media.ALBUM,
|
||||
MediaStore.Audio.Media.ALBUM_ID,
|
||||
MediaStore.Audio.Media.ARTIST,
|
||||
MediaStore.Audio.Media.ALBUM_ARTIST,
|
||||
MediaStore.Audio.Media.YEAR,
|
||||
MediaStore.Audio.Media.TRACK,
|
||||
MediaStore.Audio.Media.DURATION,
|
||||
),
|
||||
null, null,
|
||||
Genres.DEFAULT_SORT_ORDER
|
||||
)
|
||||
|
||||
// And then process those into Genre objects
|
||||
genreCursor?.use { cursor ->
|
||||
val idIndex = cursor.getColumnIndexOrThrow(Genres._ID)
|
||||
val nameIndex = cursor.getColumnIndexOrThrow(Genres.NAME)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idIndex)
|
||||
// No non-broken genre would be missing a name.
|
||||
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
||||
|
||||
genres.add(Genre(id, name, name.getGenreNameCompat() ?: name))
|
||||
}
|
||||
}
|
||||
|
||||
logD("Genre search finished with ${genres.size} genres found.")
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun loadSongs() {
|
||||
logD("Starting song search...")
|
||||
|
||||
val songCursor = resolver.query(
|
||||
Media.EXTERNAL_CONTENT_URI,
|
||||
arrayOf(
|
||||
Media._ID, // 0
|
||||
Media.TITLE, // 1
|
||||
Media.DISPLAY_NAME, // 2
|
||||
Media.ALBUM, // 3
|
||||
Media.ALBUM_ID, // 4
|
||||
Media.ARTIST, // 5
|
||||
Media.ALBUM_ARTIST, // 6
|
||||
Media.YEAR, // 7
|
||||
Media.TRACK, // 8
|
||||
Media.DURATION, // 9
|
||||
),
|
||||
selector, args,
|
||||
Media.DEFAULT_SORT_ORDER
|
||||
)
|
||||
|
||||
songCursor?.use { cursor ->
|
||||
val idIndex = cursor.getColumnIndexOrThrow(Media._ID)
|
||||
val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE)
|
||||
val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME)
|
||||
val albumIndex = cursor.getColumnIndexOrThrow(Media.ALBUM)
|
||||
val albumIdIndex = cursor.getColumnIndexOrThrow(Media.ALBUM_ID)
|
||||
val artistIndex = cursor.getColumnIndexOrThrow(Media.ARTIST)
|
||||
val albumArtistIndex = cursor.getColumnIndexOrThrow(Media.ALBUM_ARTIST)
|
||||
val yearIndex = cursor.getColumnIndexOrThrow(Media.YEAR)
|
||||
val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK)
|
||||
val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION)
|
||||
selector, args.toTypedArray(), null
|
||||
)?.use { cursor ->
|
||||
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
|
||||
val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
|
||||
val fileIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||
val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)
|
||||
val albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
|
||||
val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
|
||||
val albumArtistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ARTIST)
|
||||
val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.YEAR)
|
||||
val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TRACK)
|
||||
val durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idIndex)
|
||||
|
@ -226,20 +164,19 @@ class MusicLoader(private val context: Context) {
|
|||
it.name to it.albumName to it.artistName to it.track to it.duration
|
||||
}.toMutableList()
|
||||
|
||||
logD("Song search finished with ${songs.size} found")
|
||||
return songs
|
||||
}
|
||||
|
||||
private fun buildAlbums() {
|
||||
logD("Linking albums")
|
||||
|
||||
private fun buildAlbums(songs: List<Song>): List<Album> {
|
||||
// Group up songs by their album ids and then link them with their albums
|
||||
val albums = mutableListOf<Album>()
|
||||
val songsByAlbum = songs.groupBy { it.albumId }
|
||||
|
||||
songsByAlbum.forEach { entry ->
|
||||
// Rely on the first song in this list for album information.
|
||||
// Note: This might result in a bad year being used for an album if an album's songs
|
||||
// have multiple years. This is fixable but is currently omitted for speed.
|
||||
val song = entry.value[0]
|
||||
// Use the song with the latest year as our metadata song.
|
||||
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
|
||||
// weird years like "0" wont show up if there are alternatives.
|
||||
val song = requireNotNull(entry.value.maxByOrNull { it.year })
|
||||
|
||||
albums.add(
|
||||
Album(
|
||||
|
@ -253,14 +190,12 @@ class MusicLoader(private val context: Context) {
|
|||
}
|
||||
|
||||
albums.removeAll { it.songs.isEmpty() }
|
||||
albums = Sort.ByName(true).sortAlbums(albums).toMutableList()
|
||||
|
||||
logD("Songs successfully linked into ${albums.size} albums")
|
||||
return albums
|
||||
}
|
||||
|
||||
private fun buildArtists() {
|
||||
logD("Linking artists")
|
||||
|
||||
private fun buildArtists(context: Context, albums: List<Album>): List<Artist> {
|
||||
val artists = mutableListOf<Artist>()
|
||||
val albumsByArtist = albums.groupBy { it.artistName }
|
||||
|
||||
albumsByArtist.forEach { entry ->
|
||||
|
@ -270,8 +205,8 @@ class MusicLoader(private val context: Context) {
|
|||
entry.key
|
||||
}
|
||||
|
||||
// Because of our hacky album artist system, MediaStore artist IDs are unreliable.
|
||||
// Therefore we just use the hashCode of the artist name as our ID and move on.
|
||||
// In most cases, MediaStore artist IDs are unreliable or omitted for speed.
|
||||
// Use the hashCode of the artist name as our ID and move on.
|
||||
artists.add(
|
||||
Artist(
|
||||
id = entry.key.hashCode().toLong(),
|
||||
|
@ -282,36 +217,42 @@ class MusicLoader(private val context: Context) {
|
|||
)
|
||||
}
|
||||
|
||||
artists = Sort.ByName(true).sortParents(artists).toMutableList()
|
||||
|
||||
logD("Albums successfully linked into ${artists.size} artists")
|
||||
return artists
|
||||
}
|
||||
|
||||
private fun linkGenres() {
|
||||
logD("Linking genres")
|
||||
private fun readGenres(context: Context, songs: List<Song>): List<Genre> {
|
||||
val genres = mutableListOf<Genre>()
|
||||
|
||||
genres.forEach { genre ->
|
||||
val songCursor = resolver.query(
|
||||
Genres.Members.getContentUri("external", genre.id),
|
||||
arrayOf(Genres.Members._ID),
|
||||
null, null, null // Dont even bother blacklisting here as useless iters are less expensive than IO
|
||||
)
|
||||
// First, get a cursor for every genre in the android system
|
||||
val genreCursor = context.contentResolver.query(
|
||||
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
||||
arrayOf(
|
||||
MediaStore.Audio.Genres._ID,
|
||||
MediaStore.Audio.Genres.NAME
|
||||
),
|
||||
null, null, null
|
||||
)
|
||||
|
||||
songCursor?.use { cursor ->
|
||||
val idIndex = cursor.getColumnIndexOrThrow(Genres.Members._ID)
|
||||
// And then process those into Genre objects
|
||||
genreCursor?.use { cursor ->
|
||||
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
|
||||
val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idIndex)
|
||||
while (cursor.moveToNext()) {
|
||||
// No non-broken genre would be missing a name.
|
||||
val id = cursor.getLong(idIndex)
|
||||
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
||||
|
||||
songs.find { it.id == id }?.let { song ->
|
||||
genre.linkSong(song)
|
||||
}
|
||||
}
|
||||
val genre = Genre(
|
||||
id, name, name.getGenreNameCompat() ?: name
|
||||
)
|
||||
|
||||
linkGenre(context, genre, songs)
|
||||
genres.add(genre)
|
||||
}
|
||||
}
|
||||
|
||||
// Songs that don't have a genre will be thrown into an unknown genre.
|
||||
|
||||
val unknownGenre = Genre(
|
||||
id = Long.MIN_VALUE,
|
||||
name = MediaStore.UNKNOWN_STRING,
|
||||
|
@ -327,5 +268,27 @@ class MusicLoader(private val context: Context) {
|
|||
if (unknownGenre.songs.isNotEmpty()) {
|
||||
genres.add(unknownGenre)
|
||||
}
|
||||
|
||||
return genres
|
||||
}
|
||||
|
||||
private fun linkGenre(context: Context, genre: Genre, songs: List<Song>) {
|
||||
val songCursor = context.contentResolver.query(
|
||||
MediaStore.Audio.Genres.Members.getContentUri("external", genre.id),
|
||||
arrayOf(MediaStore.Audio.Genres.Members._ID),
|
||||
null, null, null // Dont even bother blacklisting here as useless iters are less expensive than IO
|
||||
)
|
||||
|
||||
songCursor?.use { cursor ->
|
||||
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idIndex)
|
||||
|
||||
songs.find { it.id == id }?.let { song ->
|
||||
genre.linkSong(song)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,17 +68,13 @@ class MusicStore private constructor() {
|
|||
try {
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
val loader = MusicLoader(context)
|
||||
loader.load()
|
||||
val loader = MusicLoader()
|
||||
val library = loader.load(context) ?: return Response.Err(ErrorKind.NO_MUSIC)
|
||||
|
||||
if (loader.songs.isEmpty()) {
|
||||
return Response.Err(ErrorKind.NO_MUSIC)
|
||||
}
|
||||
|
||||
mSongs = loader.songs
|
||||
mAlbums = loader.albums
|
||||
mArtists = loader.artists
|
||||
mGenres = loader.genres
|
||||
mSongs = library.songs
|
||||
mAlbums = library.albums
|
||||
mArtists = library.artists
|
||||
mGenres = library.genres
|
||||
|
||||
logD("Music load completed successfully in ${System.currentTimeMillis() - start}ms.")
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import java.text.Normalizer
|
||||
|
||||
/**
|
||||
|
@ -77,6 +78,7 @@ class SearchViewModel : ViewModel() {
|
|||
|
||||
// Searching can be quite expensive, so hop on a co-routine
|
||||
viewModelScope.launch {
|
||||
val sort = Sort.ByName(true)
|
||||
val results = mutableListOf<BaseModel>()
|
||||
|
||||
// A filter mode of null means to not filter at all.
|
||||
|
@ -84,28 +86,28 @@ class SearchViewModel : ViewModel() {
|
|||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
||||
musicStore.artists.filterByOrNull(query)?.let { artists ->
|
||||
results.add(Header(-1, HeaderString.Single(R.string.lbl_artists)))
|
||||
results.addAll(artists)
|
||||
results.addAll(sort.sortParents(artists))
|
||||
}
|
||||
}
|
||||
|
||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
||||
musicStore.albums.filterByOrNull(query)?.let { albums ->
|
||||
results.add(Header(-2, HeaderString.Single(R.string.lbl_albums)))
|
||||
results.addAll(albums)
|
||||
results.addAll(sort.sortAlbums(albums))
|
||||
}
|
||||
}
|
||||
|
||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
||||
musicStore.genres.filterByOrNull(query)?.let { genres ->
|
||||
results.add(Header(-3, HeaderString.Single(R.string.lbl_genres)))
|
||||
results.addAll(genres)
|
||||
results.addAll(sort.sortParents(genres))
|
||||
}
|
||||
}
|
||||
|
||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
||||
musicStore.songs.filterByOrNull(query)?.let { songs ->
|
||||
results.add(Header(-4, HeaderString.Single(R.string.lbl_songs)))
|
||||
results.addAll(songs)
|
||||
results.addAll(sort.sortSongs(songs))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,7 +138,7 @@ class SearchViewModel : ViewModel() {
|
|||
* Shortcut that will run a ignoreCase filter on a list and only return
|
||||
* a value if the resulting list is empty.
|
||||
*/
|
||||
private fun List<Music>.filterByOrNull(value: String): List<BaseModel>? {
|
||||
private fun <T : Music> List<T>.filterByOrNull(value: String): List<T>? {
|
||||
val filtered = filter {
|
||||
// Ensure the name we match with is correct.
|
||||
val name = if (it is MusicParent) {
|
||||
|
|
|
@ -30,6 +30,7 @@ import androidx.annotation.StringRes
|
|||
import androidx.core.content.ContextCompat
|
||||
import org.oxycblt.auxio.MainActivity
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
const val INTENT_REQUEST_CODE = 0xA0A0
|
||||
|
||||
|
@ -83,6 +84,19 @@ fun Context.newMainIntent(): PendingIntent {
|
|||
)
|
||||
}
|
||||
|
||||
fun Context.hardRestart() {
|
||||
// Instead of having to do a ton of cleanup and horrible code changes
|
||||
// to restart this application non-destructively, I just restart the UI task [There is only
|
||||
// one, after all] and then kill the application using exitProcess. Works well enough.
|
||||
val intent = Intent(applicationContext, MainActivity::class.java).setFlags(
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
|
||||
startActivity(intent)
|
||||
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a toast using the provided string resource.
|
||||
*/
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<string name="set_theme_night">"Tmavé"</string>
|
||||
<string name="set_accent">"Barva"</string>
|
||||
<string name="set_black_mode">"Černé téma"</string>
|
||||
<string name="setting_black_mode_desc">"Použít kompletně černé tmavé téma"</string>
|
||||
<string name="set_black_mode_desc">"Použít kompletně černé tmavé téma"</string>
|
||||
<string name="set_display">"Zobrazení"</string>
|
||||
<string name="set_show_covers">"Zobrazit barvy alba"</string>
|
||||
<string name="set_show_covers_desc">"Vypněte pro ušetření paměti"</string>
|
||||
|
|
|
@ -154,7 +154,7 @@
|
|||
<string name="lbl_sort_album">Album</string>
|
||||
<string name="lbl_sort_year">Jahr</string>
|
||||
<string name="set_black_mode">Schwarzes Thema</string>
|
||||
<string name="setting_black_mode_desc">Ein schwarzes Thema für das dunkle verwenden</string>
|
||||
<string name="set_black_mode_desc">Ein schwarzes Thema für das dunkle verwenden</string>
|
||||
<string name="set_loop_pause">Pause bei Wiederholung</string>
|
||||
<string name="set_loop_pause_desc">Pausiert, wenn ein Song wiederholt wird</string>
|
||||
<string name="desc_shuffle">Zufällig an- oder ausschalten</string>
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<string name="set_theme_night">Oscuro</string>
|
||||
<string name="set_accent">Acento</string>
|
||||
<string name="set_black_mode">Tema negro</string>
|
||||
<string name="setting_black_mode_desc">Usar tema negro puro</string>
|
||||
<string name="set_black_mode_desc">Usar tema negro puro</string>
|
||||
|
||||
<string name="set_display">Pantalla</string>
|
||||
<string name="set_show_covers">Mostrar carátula de álbum</string>
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
<string name="set_theme_night">Dark</string>
|
||||
<string name="set_accent">Color scheme</string>
|
||||
<string name="set_black_mode">Black theme</string>
|
||||
<string name="setting_black_mode_desc">Use a pure-black dark theme</string>
|
||||
<string name="set_black_mode_desc">Use a pure-black dark theme</string>
|
||||
|
||||
<string name="set_display">Display</string>
|
||||
<string name="set_lib_tabs">Library tabs</string>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
app:defaultValue="false"
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="KEY_BLACK_THEME"
|
||||
app:summary="@string/setting_black_mode_desc"
|
||||
app:summary="@string/set_black_mode_desc"
|
||||
app:title="@string/set_black_mode" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
|
|
@ -16,8 +16,7 @@ This is probably caused by one of two reasons:
|
|||
|
||||
#### I have a large library and Auxio takes really long to load it!
|
||||
|
||||
This is expected since reading media takes awhile, especially with libraries containing 10k songs or more.
|
||||
I hope to mitigate this in the future by allowing one to customize the music loader to optimize for speed instead of accuracy.
|
||||
This is expected since reading from the audio database takes awhile, especially with libraries containing 10k songs or more.
|
||||
|
||||
#### Why ExoPlayer?
|
||||
|
||||
|
|
Loading…
Reference in a new issue