mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
logging: add release-visible breadcrumbs and a verbose toggle
Playback now logs each track transition and end at info (title/artist/album/source/reason), sync logs per-entity counts and pruned rows, and the Subsonic client logs server error codes and the token-auth fallback. Add a Verbose logging setting that drops the file level to debug at runtime (persisted) so a support session can capture full detail without flooding normal release logs.
This commit is contained in:
@@ -121,6 +121,14 @@ class UserPreferences(
|
|||||||
|
|
||||||
private val offlineModeKey = booleanPreferencesKey("offline_mode")
|
private val offlineModeKey = booleanPreferencesKey("offline_mode")
|
||||||
|
|
||||||
|
fun verboseLogging(): Flow<Boolean> = store.data.map { prefs -> prefs[verboseLoggingKey] ?: false }
|
||||||
|
|
||||||
|
suspend fun setVerboseLogging(enabled: Boolean) {
|
||||||
|
store.edit { prefs -> prefs[verboseLoggingKey] = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val verboseLoggingKey = booleanPreferencesKey("verbose_logging")
|
||||||
|
|
||||||
suspend fun lastSeed(): ArtworkSeedValue? {
|
suspend fun lastSeed(): ArtworkSeedValue? {
|
||||||
val prefs = store.data.first()
|
val prefs = store.data.first()
|
||||||
val cacheKey = prefs[lastSeedKey] ?: return null
|
val cacheKey = prefs[lastSeedKey] ?: return null
|
||||||
|
|||||||
@@ -111,7 +111,10 @@ class SourceRepository(
|
|||||||
active = config?.let { ActiveSource(it.id, it.toMusicSource(quality, concurrency)) }
|
active = config?.let { ActiveSource(it.id, it.toMusicSource(quality, concurrency)) }
|
||||||
_quality.value = quality
|
_quality.value = quality
|
||||||
if (sourceChanged && config != null && config.useTokenAuth && !_offline.value) {
|
if (sourceChanged && config != null && config.useTokenAuth && !_offline.value) {
|
||||||
scope.launch { runCatching { config.toClient().check("ping") } }
|
scope.launch {
|
||||||
|
runCatching { config.toClient().check("ping") }
|
||||||
|
.onSuccess { Log.i("net", "connected (ping ok)") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.subtracks.data.source.subsonic
|
|||||||
|
|
||||||
import com.subtracks.R
|
import com.subtracks.R
|
||||||
import com.subtracks.UiMessage
|
import com.subtracks.UiMessage
|
||||||
|
import com.subtracks.log.Log
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@@ -60,7 +61,11 @@ class SubsonicClient(
|
|||||||
return try {
|
return try {
|
||||||
execute(method, params, body)
|
execute(method, params, body)
|
||||||
} catch (failure: SubsonicException) {
|
} catch (failure: SubsonicException) {
|
||||||
if (failure.code !in TOKEN_AUTH_UNSUPPORTED_CODES || !usedTokenAuth) throw failure
|
if (failure.code !in TOKEN_AUTH_UNSUPPORTED_CODES || !usedTokenAuth) {
|
||||||
|
Log.w("http", "$method failed: server code ${failure.code} \"${failure.message}\"")
|
||||||
|
throw failure
|
||||||
|
}
|
||||||
|
Log.w("http", "$method: token auth unsupported (server code ${failure.code}), retrying with plaintext")
|
||||||
disableTokenAuth()
|
disableTokenAuth()
|
||||||
execute(method, params, body)
|
execute(method, params, body)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ class SyncManager(
|
|||||||
val source = sourceRepository.activeMusicSource() ?: throw NoServerException()
|
val source = sourceRepository.activeMusicSource() ?: throw NoServerException()
|
||||||
Log.i("sync", "started (source ${source.id})")
|
Log.i("sync", "started (source ${source.id})")
|
||||||
serverActions.flush()
|
serverActions.flush()
|
||||||
SyncService(db, source).sync()
|
val summary = SyncService(db, source).sync()
|
||||||
Log.i("sync", "finished in ${elapsedMs(startedAt)}ms")
|
Log.i("sync", "finished in ${elapsedMs(startedAt)}ms: $summary")
|
||||||
SyncStatus.Success
|
SyncStatus.Success
|
||||||
} catch (cancellation: CancellationException) {
|
} catch (cancellation: CancellationException) {
|
||||||
throw cancellation
|
throw cancellation
|
||||||
|
|||||||
@@ -20,13 +20,21 @@ class SyncService(
|
|||||||
private val db: SubtracksDatabase,
|
private val db: SubtracksDatabase,
|
||||||
private val source: MusicSource,
|
private val source: MusicSource,
|
||||||
) {
|
) {
|
||||||
suspend fun sync() {
|
private var artistsSynced = 0
|
||||||
|
private var albumsSynced = 0
|
||||||
|
private var songsSynced = 0
|
||||||
|
private var playlistsSynced = 0
|
||||||
|
private var playlistSongsSynced = 0
|
||||||
|
private var pruned = 0
|
||||||
|
|
||||||
|
suspend fun sync(): SyncSummary {
|
||||||
syncArtists()
|
syncArtists()
|
||||||
syncAlbums()
|
syncAlbums()
|
||||||
syncSongs()
|
syncSongs()
|
||||||
aggregatePlayData()
|
aggregatePlayData()
|
||||||
syncPlaylists()
|
syncPlaylists()
|
||||||
syncPlaylistSongs()
|
syncPlaylistSongs()
|
||||||
|
return SyncSummary(artistsSynced, albumsSynced, songsSynced, playlistsSynced, playlistSongsSynced, pruned)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun syncArtists() {
|
private suspend fun syncArtists() {
|
||||||
@@ -36,8 +44,9 @@ class SyncService(
|
|||||||
if (batch.isEmpty()) return@collect
|
if (batch.isEmpty()) return@collect
|
||||||
write("artists", artistColumns, primaryKey, batch) { it.values() }
|
write("artists", artistColumns, primaryKey, batch) { it.values() }
|
||||||
batch.forEach { seen.add(idHash(it.id)) }
|
batch.forEach { seen.add(idHash(it.id)) }
|
||||||
|
artistsSynced += batch.size
|
||||||
}
|
}
|
||||||
pruneStaleIds(seen, { library.artistIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deleteArtists(source.id, it) })
|
pruned += pruneStaleIds(seen, { library.artistIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deleteArtists(source.id, it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun syncAlbums() {
|
private suspend fun syncAlbums() {
|
||||||
@@ -53,11 +62,13 @@ class SyncService(
|
|||||||
writeAlbumsAndDiscs(batch, discs)
|
writeAlbumsAndDiscs(batch, discs)
|
||||||
batch.forEach { seenAlbums.add(idHash(it.id)) }
|
batch.forEach { seenAlbums.add(idHash(it.id)) }
|
||||||
discs.forEach { seenDiscs.add(discHash(it.albumId, it.disc)) }
|
discs.forEach { seenDiscs.add(discHash(it.albumId, it.disc)) }
|
||||||
|
albumsSynced += batch.size
|
||||||
}
|
}
|
||||||
pruneStaleIds(seenAlbums, { library.albumIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deleteAlbums(source.id, it) })
|
pruned += pruneStaleIds(seenAlbums, { library.albumIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deleteAlbums(source.id, it) })
|
||||||
pruneStaleDiscs(seenDiscs, { albumId, disc -> library.discKeysAfter(source.id, albumId, disc, PRUNE_PAGE) }) {
|
pruned +=
|
||||||
deleteStaleDiscs(it)
|
pruneStaleDiscs(seenDiscs, { albumId, disc -> library.discKeysAfter(source.id, albumId, disc, PRUNE_PAGE) }) {
|
||||||
}
|
deleteStaleDiscs(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun syncSongs() {
|
private suspend fun syncSongs() {
|
||||||
@@ -67,8 +78,9 @@ class SyncService(
|
|||||||
if (batch.isEmpty()) return@collect
|
if (batch.isEmpty()) return@collect
|
||||||
writeSongs(batch)
|
writeSongs(batch)
|
||||||
batch.forEach { seen.add(idHash(it.id)) }
|
batch.forEach { seen.add(idHash(it.id)) }
|
||||||
|
songsSynced += batch.size
|
||||||
}
|
}
|
||||||
pruneStaleIds(seen, { library.songIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deleteSongs(source.id, it) })
|
pruned += pruneStaleIds(seen, { library.songIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deleteSongs(source.id, it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun writeSongs(batch: List<Song>) {
|
private suspend fun writeSongs(batch: List<Song>) {
|
||||||
@@ -106,8 +118,9 @@ class SyncService(
|
|||||||
if (batch.isEmpty()) return@collect
|
if (batch.isEmpty()) return@collect
|
||||||
write("playlists", playlistColumns, primaryKey, batch) { it.values() }
|
write("playlists", playlistColumns, primaryKey, batch) { it.values() }
|
||||||
batch.forEach { seen.add(idHash(it.id)) }
|
batch.forEach { seen.add(idHash(it.id)) }
|
||||||
|
playlistsSynced += batch.size
|
||||||
}
|
}
|
||||||
pruneStaleIds(seen, { library.playlistIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deletePlaylists(source.id, it) })
|
pruned += pruneStaleIds(seen, { library.playlistIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deletePlaylists(source.id, it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun syncPlaylistSongs() {
|
private suspend fun syncPlaylistSongs() {
|
||||||
@@ -118,6 +131,7 @@ class SyncService(
|
|||||||
if (batch.isEmpty()) return@collect
|
if (batch.isEmpty()) return@collect
|
||||||
write("playlist_songs", playlistSongColumns, playlistSongKey, batch) { it.values() }
|
write("playlist_songs", playlistSongColumns, playlistSongKey, batch) { it.values() }
|
||||||
batch.forEach { counts[it.playlistId] = (counts[it.playlistId] ?: 0L) + 1L }
|
batch.forEach { counts[it.playlistId] = (counts[it.playlistId] ?: 0L) + 1L }
|
||||||
|
playlistSongsSynced += batch.size
|
||||||
}
|
}
|
||||||
playlistIds.forEach { library.deletePlaylistSongsFrom(source.id, it, counts[it] ?: 0L) }
|
playlistIds.forEach { library.deletePlaylistSongsFrom(source.id, it, counts[it] ?: 0L) }
|
||||||
library.deleteOrphanPlaylistSongs(source.id)
|
library.deleteOrphanPlaylistSongs(source.id)
|
||||||
@@ -173,15 +187,17 @@ class SyncService(
|
|||||||
seen: HashedIds,
|
seen: HashedIds,
|
||||||
pageOf: suspend (afterId: String) -> List<String>,
|
pageOf: suspend (afterId: String) -> List<String>,
|
||||||
delete: suspend (Collection<String>) -> Unit,
|
delete: suspend (Collection<String>) -> Unit,
|
||||||
) {
|
): Int {
|
||||||
seen.freeze()
|
seen.freeze()
|
||||||
var afterId = ""
|
var afterId = ""
|
||||||
|
var pruned = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
val ids = pageOf(afterId)
|
val ids = pageOf(afterId)
|
||||||
if (ids.isEmpty()) return
|
if (ids.isEmpty()) return pruned
|
||||||
val stale = ids.filterNot { seen.contains(idHash(it)) }
|
val stale = ids.filterNot { seen.contains(idHash(it)) }
|
||||||
stale.chunked(DELETE_CHUNK).forEach { delete(it) }
|
stale.chunked(DELETE_CHUNK).forEach { delete(it) }
|
||||||
if (ids.size < PRUNE_PAGE) return
|
pruned += stale.size
|
||||||
|
if (ids.size < PRUNE_PAGE) return pruned
|
||||||
afterId = ids.last()
|
afterId = ids.last()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,22 +206,33 @@ class SyncService(
|
|||||||
seen: HashedIds,
|
seen: HashedIds,
|
||||||
pageOf: suspend (afterAlbumId: String, afterDisc: Long) -> List<DiscKey>,
|
pageOf: suspend (afterAlbumId: String, afterDisc: Long) -> List<DiscKey>,
|
||||||
delete: suspend (Collection<DiscKey>) -> Unit,
|
delete: suspend (Collection<DiscKey>) -> Unit,
|
||||||
) {
|
): Int {
|
||||||
seen.freeze()
|
seen.freeze()
|
||||||
var afterAlbumId = ""
|
var afterAlbumId = ""
|
||||||
var afterDisc = 0L
|
var afterDisc = 0L
|
||||||
|
var pruned = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
val keys = pageOf(afterAlbumId, afterDisc)
|
val keys = pageOf(afterAlbumId, afterDisc)
|
||||||
if (keys.isEmpty()) return
|
if (keys.isEmpty()) return pruned
|
||||||
val stale = keys.filterNot { seen.contains(discHash(it.albumId, it.disc)) }
|
val stale = keys.filterNot { seen.contains(discHash(it.albumId, it.disc)) }
|
||||||
stale.chunked(DELETE_CHUNK).forEach { delete(it) }
|
stale.chunked(DELETE_CHUNK).forEach { delete(it) }
|
||||||
if (keys.size < PRUNE_PAGE) return
|
pruned += stale.size
|
||||||
|
if (keys.size < PRUNE_PAGE) return pruned
|
||||||
afterAlbumId = keys.last().albumId
|
afterAlbumId = keys.last().albumId
|
||||||
afterDisc = keys.last().disc
|
afterDisc = keys.last().disc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class SyncSummary(
|
||||||
|
val artists: Int,
|
||||||
|
val albums: Int,
|
||||||
|
val songs: Int,
|
||||||
|
val playlists: Int,
|
||||||
|
val playlistSongs: Int,
|
||||||
|
val pruned: Int,
|
||||||
|
)
|
||||||
|
|
||||||
private const val DELETE_CHUNK = 500
|
private const val DELETE_CHUNK = 500
|
||||||
private const val PRUNE_PAGE = 500
|
private const val PRUNE_PAGE = 500
|
||||||
private const val INITIAL_HASH_CAPACITY = 256
|
private const val INITIAL_HASH_CAPACITY = 256
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.subtracks.data.repo.QueueRepository
|
|||||||
import com.subtracks.data.repo.SourceRepository
|
import com.subtracks.data.repo.SourceRepository
|
||||||
import com.subtracks.data.source.ServerActionSink
|
import com.subtracks.data.source.ServerActionSink
|
||||||
import com.subtracks.data.sync.SyncManager
|
import com.subtracks.data.sync.SyncManager
|
||||||
|
import com.subtracks.log.Log
|
||||||
import com.subtracks.playback.MediaSessionConnection
|
import com.subtracks.playback.MediaSessionConnection
|
||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import com.subtracks.playback.PlayerConnection
|
import com.subtracks.playback.PlayerConnection
|
||||||
@@ -48,6 +49,8 @@ import com.subtracks.ui.settings.SettingsViewModel
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koin.core.module.dsl.viewModel
|
import org.koin.core.module.dsl.viewModel
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
@@ -68,6 +71,7 @@ fun appModule(
|
|||||||
single { CoroutineScope(SupervisorJob() + Dispatchers.Default) }
|
single { CoroutineScope(SupervisorJob() + Dispatchers.Default) }
|
||||||
single(named("io")) { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
single(named("io")) { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||||
single(named("playback")) { CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) }
|
single(named("playback")) { CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) }
|
||||||
|
single(createdAtStart = true) { get<UserPreferences>().verboseLogging().onEach(Log::setVerbose).launchIn(get()) }
|
||||||
single {
|
single {
|
||||||
SourceRepository(
|
SourceRepository(
|
||||||
get(),
|
get(),
|
||||||
|
|||||||
@@ -21,19 +21,24 @@ object Log {
|
|||||||
private var fileStore: LogFileStore? = null
|
private var fileStore: LogFileStore? = null
|
||||||
private var appContext: Context? = null
|
private var appContext: Context? = null
|
||||||
private var consoleLevel = LogLevel.DEBUG
|
private var consoleLevel = LogLevel.DEBUG
|
||||||
|
private var defaultFileLevel = LogLevel.INFO
|
||||||
private var crashHandlerInstalled = false
|
private var crashHandlerInstalled = false
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
val app = context.applicationContext
|
val app = context.applicationContext
|
||||||
appContext = app
|
appContext = app
|
||||||
consoleLevel = if (BuildConfig.DEBUG) LogLevel.VERBOSE else LogLevel.INFO
|
consoleLevel = if (BuildConfig.DEBUG) LogLevel.VERBOSE else LogLevel.INFO
|
||||||
val fileLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.INFO
|
defaultFileLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.INFO
|
||||||
fileStore?.shutdown()
|
fileStore?.shutdown()
|
||||||
fileStore = LogFileStore(File(app.filesDir, "logs"), fileLevel)
|
fileStore = LogFileStore(File(app.filesDir, "logs"), defaultFileLevel)
|
||||||
installCrashHandler()
|
installCrashHandler()
|
||||||
i("app", "started ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) on Android ${Build.VERSION.SDK_INT} ${Build.MODEL}")
|
i("app", "started ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) on Android ${Build.VERSION.SDK_INT} ${Build.MODEL}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setVerbose(enabled: Boolean) {
|
||||||
|
fileStore?.setMinLevel(if (enabled) LogLevel.DEBUG else defaultFileLevel)
|
||||||
|
}
|
||||||
|
|
||||||
fun v(
|
fun v(
|
||||||
tag: String,
|
tag: String,
|
||||||
message: String,
|
message: String,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ enum class LogLevel { VERBOSE, DEBUG, INFO, WARN, ERROR }
|
|||||||
|
|
||||||
class LogFileStore(
|
class LogFileStore(
|
||||||
private val rootDir: File,
|
private val rootDir: File,
|
||||||
private val minLevel: LogLevel,
|
@Volatile private var minLevel: LogLevel,
|
||||||
private val maxFileBytes: Long = DEFAULT_MAX_FILE_BYTES,
|
private val maxFileBytes: Long = DEFAULT_MAX_FILE_BYTES,
|
||||||
private val maxFiles: Int = DEFAULT_MAX_FILES,
|
private val maxFiles: Int = DEFAULT_MAX_FILES,
|
||||||
) {
|
) {
|
||||||
@@ -55,6 +55,10 @@ class LogFileStore(
|
|||||||
runCatching { executor.submit {}.get() }
|
runCatching { executor.submit {}.get() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setMinLevel(level: LogLevel) {
|
||||||
|
minLevel = level
|
||||||
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
executor.shutdownNow()
|
executor.shutdownNow()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class Media3PlayerHandle(
|
|||||||
mediaItem: MediaItem?,
|
mediaItem: MediaItem?,
|
||||||
reason: Int,
|
reason: Int,
|
||||||
) {
|
) {
|
||||||
Log.d("playback", "${describe(mediaItem)} transition=${transitionReason(reason)}")
|
Log.i("playback", "${describe(mediaItem)} transition=${transitionReason(reason)}")
|
||||||
listener.onTransition()
|
listener.onTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +166,9 @@ class Media3PlayerHandle(
|
|||||||
"state=$playbackState position=${controller.currentPosition} " +
|
"state=$playbackState position=${controller.currentPosition} " +
|
||||||
"duration=${duration(controller.duration)} itemDuration=${currentItem?.durationMs}",
|
"duration=${duration(controller.duration)} itemDuration=${currentItem?.durationMs}",
|
||||||
)
|
)
|
||||||
|
if (playbackState == Player.STATE_ENDED) {
|
||||||
|
Log.i("playback", "ended ${describe(controller.currentMediaItem)}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ fun SettingsRoute(
|
|||||||
val downloadOverMetered by viewModel.downloadOverMetered.collectAsStateWithLifecycle()
|
val downloadOverMetered by viewModel.downloadOverMetered.collectAsStateWithLifecycle()
|
||||||
val scrobbling by viewModel.scrobbling.collectAsStateWithLifecycle()
|
val scrobbling by viewModel.scrobbling.collectAsStateWithLifecycle()
|
||||||
val offline by viewModel.offline.collectAsStateWithLifecycle()
|
val offline by viewModel.offline.collectAsStateWithLifecycle()
|
||||||
|
val verboseLogging by viewModel.verboseLogging.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
@@ -115,6 +116,7 @@ fun SettingsRoute(
|
|||||||
downloadOverMetered = downloadOverMetered,
|
downloadOverMetered = downloadOverMetered,
|
||||||
scrobbling = scrobbling,
|
scrobbling = scrobbling,
|
||||||
offline = offline,
|
offline = offline,
|
||||||
|
verboseLogging = verboseLogging,
|
||||||
onSelectSource = viewModel::selectSource,
|
onSelectSource = viewModel::selectSource,
|
||||||
onEditServer = onEditServer,
|
onEditServer = onEditServer,
|
||||||
onWifiQualityChange = viewModel::setWifiQuality,
|
onWifiQualityChange = viewModel::setWifiQuality,
|
||||||
@@ -124,6 +126,7 @@ fun SettingsRoute(
|
|||||||
onDownloadOverMeteredChange = viewModel::setDownloadOverMetered,
|
onDownloadOverMeteredChange = viewModel::setDownloadOverMetered,
|
||||||
onScrobblingChange = viewModel::setScrobbling,
|
onScrobblingChange = viewModel::setScrobbling,
|
||||||
onOfflineChange = viewModel::setOfflineMode,
|
onOfflineChange = viewModel::setOfflineMode,
|
||||||
|
onVerboseLoggingChange = viewModel::setVerboseLogging,
|
||||||
onAddServer = onAddServer,
|
onAddServer = onAddServer,
|
||||||
onOpenDownloads = onOpenDownloads,
|
onOpenDownloads = onOpenDownloads,
|
||||||
onOpenLicenses = onOpenLicenses,
|
onOpenLicenses = onOpenLicenses,
|
||||||
@@ -162,6 +165,7 @@ fun SettingsScreen(
|
|||||||
scrobbling: Boolean,
|
scrobbling: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
offline: Boolean = false,
|
offline: Boolean = false,
|
||||||
|
verboseLogging: Boolean = false,
|
||||||
onSelectSource: (Long) -> Unit,
|
onSelectSource: (Long) -> Unit,
|
||||||
onEditServer: (Long) -> Unit,
|
onEditServer: (Long) -> Unit,
|
||||||
onWifiQualityChange: (StreamQuality) -> Unit,
|
onWifiQualityChange: (StreamQuality) -> Unit,
|
||||||
@@ -171,6 +175,7 @@ fun SettingsScreen(
|
|||||||
onDownloadOverMeteredChange: (Boolean) -> Unit,
|
onDownloadOverMeteredChange: (Boolean) -> Unit,
|
||||||
onScrobblingChange: (Boolean) -> Unit,
|
onScrobblingChange: (Boolean) -> Unit,
|
||||||
onOfflineChange: (Boolean) -> Unit = {},
|
onOfflineChange: (Boolean) -> Unit = {},
|
||||||
|
onVerboseLoggingChange: (Boolean) -> Unit = {},
|
||||||
onAddServer: () -> Unit,
|
onAddServer: () -> Unit,
|
||||||
onOpenDownloads: () -> Unit,
|
onOpenDownloads: () -> Unit,
|
||||||
onOpenLicenses: () -> Unit,
|
onOpenLicenses: () -> Unit,
|
||||||
@@ -400,6 +405,26 @@ fun SettingsScreen(
|
|||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.settings_about_verbose_logging)) },
|
||||||
|
supportingContent = { Text(stringResource(R.string.settings_about_verbose_logging_description)) },
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = verboseLogging,
|
||||||
|
onCheckedChange = onVerboseLoggingChange,
|
||||||
|
colors =
|
||||||
|
SwitchDefaults.colors(
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable { onVerboseLoggingChange(!verboseLogging) },
|
||||||
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
)
|
||||||
|
}
|
||||||
item { Spacer(Modifier.height(fill)) }
|
item { Spacer(Modifier.height(fill)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,19 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
val offline: StateFlow<Boolean> = sourceRepository.offline
|
val offline: StateFlow<Boolean> = sourceRepository.offline
|
||||||
|
|
||||||
|
val verboseLogging: StateFlow<Boolean> =
|
||||||
|
userPreferences
|
||||||
|
.verboseLogging()
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
||||||
|
|
||||||
fun setOfflineMode(enabled: Boolean) {
|
fun setOfflineMode(enabled: Boolean) {
|
||||||
sourceRepository.setOfflineMode(enabled)
|
sourceRepository.setOfflineMode(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setVerboseLogging(enabled: Boolean) {
|
||||||
|
viewModelScope.launch { userPreferences.setVerboseLogging(enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
fun selectSource(id: Long) {
|
fun selectSource(id: Long) {
|
||||||
viewModelScope.launch { sourceRepository.selectSource(id) }
|
viewModelScope.launch { sourceRepository.selectSource(id) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,8 @@
|
|||||||
<string name="settings_about_share_logs_empty">No logs to send yet</string>
|
<string name="settings_about_share_logs_empty">No logs to send yet</string>
|
||||||
<string name="settings_about_share_logs_failed">Couldn\'t prepare the logs</string>
|
<string name="settings_about_share_logs_failed">Couldn\'t prepare the logs</string>
|
||||||
<string name="settings_about_share_logs_subject">Subtracks logs</string>
|
<string name="settings_about_share_logs_subject">Subtracks logs</string>
|
||||||
|
<string name="settings_about_verbose_logging">Verbose logging</string>
|
||||||
|
<string name="settings_about_verbose_logging_description">Log extra detail for troubleshooting. Log files get larger.</string>
|
||||||
<string name="settings_about_version">Version %1$s</string>
|
<string name="settings_about_version">Version %1$s</string>
|
||||||
<string name="settings_music_name">Playback</string>
|
<string name="settings_music_name">Playback</string>
|
||||||
<string name="settings_music_options_scrobble_description_on">Send now playing and play counts</string>
|
<string name="settings_music_options_scrobble_description_on">Send now playing and play counts</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user