diff --git a/.github/workflows/build_app.yml b/.github/workflows/build_app.yml new file mode 100644 index 00000000..49c83e99 --- /dev/null +++ b/.github/workflows/build_app.yml @@ -0,0 +1,42 @@ +name: Build Lissen App + +env: + # The name of the main module repository + main_project_module: app + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + # Set Current Date As Env Variable + - name: Set current date as env variable + run: echo "date_today=$(date +'%Y-%m-%d')" >> $GITHUB_ENV + + # Set Repository Name As Env Variable + - name: Set repository name as env variable + run: echo "repository_name=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV + + - name: Set Up JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '21' + + - name: Change wrapper permissions + run: chmod +x ./gradlew + + # Run Build Project + - name: Build gradle project + run: ./gradlew build -Proom.schemaLocation=$GITHUB_WORKSPACE/app/schemas diff --git a/README.md b/README.md index 49670260..2820be5d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,11 @@ # Lissen - Clean Audiobookshelf Player +[![Build Lissen App](https://github.com/GrakovNe/lissen-android/actions/workflows/build_app.yml/badge.svg)](https://github.com/GrakovNe/lissen-android/actions/workflows/build_app.yml) -

- Screenshot 1 - Screenshot 2 - Screenshot 3 - Screenshot 4 +

+ Get it on Google Play   Get it on F-Droid

- - ### Features * Beautiful Interface: Intuitive design that makes browsing and listening to your audiobooks easy and enjoyable. @@ -16,7 +13,16 @@ * Streaming Support: Stream your audiobooks directly from the cloud without needing to download them first. * Offline Listening: Download audiobooks to listen offline, ideal for those who want to access their collection without an internet connection. -### Installation +### Screenshots + +

+ Screenshot 1 + Screenshot 2 + Screenshot 3 + Screenshot 4 +

+ +### Building 1. Clone the repository: ``` @@ -35,5 +41,19 @@ nano local.properties ``` 5. Build and run the app on an Android device or emulator. +### Demo Environment + + +You can connect to a demo [Audiobookshelf](https://github.com/advplyr/audiobookshelf) instance through the Lissen app: + +``` +URL: https://demo.lissenapp.org + +Username: demo +Password: demo +``` + +This instance contains only Public Domain audiobooks from [LibriVox](https://librivox.org/) and is intended solely for demonstrating the client’s functionality. + ## License Lissen is open-source and licensed under the MIT License. See the LICENSE file for more details. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 961ad5bb..49052dfe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -98,8 +98,6 @@ dependencies { implementation(libs.androidx.media3.session) kapt(libs.hilt.android.compiler) - implementation(libs.icons.lucide) - implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) @@ -121,7 +119,6 @@ dependencies { implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) - annotationProcessor(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler) testImplementation(libs.junit) diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/AudiobookshelfChannel.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/AudiobookshelfChannel.kt index fe04d014..1ba41b43 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/AudiobookshelfChannel.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/AudiobookshelfChannel.kt @@ -4,6 +4,7 @@ import android.net.Uri import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import org.grakovne.lissen.BuildConfig import org.grakovne.lissen.channel.audiobookshelf.api.AudioBookshelfDataRepository import org.grakovne.lissen.channel.audiobookshelf.api.AudioBookshelfMediaRepository import org.grakovne.lissen.channel.audiobookshelf.api.AudioBookshelfSyncService @@ -14,7 +15,9 @@ import org.grakovne.lissen.channel.audiobookshelf.converter.LibraryResponseConve import org.grakovne.lissen.channel.audiobookshelf.converter.LibrarySearchItemsConverter import org.grakovne.lissen.channel.audiobookshelf.converter.PlaybackSessionResponseConverter import org.grakovne.lissen.channel.audiobookshelf.converter.RecentBookResponseConverter +import org.grakovne.lissen.channel.audiobookshelf.model.DeviceInfo import org.grakovne.lissen.channel.audiobookshelf.model.LibraryResponse +import org.grakovne.lissen.channel.audiobookshelf.model.StartPlaybackRequest import org.grakovne.lissen.channel.common.ApiResult import org.grakovne.lissen.channel.common.ApiResult.Success import org.grakovne.lissen.channel.common.ChannelCode @@ -146,9 +149,9 @@ class AudiobookshelfChannel @Inject constructor( supportedMimeTypes: List, deviceId: String ): ApiResult { - val request = org.grakovne.lissen.channel.audiobookshelf.model.StartPlaybackRequest( + val request = StartPlaybackRequest( supportedMimeTypes = supportedMimeTypes, - deviceInfo = org.grakovne.lissen.channel.audiobookshelf.model.DeviceInfo( + deviceInfo = DeviceInfo( clientName = getClientName(), deviceId = deviceId, deviceName = getClientName() @@ -196,7 +199,7 @@ class AudiobookshelfChannel @Inject constructor( password: String ): ApiResult = dataRepository.authorize(host, username, password) - private fun getClientName() = "Lissen App Android" + private fun getClientName() = "Lissen App ${BuildConfig.VERSION_NAME}" private val supportedLibraryTypes = listOf("book") } diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemIdResponseConverter.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemIdResponseConverter.kt index 598133dc..8c7f49d2 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemIdResponseConverter.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemIdResponseConverter.kt @@ -32,19 +32,23 @@ class LibraryItemIdResponseConverter @Inject constructor() { } val filesAsChapters: () -> List = { - item.media.audioFiles.fold(0.0 to mutableListOf()) { (accDuration, chapters), file -> - chapters.add( - BookChapter( - start = accDuration, - end = accDuration + file.duration, - title = file.metaTags?.tagTitle - ?: file.metadata.filename.removeSuffix(file.metadata.ext), - duration = file.duration, - id = file.ino + item + .media + .audioFiles + .sortedBy { it.index } + .fold(0.0 to mutableListOf()) { (accDuration, chapters), file -> + chapters.add( + BookChapter( + start = accDuration, + end = accDuration + file.duration, + title = file.metaTags?.tagTitle + ?: file.metadata.filename.removeSuffix(file.metadata.ext), + duration = file.duration, + id = file.ino + ) ) - ) - accDuration + file.duration to chapters - }.second + accDuration + file.duration to chapters + }.second } return DetailedBook( @@ -54,6 +58,7 @@ class LibraryItemIdResponseConverter @Inject constructor() { files = item .media .audioFiles + .sortedBy { it.index } .map { BookFile( id = it.ino, diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemResponseConverter.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemResponseConverter.kt index b2a33ca3..119dea9b 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemResponseConverter.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemResponseConverter.kt @@ -12,10 +12,12 @@ class LibraryItemResponseConverter @Inject constructor() { fun apply(response: LibraryItemsResponse): PagedItems = response .results - .map { + .mapNotNull { + val title = it.media.metadata.title ?: return@mapNotNull null + Book( id = it.id, - title = it.media.metadata.title, + title = title, author = it.media.metadata.authorName, cachedState = BookCachedState.ABLE_TO_CACHE, duration = it.media.duration.toInt() diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibrarySearchItemsConverter.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibrarySearchItemsConverter.kt index da6341c5..ac7946c3 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibrarySearchItemsConverter.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibrarySearchItemsConverter.kt @@ -10,10 +10,12 @@ import javax.inject.Singleton class LibrarySearchItemsConverter @Inject constructor() { fun apply(response: List): List { return response - .map { + .mapNotNull { + val title = it.media.metadata.title ?: return@mapNotNull null + Book( id = it.id, - title = it.media.metadata.title, + title = title, author = it.media.metadata.authorName, cachedState = BookCachedState.ABLE_TO_CACHE, duration = it.media.duration.toInt() diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryItemsResponse.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryItemsResponse.kt index cbcbd8e3..a4657230 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryItemsResponse.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryItemsResponse.kt @@ -2,57 +2,20 @@ package org.grakovne.lissen.channel.audiobookshelf.model data class LibraryItemsResponse( val results: List, - val total: Int, - val limit: Int, - val page: Int, - val sortBy: String, - val sortDesc: Boolean, - val filterBy: String, - val mediaType: String, - val minified: Boolean, - val collapseseries: Boolean, - val include: String + val page: Int ) data class LibraryItem( val id: String, - val libraryId: String, - val folderId: String, - val path: String, - val relPath: String, - val isFile: Boolean, - val mtimeMs: Long, - val ctimeMs: Long, - val birthtimeMs: Long, - val addedAt: Long, - val updatedAt: Long, - val isMissing: Boolean, - val isInvalid: Boolean, - val mediaType: String, val media: Media ) data class Media( - val numTracks: Int, - val numAudioFiles: Int, - val numChapters: Int, val duration: Double, - val metadata: Metadata, - val size: Long + val metadata: Metadata ) data class Metadata( - val title: String, - val titleIgnorePrefix: String, - val subtitle: String?, - val authorName: String?, - val genres: List, - val publishedYear: String?, - val publishedDate: String?, - val publisher: String?, - val description: String?, - val isbn: String?, - val asin: String?, - val language: String?, - val explicit: Boolean + val title: String?, + val authorName: String? ) diff --git a/app/src/main/java/org/grakovne/lissen/channel/common/UserAgent.kt b/app/src/main/java/org/grakovne/lissen/channel/common/UserAgent.kt index 6cf6c0fc..798a01a6 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/common/UserAgent.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/common/UserAgent.kt @@ -3,4 +3,4 @@ package org.grakovne.lissen.channel.common import android.os.Build import org.grakovne.lissen.BuildConfig -val USER_AGENT = "Lissen/${BuildConfig.VERSION_NAME} (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL}) ExoPlayer/1.4.1" \ No newline at end of file +val USER_AGENT = "Lissen/${BuildConfig.VERSION_NAME} (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL}) ExoPlayer/1.4.1" diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt b/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt index 51e06df5..8da90f76 100644 --- a/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt +++ b/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt @@ -5,6 +5,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns import androidx.room.Transaction import androidx.room.Update import org.grakovne.lissen.content.cache.entity.BookChapterEntity @@ -82,6 +83,7 @@ interface CachedBookDao { ): List @Transaction + @RewriteQueriesToDropUnusedColumns @Query( """ SELECT * FROM detailed_books diff --git a/app/src/main/java/org/grakovne/lissen/playback/MediaRepository.kt b/app/src/main/java/org/grakovne/lissen/playback/MediaRepository.kt index 8c5ff7c6..32aa8b1c 100644 --- a/app/src/main/java/org/grakovne/lissen/playback/MediaRepository.kt +++ b/app/src/main/java/org/grakovne/lissen/playback/MediaRepository.kt @@ -7,7 +7,6 @@ import android.content.Intent import android.content.IntentFilter import android.os.Handler import android.os.Looper -import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.localbroadcastmanager.content.LocalBroadcastManager @@ -130,7 +129,7 @@ class MediaRepository @Inject constructor( val intent = Intent(context, PlaybackService::class.java).apply { action = PlaybackService.ACTION_PLAY } - ContextCompat.startForegroundService(context, intent) + context.startForegroundService(intent) } fun pauseAudio() { diff --git a/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt b/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt index cf79cd4d..a7f976e3 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.layout.ContentScale import coil.ImageLoader import coil.compose.AsyncImage import coil.request.ImageRequest -import com.valentinilk.shimmer.shimmer @Composable fun AsyncShimmeringImage( @@ -37,7 +36,6 @@ fun AsyncShimmeringImage( Box( modifier = Modifier .fillMaxSize() - .shimmer() .background(Color.Gray) ) } diff --git a/app/src/main/java/org/grakovne/lissen/ui/components/BookCoverFetcher.kt b/app/src/main/java/org/grakovne/lissen/ui/components/BookCoverFetcher.kt index f5ef9f1a..ecba3bc1 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/components/BookCoverFetcher.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/components/BookCoverFetcher.kt @@ -4,11 +4,9 @@ import android.content.Context import android.net.Uri import coil.ImageLoader import coil.decode.ImageSource -import coil.disk.DiskCache import coil.fetch.FetchResult import coil.fetch.Fetcher import coil.fetch.SourceResult -import coil.memory.MemoryCache import coil.request.Options import dagger.Module import dagger.Provides @@ -72,17 +70,7 @@ object ImageLoaderModule { ): ImageLoader { return ImageLoader .Builder(context) - .components { - add(bookCoverFetcherFactory) - } - .memoryCache { - MemoryCache.Builder(context).build() - } - .diskCache { - DiskCache.Builder() - .directory(context.cacheDir.resolve("сover_cache")) - .build() - } + .components { add(bookCoverFetcherFactory) } .build() } } diff --git a/app/src/main/java/org/grakovne/lissen/ui/extensions/TimeExtensions.kt b/app/src/main/java/org/grakovne/lissen/ui/extensions/TimeExtensions.kt index 4abcaa59..4714ac35 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/extensions/TimeExtensions.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/extensions/TimeExtensions.kt @@ -1,31 +1,28 @@ package org.grakovne.lissen.ui.extensions -import android.annotation.SuppressLint +import java.util.Locale -@SuppressLint("DefaultLocale") fun Int.formatFully(): String { val hours = this / 3600 val minutes = (this % 3600) / 60 - val remainingSeconds = this % 60 + val seconds = this % 60 return if (hours > 0) { - String.format("%02d:%02d:%02d", hours, minutes, remainingSeconds) + String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds) } else { - String.format("%02d:%02d", minutes, remainingSeconds) + String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) } } -@SuppressLint("DefaultLocale") fun Int.formatShortly(): String { val hours = this / 3600 val minutes = (this % 3600) / 60 - return String.format("%02dh %02dm", hours, minutes) + return String.format(Locale.getDefault(), "%02dh %02dm", hours, minutes) } -@SuppressLint("DefaultLocale") fun Int.formatLeadingMinutes(): String { val minutes = this / 60 - val remainingSeconds = this % 60 + val seconds = this % 60 - return String.format("%02d:%02d", minutes, remainingSeconds) + return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) } diff --git a/app/src/main/java/org/grakovne/lissen/ui/icons/Search.kt b/app/src/main/java/org/grakovne/lissen/ui/icons/Search.kt new file mode 100644 index 00000000..79c9b06a --- /dev/null +++ b/app/src/main/java/org/grakovne/lissen/ui/icons/Search.kt @@ -0,0 +1,76 @@ +/** + * Copyright @alexstyl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * File has been copied from https://composeicons.com/icons/lucide/search-check under ISC Licence + */ +package org.grakovne.lissen.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val Search: ImageVector + get() { + if (_Search != null) { + return _Search!! + } + _Search = ImageVector.Builder( + name = "Search", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + fill = null, + fillAlpha = 1.0f, + stroke = SolidColor(Color(0xFF000000)), + strokeAlpha = 1.0f, + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + strokeLineMiter = 1.0f, + pathFillType = PathFillType.NonZero + ) { + moveTo(19f, 11f) + arcTo(8f, 8f, 0f, isMoreThanHalf = false, isPositiveArc = true, 11f, 19f) + arcTo(8f, 8f, 0f, isMoreThanHalf = false, isPositiveArc = true, 3f, 11f) + arcTo(8f, 8f, 0f, isMoreThanHalf = false, isPositiveArc = true, 19f, 11f) + close() + } + path( + fill = null, + fillAlpha = 1.0f, + stroke = SolidColor(Color(0xFF000000)), + strokeAlpha = 1.0f, + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + strokeLineMiter = 1.0f, + pathFillType = PathFillType.NonZero + ) { + moveTo(21f, 21f) + lineToRelative(-4.3f, -4.3f) + } + }.build() + return _Search!! + } + +private var _Search: ImageVector? = null diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt index 131ed7c2..9f778872 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt @@ -312,7 +312,7 @@ fun LibraryScreen( } } - item { Spacer(modifier = Modifier.height(8.dp)) } + item(key = "library_spacer") { Spacer(modifier = Modifier.height(8.dp)) } when { isPlaceholderRequired -> item { LibraryPlaceholderComposable() } @@ -326,7 +326,7 @@ fun LibraryScreen( } } - else -> items(count = library.itemCount) { + else -> items(count = library.itemCount, key = { "library_item_$it" }) { val book = library[it] ?: return@items val isVisible = remember(hiddenBooks, book.id) { derivedStateOf { libraryViewModel.isVisible(book.id) } diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/DefaultActionComposable.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/DefaultActionComposable.kt index b6ff39ea..e1355da0 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/DefaultActionComposable.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/DefaultActionComposable.kt @@ -27,12 +27,11 @@ import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.composables.icons.lucide.Lucide -import com.composables.icons.lucide.Search import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.grakovne.lissen.R +import org.grakovne.lissen.ui.icons.Search import org.grakovne.lissen.ui.navigation.AppNavigationService import org.grakovne.lissen.viewmodel.CachingModelView import org.grakovne.lissen.viewmodel.LibraryViewModel @@ -54,7 +53,7 @@ fun DefaultActionComposable( modifier = Modifier.offset(x = 4.dp) ) { Icon( - imageVector = Lucide.Search, + imageVector = Search, contentDescription = null ) } diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/player/composable/TrackControlComposable.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/player/composable/TrackControlComposable.kt index 380d80ff..fe5e3f42 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/screens/player/composable/TrackControlComposable.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/screens/player/composable/TrackControlComposable.kt @@ -123,19 +123,14 @@ fun TrackControlComposable( ) { IconButton( onClick = { - if (currentTrackIndex > 0) { - viewModel.previousTrack() - } + viewModel.previousTrack() }, - enabled = currentTrackIndex > 0 + enabled = true ) { Icon( imageVector = Icons.Rounded.SkipPrevious, contentDescription = "Previous Track", - tint = when (currentTrackIndex > 0) { - true -> colorScheme.onBackground - else -> colorScheme.onBackground.copy(alpha = 0.3f) - }, + tint = colorScheme.onBackground, modifier = Modifier.size(36.dp) ) } diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/settings/composable/GeneralSettingsItemComposable.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/settings/composable/GeneralSettingsItemComposable.kt index 90a2ce32..fd0499bc 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/screens/settings/composable/GeneralSettingsItemComposable.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/screens/settings/composable/GeneralSettingsItemComposable.kt @@ -10,8 +10,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -65,7 +65,7 @@ fun GeneralSettingsItemComposable( onDismissRequest() } ) - Divider() + HorizontalDivider() } } } diff --git a/app/src/main/java/org/grakovne/lissen/viewmodel/PlayerViewModel.kt b/app/src/main/java/org/grakovne/lissen/viewmodel/PlayerViewModel.kt index e2939f7e..80224301 100644 --- a/app/src/main/java/org/grakovne/lissen/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/org/grakovne/lissen/viewmodel/PlayerViewModel.kt @@ -135,8 +135,15 @@ class PlayerViewModel @Inject constructor( } fun previousTrack() { - val previousChapterIndex = currentChapterIndex.value?.let { it - 1 } ?: return - setChapter(previousChapterIndex) + val position = currentChapterPosition.value ?: return + val index = currentChapterIndex.value ?: return + + val currentIndexReplay = (position > CURRENT_TRACK_REPLAY_THRESHOLD || index == 0) + + when { + currentIndexReplay -> setChapter(index) + index > 0 -> setChapter(index - 1) + } } fun togglePlayPause() { @@ -182,4 +189,9 @@ class PlayerViewModel @Inject constructor( return 0.0 } + + companion object { + + private const val CURRENT_TRACK_REPLAY_THRESHOLD = 5 + } } diff --git a/gradle.properties b/gradle.properties index 132244e5..f0a2e55f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,23 +1,4 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. For more details, visit -# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library android.nonTransitiveRClass=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed8db1e2..0c3b3a5a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,6 @@ composeShimmerAndroid = "1.3.1" converterGson = "2.9.0" hiltAndroid = "2.52" hiltNavigationCompose = "1.2.0" -iconsLucide = "1.0.0" kotlin = "1.9.24" coreKtx = "1.15.0" junit = "4.13.2" @@ -58,7 +57,6 @@ compose-shimmer-android = { module = "com.valentinilk.shimmer:compose-shimmer-an converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" } -icons-lucide = { module = "com.composables:icons-lucide", version.ref = "iconsLucide" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }