├── app ├── .gitignore ├── src │ └── main │ │ ├── ic_launcher-playstore.png │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ │ └── colors.xml │ │ ├── values-v29 │ │ │ └── colors.xml │ │ ├── values │ │ │ ├── ic_launcher_background.xml │ │ │ ├── preloaded_fonts.xml │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── xml │ │ │ └── automotive_app_desc.xml │ │ ├── drawable │ │ │ ├── bg_radio_gradient.xml │ │ │ ├── ic_play.xml │ │ │ ├── ic_pause.xml │ │ │ ├── ic_tune.xml │ │ │ ├── ic_playing.xml │ │ │ ├── ic_settings.xml │ │ │ ├── ic_changed_lately.xml │ │ │ ├── ic_random.xml │ │ │ ├── ic_top_clicks.xml │ │ │ └── ic_top_vote.xml │ │ └── font │ │ │ └── khula_semibold.xml │ │ ├── java │ │ └── com │ │ │ └── egoriku │ │ │ └── radiotok │ │ │ ├── domain │ │ │ ├── datasource │ │ │ │ └── IRadioDnsServer.kt │ │ │ ├── repository │ │ │ │ └── IRadioFetchNetworkRepository.kt │ │ │ ├── model │ │ │ │ ├── section │ │ │ │ │ └── FeedType.kt │ │ │ │ └── Feed.kt │ │ │ ├── usecase │ │ │ │ └── RadioCacheUseCase.kt │ │ │ └── common │ │ │ │ └── internal │ │ │ │ └── StringResourceProvider.kt │ │ │ ├── presentation │ │ │ ├── screen │ │ │ │ ├── feed │ │ │ │ │ ├── FeedState.kt │ │ │ │ │ ├── FeedViewModel.kt │ │ │ │ │ └── ui │ │ │ │ │ │ ├── FeedRow.kt │ │ │ │ │ │ ├── MoreItem.kt │ │ │ │ │ │ ├── PlaylistWithIcon.kt │ │ │ │ │ │ └── InstantRadio.kt │ │ │ │ ├── playlist │ │ │ │ │ ├── model │ │ │ │ │ │ └── Playlist.kt │ │ │ │ │ ├── PlaylistState.kt │ │ │ │ │ ├── components │ │ │ │ │ │ ├── ShuffleButton.kt │ │ │ │ │ │ ├── CircleResourceImage.kt │ │ │ │ │ │ └── RadioListItem.kt │ │ │ │ │ └── PlaylistViewModel.kt │ │ │ │ ├── main │ │ │ │ │ ├── PlayerControlsActions.kt │ │ │ │ │ └── ui │ │ │ │ │ │ ├── actions │ │ │ │ │ │ ├── TuneAction.kt │ │ │ │ │ │ ├── SkipNextAction.kt │ │ │ │ │ │ ├── NotInterestedAction.kt │ │ │ │ │ │ ├── PlayPauseButton.kt │ │ │ │ │ │ └── LikeAction.kt │ │ │ │ │ │ ├── RadioLogo.kt │ │ │ │ │ │ ├── RadioLogoImage.kt │ │ │ │ │ │ └── PlayerControls.kt │ │ │ │ └── settings │ │ │ │ │ ├── SettingsViewModel.kt │ │ │ │ │ ├── ui │ │ │ │ │ └── SettingItem.kt │ │ │ │ │ └── SettingScreen.kt │ │ │ ├── state │ │ │ │ └── RadioPlaybackState.kt │ │ │ ├── MainActivity.kt │ │ │ ├── ui │ │ │ │ └── Theme.kt │ │ │ └── RadioViewModel.kt │ │ │ ├── foundation │ │ │ ├── HSpacer.kt │ │ │ ├── header │ │ │ │ ├── SectionHeader.kt │ │ │ │ └── ScreenHeader.kt │ │ │ ├── button │ │ │ │ ├── IconButton.kt │ │ │ │ └── CircleIconButtonLarge.kt │ │ │ └── NetworkImage.kt │ │ │ ├── data │ │ │ ├── repository │ │ │ │ └── RadioFetchNetworkRepository.kt │ │ │ ├── mapper │ │ │ │ └── NetworkStationToDbMapper.kt │ │ │ ├── retrofit │ │ │ │ ├── ApiEndpoint.kt │ │ │ │ └── HostSelectionInterceptor.kt │ │ │ └── datasource │ │ │ │ └── RadioDnsServer.kt │ │ │ ├── extension │ │ │ ├── Modifier.kt │ │ │ └── Bitmap.kt │ │ │ ├── RadioApplication.kt │ │ │ └── util │ │ │ └── BackupManager.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── assets ├── android_app.png ├── android_auto_personal.jpg └── android_auto_shuffle.jpg ├── db ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── com │ │ └── egoriku │ │ └── radiotok │ │ └── db │ │ ├── RadioTokDb.kt │ │ ├── koin │ │ └── DbModule.kt │ │ ├── entity │ │ └── StationDbEntity.kt │ │ └── dao │ │ └── StationDao.kt └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── common ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── egoriku │ │ │ └── radiotok │ │ │ └── common │ │ │ ├── Constant.kt │ │ │ ├── ext │ │ │ ├── IMapper.kt │ │ │ ├── Log.kt │ │ │ ├── ResultOf.kt │ │ │ ├── Context.kt │ │ │ ├── Uri.kt │ │ │ └── String.kt │ │ │ ├── model │ │ │ └── RadioItemModel.kt │ │ │ ├── mapper │ │ │ └── MetadataBuilder.kt │ │ │ └── provider │ │ │ ├── IStringResourceProvider.kt │ │ │ └── IBitmapProvider.kt │ │ └── res │ │ ├── values │ │ └── colors.xml │ │ ├── drawable-anydpi │ │ ├── ic_auto_not_interested.xml │ │ ├── ic_auto_country.xml │ │ ├── ic_auto_playing.xml │ │ ├── ic_auto_smart_playlist.xml │ │ ├── ic_auto_changed_lately.xml │ │ ├── ic_auto_language.xml │ │ ├── ic_auto_collection.xml │ │ ├── ic_auto_shuffle.xml │ │ ├── ic_auto_liked.xml │ │ ├── ic_auto_genres.xml │ │ ├── ic_auto_top_clicks.xml │ │ ├── ic_auto_radio_waves.xml │ │ ├── ic_auto_personal.xml │ │ ├── ic_auto_top_vote.xml │ │ └── ic_auto_history.xml │ │ └── drawable │ │ └── ic_history.xml └── build.gradle ├── mediaItemDsl ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── egoriku │ │ └── mediaitemdsl │ │ ├── appearance │ │ ├── AppearanceMarker.kt │ │ └── AppearanceBuilder.kt │ │ ├── mediaitem │ │ ├── MediaItemMarker.kt │ │ └── MediaItemBuilder.kt │ │ └── MediaItemDsl.kt └── build.gradle ├── datasource ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── com │ │ └── egoriku │ │ └── radiotok │ │ └── datasource │ │ ├── entity │ │ ├── params │ │ │ ├── MetadataParams.kt │ │ │ └── PlaylistParams.kt │ │ ├── MetadataEntity.kt │ │ └── RadioEntity.kt │ │ ├── intertal │ │ ├── repair │ │ │ ├── Metadata.kt │ │ │ └── RadioStationRepair.kt │ │ ├── datasource │ │ │ ├── AllStationsDataSource.kt │ │ │ ├── RadioInfoDataSource.kt │ │ │ ├── playlist │ │ │ │ ├── TopVoteDataSource.kt │ │ │ │ ├── TopClicksDataSource.kt │ │ │ │ ├── PlayingNowDataSource.kt │ │ │ │ ├── ChangedLatelyDataSource.kt │ │ │ │ └── LocalStationsDataSource.kt │ │ │ ├── random │ │ │ │ └── entity │ │ │ │ │ └── ServerStats.kt │ │ │ └── metadata │ │ │ │ ├── TagsDataSource.kt │ │ │ │ ├── CountriesDataSource.kt │ │ │ │ └── LanguagesDataSource.kt │ │ ├── api │ │ │ └── Api.kt │ │ └── EntitySourceFactory.kt │ │ ├── datasource │ │ ├── IAllStationsDataSource.kt │ │ ├── playlist │ │ │ ├── ITopVoteDataSource.kt │ │ │ ├── ITopClicksDataSource.kt │ │ │ ├── IPlayingNowDataSource.kt │ │ │ ├── IChangedLatelyDataSource.kt │ │ │ └── ILocalStationsDataSource.kt │ │ ├── IRadioInfoDataSource.kt │ │ └── metadata │ │ │ ├── ITagsDataSource.kt │ │ │ ├── ICountriesDataSource.kt │ │ │ └── ILanguagesDataSource.kt │ │ ├── IEntitySourceFactory.kt │ │ └── koin │ │ └── DataSourceModule.kt └── build.gradle.kts ├── radioPlayer ├── src │ └── main │ │ ├── res │ │ ├── drawable │ │ │ └── ic_radio_round.webp │ │ └── drawable-anydpi │ │ │ ├── ic_skip_next.xml │ │ │ ├── ic_favorite.xml │ │ │ ├── ic_not_interested.xml │ │ │ ├── ic_favorite_border.xml │ │ │ └── ic_radio_tower.xml │ │ ├── java │ │ └── com │ │ │ └── egoriku │ │ │ └── radiotok │ │ │ └── radioplayer │ │ │ ├── constant │ │ │ ├── PlayerConstants.kt │ │ │ ├── CustomAction.kt │ │ │ └── MediaBrowserConstant.kt │ │ │ ├── repository │ │ │ ├── IMediaMetadataRepository.kt │ │ │ ├── RadioEntityToModelMapper.kt │ │ │ ├── IMediaItemRepository.kt │ │ │ └── MediaMetadataRepository.kt │ │ │ ├── listener │ │ │ ├── EventHandler.kt │ │ │ ├── RadioPlayerEventListener.kt │ │ │ └── RadioPlaybackPreparer.kt │ │ │ ├── ext │ │ │ ├── MediaTransportControls.kt │ │ │ └── PlaybackStateCompat.kt │ │ │ ├── data │ │ │ ├── mediator │ │ │ │ └── IRadioCacheMediator.kt │ │ │ ├── RadioStateMediator.kt │ │ │ ├── mapper │ │ │ │ └── MediaMetadataToMediaSource.kt │ │ │ └── CurrentRadioQueueHolder.kt │ │ │ ├── notification │ │ │ ├── listener │ │ │ │ ├── NotificationMediaButtonEventHandler.kt │ │ │ │ └── RadioPlayerNotificationListener.kt │ │ │ ├── actions │ │ │ │ ├── DislikeActionProvider.kt │ │ │ │ └── FavoriteActionProvider.kt │ │ │ └── description │ │ │ │ └── DescriptionAdapter.kt │ │ │ ├── koin │ │ │ └── RadioPlayerModule.kt │ │ │ └── queue │ │ │ └── RadioQueueNavigator.kt │ │ └── AndroidManifest.xml └── build.gradle ├── .github ├── dependabot.yml └── workflows │ └── auto_release.yml ├── settings.gradle ├── README.md ├── gradle.properties ├── .gitignore └── gradlew.bat /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /assets/android_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/assets/android_app.png -------------------------------------------------------------------------------- /assets/android_auto_personal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/assets/android_auto_personal.jpg -------------------------------------------------------------------------------- /assets/android_auto_shuffle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/assets/android_auto_shuffle.jpg -------------------------------------------------------------------------------- /db/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /common/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/Constant.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.common 2 | 3 | const val EMPTY = "" -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /mediaItemDsl/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /datasource/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/ext/IMapper.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.common.ext 2 | 3 | typealias IMapper = (V) -> R -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /radioPlayer/src/main/res/drawable/ic_radio_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorikftp/RadioTok/HEAD/radioPlayer/src/main/res/drawable/ic_radio_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF393939 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-v29/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/transparent 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /common/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/domain/datasource/IRadioDnsServer.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.domain.datasource 2 | 3 | interface IRadioDnsServer { 4 | 5 | suspend fun lookup(): List 6 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/entity/params/MetadataParams.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.entity.params 2 | 3 | enum class MetadataParams { 4 | Tags, 5 | Languages, 6 | Countries 7 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/repair/Metadata.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.repair 2 | 3 | internal data class Metadata( 4 | val id: String, 5 | val logoUrl: String 6 | ) -------------------------------------------------------------------------------- /app/src/main/res/values/preloaded_fonts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @font/khula_semibold 5 | 6 | 7 | -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/ext/Log.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | 3 | package com.egoriku.radiotok.common.ext 4 | 5 | import android.util.Log 6 | 7 | inline fun logD(message: String) { 8 | Log.d("kek", message) 9 | } -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/ext/ResultOf.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.common.ext 2 | 3 | sealed class ResultOf { 4 | class Success(val value: T) : ResultOf() 5 | class Failure(val message: String) : ResultOf() 6 | } -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #33393939 4 | @color/immersive_sys_ui 5 | #F2F2F7 6 | -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/entity/params/PlaylistParams.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.entity.params 2 | 3 | enum class PlaylistParams { 4 | ChangedLately, 5 | LocalStations, 6 | PlayingNow, 7 | TopClicks, 8 | TopVote 9 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 02 09:37:52 MSK 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/domain/repository/IRadioFetchNetworkRepository.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.domain.repository 2 | 3 | import com.egoriku.radiotok.datasource.entity.RadioEntity 4 | 5 | interface IRadioFetchNetworkRepository { 6 | 7 | suspend fun load(): List 8 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/IAllStationsDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource 2 | 3 | import com.egoriku.radiotok.datasource.entity.RadioEntity 4 | 5 | interface IAllStationsDataSource { 6 | 7 | suspend fun loadAll(): List 8 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/playlist/ITopVoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource.playlist 2 | 3 | import com.egoriku.radiotok.datasource.entity.RadioEntity 4 | 5 | interface ITopVoteDataSource { 6 | 7 | suspend fun load(): List 8 | } -------------------------------------------------------------------------------- /mediaItemDsl/src/main/java/com/egoriku/mediaitemdsl/appearance/AppearanceMarker.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.mediaitemdsl.appearance 2 | 3 | import androidx.annotation.RestrictTo 4 | 5 | @DslMarker 6 | @Target(AnnotationTarget.CLASS) 7 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 8 | internal annotation class AppearanceMarker -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/playlist/ITopClicksDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource.playlist 2 | 3 | import com.egoriku.radiotok.datasource.entity.RadioEntity 4 | 5 | interface ITopClicksDataSource { 6 | 7 | suspend fun load(): List 8 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/IRadioInfoDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource 2 | 3 | import com.egoriku.radiotok.datasource.entity.RadioEntity 4 | 5 | interface IRadioInfoDataSource { 6 | 7 | suspend fun loadByIds(ids: List): List 8 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/playlist/IPlayingNowDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource.playlist 2 | 3 | import com.egoriku.radiotok.datasource.entity.RadioEntity 4 | 5 | interface IPlayingNowDataSource { 6 | 7 | suspend fun load(): List 8 | } -------------------------------------------------------------------------------- /mediaItemDsl/src/main/java/com/egoriku/mediaitemdsl/mediaitem/MediaItemMarker.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.mediaitemdsl.mediaitem 2 | 3 | import androidx.annotation.RestrictTo 4 | 5 | @DslMarker 6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) 7 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 8 | annotation class MediaItemMarker -------------------------------------------------------------------------------- /app/src/main/res/xml/automotive_app_desc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/playlist/IChangedLatelyDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource.playlist 2 | 3 | import com.egoriku.radiotok.datasource.entity.RadioEntity 4 | 5 | interface IChangedLatelyDataSource { 6 | 7 | suspend fun load(): List 8 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/playlist/ILocalStationsDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource.playlist 2 | 3 | import com.egoriku.radiotok.datasource.entity.RadioEntity 4 | 5 | interface ILocalStationsDataSource { 6 | 7 | suspend fun load(): List 8 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/feed/FeedState.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.feed 2 | 3 | import com.egoriku.radiotok.domain.model.Feed 4 | 5 | sealed class FeedState { 6 | object Loading : FeedState() 7 | data class Success(val feed: Feed) : FeedState() 8 | object Error : FeedState() 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/state/RadioPlaybackState.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.state 2 | 3 | data class RadioPlaybackState( 4 | val isPlaying: Boolean = false, 5 | val isPrepared: Boolean = false, 6 | val isPlayEnabled: Boolean = false, 7 | val isError: Boolean = false, 8 | val isLiked: Boolean = false, 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/playlist/model/Playlist.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.playlist.model 2 | 3 | import com.egoriku.radiotok.common.model.RadioItemModel 4 | 5 | data class Playlist( 6 | val id: String, 7 | val icon: Int, 8 | val title: String, 9 | val radioStations: List 10 | ) -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/entity/MetadataEntity.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class MetadataEntity( 6 | 7 | @SerializedName("name") 8 | val name: String, 9 | 10 | @SerializedName("stationcount") 11 | val count: Int 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_radio_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/constant/PlayerConstants.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.constant 2 | 3 | object PlayerConstants { 4 | const val NOTIFICATION_CHANNEL_ID = "radio_channel" 5 | const val NOTIFICATION_ID = 1 6 | 7 | const val SERVICE_TAG = "RadioService" 8 | 9 | const val NETWORK_ERROR = "NETWORK_ERROR" 10 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/metadata/ITagsDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource.metadata 2 | 3 | import com.egoriku.radiotok.datasource.entity.MetadataEntity 4 | 5 | interface ITagsDataSource { 6 | 7 | suspend fun load(): List 8 | 9 | suspend fun loadPortion(size: Int): List 10 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/metadata/ICountriesDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource.metadata 2 | 3 | import com.egoriku.radiotok.datasource.entity.MetadataEntity 4 | 5 | interface ICountriesDataSource { 6 | 7 | suspend fun load(): List 8 | 9 | suspend fun loadPortion(size: Int): List 10 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/datasource/metadata/ILanguagesDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.datasource.metadata 2 | 3 | import com.egoriku.radiotok.datasource.entity.MetadataEntity 4 | 5 | interface ILanguagesDataSource { 6 | 7 | suspend fun load(): List 8 | 9 | suspend fun loadPortion(size: Int): List 10 | } -------------------------------------------------------------------------------- /mediaItemDsl/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | minSdk 21 11 | targetSdk 31 12 | versionCode 1 13 | versionName "1.0" 14 | } 15 | } 16 | 17 | dependencies { 18 | implementation(libs.core) 19 | implementation(libs.media) 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/playlist/PlaylistState.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.playlist 2 | 3 | import com.egoriku.radiotok.presentation.screen.playlist.model.Playlist 4 | 5 | sealed class PlaylistState { 6 | object Loading : PlaylistState() 7 | data class Success(val playlist: Playlist) : PlaylistState() 8 | object Error : PlaylistState() 9 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /radioPlayer/src/main/res/drawable-anydpi/ic_skip_next.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/font/khula_semibold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /db/src/main/kotlin/com/egoriku/radiotok/db/RadioTokDb.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.egoriku.radiotok.db.dao.StationDao 6 | import com.egoriku.radiotok.db.entity.StationDbEntity 7 | 8 | @Database(entities = [StationDbEntity::class], version = 1) 9 | abstract class RadioTokDb: RoomDatabase() { 10 | 11 | abstract fun stationDao(): StationDao 12 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/repository/IMediaMetadataRepository.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.repository 2 | 3 | import android.support.v4.media.MediaMetadataCompat 4 | 5 | interface IMediaMetadataRepository { 6 | 7 | suspend fun getRandomItem(): MediaMetadataCompat 8 | suspend fun getLikedItem(): MediaMetadataCompat 9 | suspend fun loadByStationId(id: String): MediaMetadataCompat 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/foundation/HSpacer.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.foundation 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.unit.Dp 8 | 9 | @Composable 10 | fun HSpacer(height: Dp) { 11 | Spacer(modifier = Modifier.height(height)) 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/model/RadioItemModel.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.common.model 2 | 3 | import com.egoriku.radiotok.common.EMPTY 4 | 5 | data class RadioItemModel( 6 | val id: String = EMPTY, 7 | val streamUrl: String = EMPTY, 8 | val title: String = EMPTY, 9 | val subTitle: String = EMPTY, 10 | val icon: String = EMPTY, 11 | val hls: Long = 0L, 12 | val metadata: String = EMPTY 13 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tune.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /db/src/main/kotlin/com/egoriku/radiotok/db/koin/DbModule.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.db.koin 2 | 3 | import androidx.room.Room 4 | import com.egoriku.radiotok.db.RadioTokDb 5 | import org.koin.android.ext.koin.androidApplication 6 | import org.koin.dsl.module 7 | 8 | val dbModule = module { 9 | single { 10 | Room.databaseBuilder( 11 | androidApplication(), RadioTokDb::class.java, "radiotok" 12 | ).fallbackToDestructiveMigration() 13 | .build() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /radioPlayer/src/main/res/drawable-anydpi/ic_favorite.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/listener/EventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.listener 2 | 3 | import kotlinx.coroutines.flow.MutableSharedFlow 4 | import kotlinx.coroutines.flow.asSharedFlow 5 | 6 | class EventHandler { 7 | 8 | private val _event = MutableSharedFlow() 9 | val event = _event.asSharedFlow() 10 | 11 | suspend fun playNextRandom() { 12 | _event.emit(Event.PlayNextRandom) 13 | } 14 | 15 | sealed class Event { 16 | object PlayNextRandom : Event() 17 | } 18 | } -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/ext/Context.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.common.ext 2 | 3 | import android.content.Context 4 | import android.telephony.TelephonyManager 5 | import androidx.annotation.DrawableRes 6 | import androidx.appcompat.content.res.AppCompatResources 7 | 8 | val Context.telephonyManager: TelephonyManager 9 | get() = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager 10 | 11 | fun Context.drawableCompat(@DrawableRes resId: Int) = requireNotNull( 12 | AppCompatResources.getDrawable(this, resId) 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/domain/model/section/FeedType.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.domain.model.section 2 | 3 | sealed class FeedType { 4 | 5 | data class InstantPlay( 6 | val mediaId: String, 7 | val name: String, 8 | val icon: Int 9 | ) : FeedType() 10 | 11 | data class Playlist( 12 | val id: String, 13 | val name: String, 14 | val icon: Int 15 | ) : FeedType() 16 | 17 | data class SimplePlaylist( 18 | val name: String, 19 | val count: String 20 | ) : FeedType() 21 | } -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/ext/Uri.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | 3 | package com.egoriku.radiotok.common.ext 4 | 5 | import android.content.ContentResolver 6 | import android.content.Context 7 | import android.net.Uri 8 | 9 | inline fun Context.getIconUri(iconId: Int): Uri = Uri.Builder() 10 | .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 11 | .authority(resources.getResourcePackageName(iconId)) 12 | .appendPath(resources.getResourceTypeName(iconId)) 13 | .appendPath(resources.getResourceEntryName(iconId)) 14 | .build() -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("VERSION_CATALOGS") 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | dependencyResolutionManagement { 5 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 6 | repositories { 7 | google() 8 | mavenCentral() 9 | maven { url "https://oss.sonatype.org/content/repositories/snapshots" } 10 | } 11 | } 12 | 13 | rootProject.name = "RadioTok" 14 | 15 | include ':app' 16 | 17 | include ':common' 18 | include ':datasource' 19 | include ':db' 20 | include ':mediaItemDsl' 21 | include ':radioPlayer' -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/data/repository/RadioFetchNetworkRepository.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.data.repository 2 | 3 | import com.egoriku.radiotok.datasource.datasource.IAllStationsDataSource 4 | import com.egoriku.radiotok.datasource.entity.RadioEntity 5 | import com.egoriku.radiotok.domain.repository.IRadioFetchNetworkRepository 6 | 7 | class RadioFetchNetworkRepository( 8 | private val allStationsDataSource: IAllStationsDataSource 9 | ) : IRadioFetchNetworkRepository { 10 | 11 | override suspend fun load(): List = allStationsDataSource.loadAll() 12 | } 13 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | minSdk 21 12 | targetSdk 31 13 | 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation(libs.appcompat) 26 | implementation(libs.gson) 27 | } -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_not_interested.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/constant/CustomAction.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.constant 2 | 3 | object CustomAction { 4 | const val CUSTOM_ACTION_LIKE = "com.egoriku.radiotok.CUSTOM_ACTION_LIKE" 5 | const val CUSTOM_ACTION_UNLIKE = "com.egoriku.radiotok.CUSTOM_ACTION_UNLIKE" 6 | const val CUSTOM_ACTION_DISLIKE = "com.egoriku.radiotok.CUSTOM_ACTION_DISLIKE" 7 | const val CUSTOM_ACTION_NEXT = "com.egoriku.radiotok.CUSTOM_ACTION_NEXT" 8 | 9 | const val ACTION_DISLIKE = "ACTION_DISLIKE" 10 | const val ACTION_TOGGLE_FAVORITE = "ACTION_TOGGLE_FAVORITE" 11 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/AllStationsDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource 2 | 3 | import com.egoriku.radiotok.datasource.datasource.IAllStationsDataSource 4 | import com.egoriku.radiotok.datasource.intertal.api.Api 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | 8 | internal class AllStationsDataSource(private val api: Api) : IAllStationsDataSource { 9 | 10 | override suspend fun loadAll() = withContext(Dispatchers.IO) { 11 | runCatching { api.allStations() }.getOrThrow() 12 | } 13 | } -------------------------------------------------------------------------------- /db/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | id("com.google.devtools.ksp") 5 | } 6 | 7 | android { 8 | compileSdk = 31 9 | 10 | defaultConfig { 11 | minSdk = 21 12 | targetSdk = 31 13 | } 14 | 15 | buildTypes { 16 | release { 17 | isMinifyEnabled = true 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation(projects.common) 24 | 25 | ksp(libs.room.compiler) 26 | 27 | implementation(libs.koin.android) 28 | 29 | api(libs.room.runtime) 30 | implementation(libs.room.ktx) 31 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/res/drawable-anydpi/ic_not_interested.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/main/PlayerControlsActions.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.main 2 | 3 | import com.egoriku.radiotok.presentation.RadioViewModel 4 | 5 | class PlayerControlsActions(viewModel: RadioViewModel) { 6 | 7 | val dislikeRadioStationEvent: () -> Unit = { viewModel.dislikeRadioStation() } 8 | val tuneRadiosEvent: () -> Unit = {} 9 | val playPauseEvent: () -> Unit = { viewModel.togglePlayPause() } 10 | val nextRadioEvent: () -> Unit = { viewModel.nextRadioStation() } 11 | val toggleFavoriteEvent: () -> Unit = { viewModel.likeRadioStation() } 12 | } -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/mapper/MetadataBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.common.mapper 2 | 3 | import com.egoriku.radiotok.common.ext.toFlagEmoji 4 | 5 | object MetadataBuilder { 6 | 7 | fun build(countryCode: String, tags: String) = buildList { 8 | if (countryCode.isNotEmpty()) { 9 | add(countryCode.toFlagEmoji) 10 | } 11 | 12 | if (tags.isNotEmpty()) { 13 | add( 14 | tags 15 | .replace(",,", ",") 16 | .replace(",", ", ") 17 | ) 18 | } 19 | }.joinToString(separator = " ") 20 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/IEntitySourceFactory.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource 2 | 3 | import com.egoriku.radiotok.datasource.entity.MetadataEntity 4 | import com.egoriku.radiotok.datasource.entity.RadioEntity 5 | import com.egoriku.radiotok.datasource.entity.params.MetadataParams 6 | import com.egoriku.radiotok.datasource.entity.params.PlaylistParams 7 | 8 | interface IEntitySourceFactory { 9 | 10 | suspend fun loadBy(params: PlaylistParams): List 11 | 12 | suspend fun loadBy(params: MetadataParams): List 13 | 14 | suspend fun loadByIds(ids: List): List 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/extension/Modifier.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.extension 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.composed 8 | 9 | inline fun Modifier.noRippleClickable( 10 | enabled: Boolean = true, 11 | noinline onClick: () -> Unit 12 | ): Modifier = composed { 13 | clickable( 14 | enabled = enabled, 15 | indication = null, 16 | interactionSource = remember { MutableInteractionSource() }, 17 | onClick = onClick 18 | ) 19 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/ext/MediaTransportControls.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | 3 | package com.egoriku.radiotok.radioplayer.ext 4 | 5 | import android.support.v4.media.session.MediaControllerCompat.TransportControls 6 | import androidx.core.os.bundleOf 7 | import com.egoriku.radiotok.radioplayer.constant.CustomAction.ACTION_DISLIKE 8 | import com.egoriku.radiotok.radioplayer.constant.CustomAction.ACTION_TOGGLE_FAVORITE 9 | 10 | inline fun TransportControls.sendDislikeAction() { 11 | sendCustomAction(ACTION_DISLIKE, bundleOf()) 12 | } 13 | 14 | inline fun TransportControls.sendLikeAction() { 15 | sendCustomAction(ACTION_TOGGLE_FAVORITE, bundleOf()) 16 | } -------------------------------------------------------------------------------- /.github/workflows/auto_release.yml: -------------------------------------------------------------------------------- 1 | name: Build & Publish Release APK 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | Gradle: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout code 13 | uses: actions/checkout@v2 14 | - name: setup jdk 15 | uses: actions/setup-java@v1 16 | with: 17 | java-version: 11 18 | - name: Make Gradle executable 19 | run: chmod +x ./gradlew 20 | - name: Build Release APK 21 | run: ./gradlew assembleRelease 22 | - name: Releasing using Hub 23 | uses: kyze8439690/action-release-releaseapk@master 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 26 | APP_FOLDER: app 27 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_country.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/data/mediator/IRadioCacheMediator.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.data.mediator 2 | 3 | import android.support.v4.media.MediaBrowserCompat 4 | import android.support.v4.media.MediaMetadataCompat 5 | import com.egoriku.radiotok.radioplayer.model.MediaPath 6 | 7 | interface IRadioCacheMediator { 8 | 9 | suspend fun playSingle(id: String) 10 | 11 | suspend fun playNextRandom() 12 | 13 | suspend fun getMediaBrowserItemsBy(mediaPath: MediaPath): List 14 | 15 | suspend fun getMediaMetadataBy(mediaPath: MediaPath): MediaMetadataCompat 16 | 17 | suspend fun updatePlaylist(mediaPath: MediaPath) 18 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/res/drawable-anydpi/ic_favorite_border.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/provider/IStringResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.common.provider 2 | 3 | interface IStringResourceProvider { 4 | 5 | val shuffleAndPlay: String 6 | val personalPlaylists: String 7 | val smartPlaylists: String 8 | val byTags: String 9 | val byCountry: String 10 | val byLanguage: String 11 | val catalog: String 12 | 13 | val likedRadio: String 14 | val randomRadio: String 15 | 16 | val liked: String 17 | val recentlyPlayed: String 18 | val disliked: String 19 | 20 | val localStations: String 21 | val topClicks: String 22 | val topVote: String 23 | val changedLately: String 24 | val playing: String 25 | 26 | fun getStationsCount(count: Int): String 27 | } -------------------------------------------------------------------------------- /datasource/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | } 5 | 6 | android { 7 | compileSdk = 31 8 | 9 | defaultConfig { 10 | minSdk = 21 11 | targetSdk = 31 12 | consumerProguardFiles("consumer-rules.pro") 13 | } 14 | 15 | buildTypes { 16 | release { 17 | isMinifyEnabled = true 18 | proguardFiles( 19 | "proguard-rules.pro", 20 | getDefaultProguardFile("proguard-android-optimize.txt") 21 | ) 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation(projects.common) 28 | 29 | implementation(libs.coroutines.android) 30 | implementation(libs.gson) 31 | implementation(libs.koin.android) 32 | implementation(libs.retrofit) 33 | } -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/provider/IBitmapProvider.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.common.provider 2 | 3 | import android.graphics.Bitmap 4 | import android.net.Uri 5 | 6 | interface IBitmapProvider { 7 | 8 | val icCollection: Bitmap 9 | val icPersonal: Bitmap 10 | val icRadioWaves: Bitmap 11 | val icSmartPlaylist: Bitmap 12 | 13 | val icChangedLatelyRound: Bitmap 14 | val icCountryRounded: Bitmap 15 | val icDislikedRound: Bitmap 16 | val icTagsRound: Bitmap 17 | val icHistoryRound: Bitmap 18 | val icLanguageRound: Bitmap 19 | val icLikedRound: Bitmap 20 | val icLocalRound: Bitmap 21 | val icPlayingRound: Bitmap 22 | val icShuffleRound: Bitmap 23 | val icTopClicksRound: Bitmap 24 | val icTopVoteRound: Bitmap 25 | 26 | val bgRadioGradient: Uri 27 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Icon 3 |
4 | 5 |

6 | RadioTok - listen to the radio from over the world 7 |

8 | 9 | 10 | ## Android phone UI 11 | Icon 12 | 13 | ## Android Auto UI 14 | Icon Icon 15 | 16 | 17 | ## Key features: 18 | - Simple approach for listening radio 19 | - Full support Android Auto, able to like/dislike radio stations, playlists and etc 20 | - Build with Jetpack Compose 21 | - Custom MediaItem DSL library 22 | 23 | ## Roadmap 24 | 25 | https://github.com/egorikftp/RadioTok/issues/7 26 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /radioPlayer/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | } 5 | 6 | android { 7 | compileSdk = 31 8 | buildToolsVersion = "30.0.3" 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | targetSdkVersion 31 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled true 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation projects.common 24 | implementation projects.datasource 25 | 26 | implementation projects.db 27 | implementation projects.mediaItemDsl 28 | 29 | implementation libs.core 30 | implementation libs.glide 31 | implementation libs.koin.android 32 | implementation libs.lifecycle.runtime 33 | implementation libs.media 34 | 35 | implementation libs.bundles.exoPlayer 36 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/RadioInfoDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource 2 | 3 | import com.egoriku.radiotok.datasource.datasource.IRadioInfoDataSource 4 | import com.egoriku.radiotok.datasource.intertal.api.Api 5 | import com.egoriku.radiotok.datasource.intertal.repair.RadioStationRepair 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | internal class RadioInfoDataSource(private val api: Api) : IRadioInfoDataSource { 10 | 11 | override suspend fun loadByIds(ids: List) = withContext(Dispatchers.IO) { 12 | runCatching { 13 | api.stationsById(ids = ids.joinToString(separator = ",")) 14 | .map { RadioStationRepair.tryToFix(it) } 15 | }.getOrDefault(emptyList()) 16 | } 17 | } -------------------------------------------------------------------------------- /db/src/main/kotlin/com/egoriku/radiotok/db/entity/StationDbEntity.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.db.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity 8 | data class StationDbEntity( 9 | @PrimaryKey(autoGenerate = true) val id: Int = 0, 10 | @ColumnInfo(name = "stationUuid") val stationUuid: String, 11 | @ColumnInfo(name = "name") val name: String, 12 | @ColumnInfo(name = "streamUrl") val streamUrl: String, 13 | @ColumnInfo(name = "icon") val icon: String, 14 | @ColumnInfo(name = "hls") val hls: Long, 15 | @ColumnInfo(name = "countryCode") val countryCode: String, 16 | @ColumnInfo(name = "tags") val tags: String, 17 | @ColumnInfo(name = "isLiked") val isLiked: Boolean, 18 | @ColumnInfo(name = "isExcluded") val isExcluded: Boolean, 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.settings 2 | 3 | import android.net.Uri 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.egoriku.radiotok.util.BackupManager 7 | import kotlinx.coroutines.launch 8 | 9 | class SettingsViewModel( 10 | private val backupManager: BackupManager 11 | ) : ViewModel() { 12 | 13 | val backupFileName: String 14 | get() = backupManager.backupFileName 15 | 16 | fun createBackup(uri: Uri) { 17 | viewModelScope.launch { 18 | backupManager.backup(outputFile = uri) 19 | } 20 | } 21 | 22 | fun restoreBackup(uri: Uri) { 23 | viewModelScope.launch { 24 | backupManager.restore(inputFile = uri) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/playlist/TopVoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource.playlist 2 | 3 | import com.egoriku.radiotok.datasource.datasource.playlist.ITopVoteDataSource 4 | import com.egoriku.radiotok.datasource.entity.RadioEntity 5 | import com.egoriku.radiotok.datasource.intertal.api.Api 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | internal class TopVoteDataSource(private val api: Api) : ITopVoteDataSource { 10 | 11 | private var result: List? = null 12 | 13 | override suspend fun load() = runCatching { 14 | withContext(Dispatchers.IO) { 15 | result ?: api.topVote(limit = 100).also { 16 | result = it 17 | } 18 | } 19 | }.getOrDefault(emptyList()) 20 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/playlist/TopClicksDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource.playlist 2 | 3 | import com.egoriku.radiotok.datasource.datasource.playlist.ITopClicksDataSource 4 | import com.egoriku.radiotok.datasource.entity.RadioEntity 5 | import com.egoriku.radiotok.datasource.intertal.api.Api 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | internal class TopClicksDataSource(private val api: Api) : ITopClicksDataSource { 10 | 11 | private var result: List? = null 12 | 13 | override suspend fun load() = runCatching { 14 | withContext(Dispatchers.IO) { 15 | result ?: api.topClicks(limit = 100).also { 16 | result = it 17 | } 18 | } 19 | }.getOrDefault(emptyList()) 20 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/res/drawable-anydpi/ic_radio_tower.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/playlist/PlayingNowDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource.playlist 2 | 3 | import com.egoriku.radiotok.datasource.datasource.playlist.IPlayingNowDataSource 4 | import com.egoriku.radiotok.datasource.entity.RadioEntity 5 | import com.egoriku.radiotok.datasource.intertal.api.Api 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | internal class PlayingNowDataSource(private val api: Api) : IPlayingNowDataSource { 10 | 11 | private var result: List? = null 12 | 13 | override suspend fun load() = runCatching { 14 | withContext(Dispatchers.IO) { 15 | result ?: api.playingNow(limit = 100).also { 16 | result = it 17 | } 18 | } 19 | }.getOrDefault(emptyList()) 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/data/mapper/NetworkStationToDbMapper.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.data.mapper 2 | 3 | import com.egoriku.radiotok.common.ext.IMapper 4 | import com.egoriku.radiotok.datasource.entity.RadioEntity 5 | import com.egoriku.radiotok.db.entity.StationDbEntity 6 | 7 | class NetworkStationToDbMapper : IMapper { 8 | 9 | override fun invoke(networkEntity: RadioEntity) = 10 | StationDbEntity( 11 | stationUuid = networkEntity.stationUuid, 12 | name = networkEntity.name, 13 | streamUrl = networkEntity.streamUrl, 14 | icon = networkEntity.icon, 15 | hls = networkEntity.hls, 16 | countryCode = networkEntity.countryCode, 17 | tags = networkEntity.tags, 18 | isLiked = false, 19 | isExcluded = false 20 | ) 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/data/retrofit/ApiEndpoint.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.data.retrofit 2 | 3 | import com.egoriku.radiotok.datasource.intertal.api.Api 4 | import okhttp3.OkHttpClient 5 | import retrofit2.Retrofit 6 | import retrofit2.converter.gson.GsonConverterFactory 7 | import retrofit2.create 8 | 9 | private const val DEFAULT_ENDPOINT = "https://github.com/" 10 | 11 | internal class ApiEndpoint(hostSelectionInterceptor: HostSelectionInterceptor) { 12 | 13 | private val okHttpClient = OkHttpClient.Builder() 14 | .addInterceptor(hostSelectionInterceptor) 15 | .build() 16 | 17 | private val retrofit = Retrofit.Builder() 18 | .baseUrl(DEFAULT_ENDPOINT) 19 | .callFactory(okHttpClient) 20 | .addConverterFactory(GsonConverterFactory.create()) 21 | .build() 22 | 23 | fun create(): Api = retrofit.create() 24 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/playlist/ChangedLatelyDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource.playlist 2 | 3 | import com.egoriku.radiotok.datasource.datasource.playlist.IChangedLatelyDataSource 4 | import com.egoriku.radiotok.datasource.entity.RadioEntity 5 | import com.egoriku.radiotok.datasource.intertal.api.Api 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | internal class ChangedLatelyDataSource(private val api: Api) : IChangedLatelyDataSource { 10 | 11 | private var result: List? = null 12 | 13 | override suspend fun load() = runCatching { 14 | withContext(Dispatchers.IO) { 15 | result ?: api.changedLately(limit = 100).also { 16 | result = it 17 | } 18 | } 19 | }.getOrDefault(emptyList()) 20 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/ext/PlaybackStateCompat.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.ext 2 | 3 | import android.support.v4.media.session.PlaybackStateCompat 4 | 5 | inline val PlaybackStateCompat.isPlaying 6 | get() = state == PlaybackStateCompat.STATE_BUFFERING || 7 | state == PlaybackStateCompat.STATE_PLAYING 8 | 9 | inline val PlaybackStateCompat.isPrepared 10 | get() = state == PlaybackStateCompat.STATE_BUFFERING || 11 | state == PlaybackStateCompat.STATE_PLAYING || 12 | state == PlaybackStateCompat.STATE_PAUSED 13 | 14 | inline val PlaybackStateCompat.isError 15 | get() = state == PlaybackStateCompat.STATE_ERROR 16 | 17 | inline val PlaybackStateCompat.isPlayEnabled 18 | get() = actions and PlaybackStateCompat.ACTION_PLAY != 0L || 19 | (actions and PlaybackStateCompat.ACTION_PLAY_PAUSE != 0L && state == PlaybackStateCompat.STATE_PAUSED) -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/data/RadioStateMediator.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.data 2 | 3 | import com.egoriku.radiotok.db.RadioTokDb 4 | import com.google.android.exoplayer2.ui.PlayerNotificationManager 5 | import kotlinx.coroutines.runBlocking 6 | 7 | class RadioStateMediator(private val radioTokDb: RadioTokDb) { 8 | 9 | fun exclude(id: String) = runBlocking { 10 | radioTokDb.stationDao().toggleExcludedState(stationId = id) 11 | } 12 | 13 | fun isLiked(id: String) = runBlocking { 14 | radioTokDb.stationDao().isStationLiked(stationId = id) 15 | } 16 | 17 | fun toggleLiked( 18 | id: String, 19 | playerNotificationManager: PlayerNotificationManager? = null 20 | ) { 21 | runBlocking { 22 | radioTokDb.stationDao().toggleLikedState(stationId = id) 23 | } 24 | 25 | playerNotificationManager?.invalidate() 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/playlist/components/ShuffleButton.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.playlist.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.shape.CircleShape 5 | import androidx.compose.material.Button 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.clip 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun ShuffleButton() { 14 | Button( 15 | onClick = {}, 16 | modifier = Modifier 17 | .padding(vertical = 12.dp) 18 | .clip(CircleShape) 19 | ) { 20 | // TODO: 17.09.21 Localize string 21 | Text( 22 | text = "PLAY All", 23 | modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp) 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /radioPlayer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/repository/RadioEntityToModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.repository 2 | 3 | import com.egoriku.radiotok.common.ext.IMapper 4 | import com.egoriku.radiotok.common.mapper.MetadataBuilder 5 | import com.egoriku.radiotok.common.model.RadioItemModel 6 | import com.egoriku.radiotok.datasource.entity.RadioEntity 7 | 8 | class RadioEntityToModelMapper : IMapper { 9 | 10 | override fun invoke(radioEntity: RadioEntity): RadioItemModel = 11 | RadioItemModel( 12 | id = radioEntity.stationUuid, 13 | title = radioEntity.name, 14 | streamUrl = radioEntity.streamUrl, 15 | icon = radioEntity.icon, 16 | hls = radioEntity.hls, 17 | metadata = MetadataBuilder.build( 18 | countryCode = radioEntity.countryCode, 19 | tags = radioEntity.tags 20 | ) 21 | ) 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.core.view.WindowCompat 7 | import cafe.adriel.voyager.navigator.Navigator 8 | import com.egoriku.radiotok.presentation.screen.main.MainScreen 9 | import com.egoriku.radiotok.presentation.ui.RadioTokTheme 10 | import com.google.accompanist.insets.ProvideWindowInsets 11 | 12 | class MainActivity : ComponentActivity() { 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | WindowCompat.setDecorFitsSystemWindows(window, false) 18 | 19 | setContent { 20 | RadioTokTheme { 21 | ProvideWindowInsets { 22 | Navigator(screen = MainScreen) 23 | } 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_playing.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /common/src/main/java/com/egoriku/radiotok/common/ext/String.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.common.ext 2 | 3 | import java.util.* 4 | 5 | val String.toFlagEmoji: String 6 | get() { 7 | // 1. It first checks if the string consists of only 2 characters: 8 | // ISO 3166-1 alpha-2 two-letter country codes (https://en.wikipedia.org/wiki/Regional_Indicator_Symbol). 9 | if (this.length != 2) { 10 | return this 11 | } 12 | 13 | val countryCodeCaps = uppercase(Locale.getDefault()) 14 | val firstLetter = Character.codePointAt(countryCodeCaps, 0) - 0x41 + 0x1F1E6 15 | val secondLetter = Character.codePointAt(countryCodeCaps, 1) - 0x41 + 0x1F1E6 16 | 17 | // 2. It then checks if both characters are alphabet 18 | if (!countryCodeCaps[0].isLetter() || !countryCodeCaps[1].isLetter()) { 19 | return this 20 | } 21 | 22 | return String(Character.toChars(firstLetter)) + String(Character.toChars(secondLetter)) 23 | } -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_playing.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /mediaItemDsl/src/main/java/com/egoriku/mediaitemdsl/MediaItemDsl.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.mediaitemdsl 2 | 3 | import android.support.v4.media.MediaBrowserCompat.MediaItem 4 | import android.support.v4.media.MediaMetadataCompat 5 | import com.egoriku.mediaitemdsl.mediaitem.MediaItemBuilder 6 | 7 | inline fun mediaItem( 8 | flag: Int, 9 | body: MediaItemBuilder.() -> Unit 10 | ) = MediaItemBuilder(flag).apply(body).build() 11 | 12 | inline fun playableMediaItem( 13 | body: MediaItemBuilder.() -> Unit 14 | ) = MediaItemBuilder(flag = MediaItem.FLAG_PLAYABLE).apply(body).build() 15 | 16 | fun playableMediaItem( 17 | metadata: MediaMetadataCompat 18 | ) = MediaItem(metadata.description, MediaItem.FLAG_PLAYABLE) 19 | 20 | inline fun browsableMediaItem( 21 | body: MediaItemBuilder.() -> Unit 22 | ) = MediaItemBuilder(flag = MediaItem.FLAG_BROWSABLE).apply(body).build() 23 | 24 | fun browsableMediaItem( 25 | metadata: MediaMetadataCompat 26 | ) = MediaItem(metadata.description, MediaItem.FLAG_BROWSABLE) 27 | -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/random/entity/ServerStats.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource.random.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ServerStats( 6 | 7 | @SerializedName("supported_version") 8 | val supportedVersion: Int, 9 | 10 | @SerializedName("software_version") 11 | val softwareVersion: String, 12 | 13 | @SerializedName("status") 14 | val status: String, 15 | 16 | @SerializedName("stations") 17 | val stations: Int, 18 | 19 | @SerializedName("stations_broken") 20 | val stationsBroken: Int, 21 | 22 | @SerializedName("tags") 23 | val tags: Int, 24 | 25 | @SerializedName("clicks_last_hour") 26 | val clicksLastHour: Int, 27 | 28 | @SerializedName("clicks_last_day") 29 | val clicksLastDay: Int, 30 | 31 | @SerializedName("languages") 32 | val languages: Int, 33 | 34 | @SerializedName("countries") 35 | val countries: Int 36 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/ui/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.ui 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.lightColors 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.graphics.Color 8 | 9 | val RadioColors = lightColors( 10 | primary = Color(0xFFEE894F), 11 | onPrimary = Color(0xFFF4EFE7), 12 | primaryVariant = Color(0xFFEE894F), 13 | secondary = Color(0xFF393939), 14 | onSecondary = Color(0xFFF4EFE7), 15 | error = Color.Red, 16 | onError = Color(0xFFF4EFE7) 17 | ) 18 | 19 | @Composable 20 | fun RadioTokTheme( 21 | darkTheme: Boolean = isSystemInDarkTheme(), 22 | content: @Composable () -> Unit 23 | ) { 24 | val colors = if (darkTheme) { 25 | RadioColors 26 | } else { 27 | RadioColors 28 | } 29 | 30 | MaterialTheme( 31 | colors = colors, 32 | content = content 33 | ) 34 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/metadata/TagsDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource.metadata 2 | 3 | import com.egoriku.radiotok.datasource.datasource.metadata.ITagsDataSource 4 | import com.egoriku.radiotok.datasource.entity.MetadataEntity 5 | import com.egoriku.radiotok.datasource.intertal.api.Api 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | internal class TagsDataSource(private val api: Api) : ITagsDataSource { 10 | 11 | private var result: List? = null 12 | 13 | override suspend fun load() = runCatching { 14 | withContext(Dispatchers.IO) { 15 | result ?: api.allTags().also { 16 | result = it 17 | } 18 | } 19 | }.getOrDefault(emptyList()) 20 | 21 | override suspend fun loadPortion(size: Int) = runCatching { 22 | load() 23 | requireNotNull(result).subList(0, size) 24 | }.getOrDefault(emptyList()) 25 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/entity/RadioEntity.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class RadioEntity( 6 | @SerializedName("name") 7 | val name: String, 8 | 9 | @SerializedName("stationuuid") 10 | val stationUuid: String, 11 | 12 | @SerializedName("url_resolved") 13 | val streamUrl: String, 14 | 15 | @SerializedName("homepage") 16 | val homePageUrl: String, 17 | 18 | @SerializedName("favicon") 19 | val icon: String, 20 | 21 | @SerializedName("countrycode") 22 | val countryCode: String, 23 | 24 | @SerializedName("state") 25 | val state: String, 26 | 27 | @SerializedName("tags") 28 | val tags: String, 29 | 30 | @SerializedName("language") 31 | val language: String, 32 | 33 | @SerializedName("bitrate") 34 | val bitrate: Int, 35 | 36 | @SerializedName("codec") 37 | val codec: String, 38 | 39 | @SerializedName("hls") 40 | val hls: Long 41 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/main/ui/actions/TuneAction.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.main.ui.actions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.res.painterResource 6 | import androidx.compose.ui.res.stringResource 7 | import androidx.compose.ui.tooling.preview.Preview 8 | import com.egoriku.radiotok.R 9 | import com.egoriku.radiotok.foundation.button.IconButton 10 | import com.egoriku.radiotok.presentation.ui.RadioTokTheme 11 | 12 | @Preview(showBackground = true) 13 | @Composable 14 | fun TuneActionPreview() { 15 | RadioTokTheme { 16 | TuneAction {} 17 | } 18 | } 19 | 20 | @Composable 21 | fun TuneAction( 22 | modifier: Modifier = Modifier, 23 | onClick: () -> Unit 24 | ) { 25 | IconButton( 26 | modifier = modifier, 27 | painter = painterResource(R.drawable.ic_tune), 28 | contentDescription = stringResource(id = R.string.cc_tune) 29 | ) { 30 | onClick() 31 | } 32 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/metadata/CountriesDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource.metadata 2 | 3 | import com.egoriku.radiotok.datasource.datasource.metadata.ICountriesDataSource 4 | import com.egoriku.radiotok.datasource.entity.MetadataEntity 5 | import com.egoriku.radiotok.datasource.intertal.api.Api 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | internal class CountriesDataSource(private val api: Api) : ICountriesDataSource { 10 | 11 | private var result: List? = null 12 | 13 | override suspend fun load() = runCatching { 14 | withContext(Dispatchers.IO) { 15 | result ?: api.allCountries().also { 16 | result = it 17 | } 18 | } 19 | }.getOrDefault(emptyList()) 20 | 21 | override suspend fun loadPortion(size: Int) = runCatching { 22 | load() 23 | requireNotNull(result).subList(0, size) 24 | }.getOrDefault(emptyList()) 25 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/metadata/LanguagesDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource.metadata 2 | 3 | import com.egoriku.radiotok.datasource.datasource.metadata.ILanguagesDataSource 4 | import com.egoriku.radiotok.datasource.entity.MetadataEntity 5 | import com.egoriku.radiotok.datasource.intertal.api.Api 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | internal class LanguagesDataSource(private val api: Api) : ILanguagesDataSource { 10 | 11 | private var result: List? = null 12 | 13 | override suspend fun load() = runCatching { 14 | withContext(Dispatchers.IO) { 15 | result ?: api.allLanguages().also { 16 | result = it 17 | } 18 | } 19 | }.getOrDefault(emptyList()) 20 | 21 | override suspend fun loadPortion(size: Int) = runCatching { 22 | load() 23 | requireNotNull(result).subList(0, size) 24 | }.getOrDefault(emptyList()) 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/main/ui/actions/SkipNextAction.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.main.ui.actions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.res.painterResource 6 | import androidx.compose.ui.res.stringResource 7 | import androidx.compose.ui.tooling.preview.Preview 8 | import com.egoriku.radiotok.R 9 | import com.egoriku.radiotok.foundation.button.IconButton 10 | import com.egoriku.radiotok.presentation.ui.RadioTokTheme 11 | 12 | @Preview(showBackground = true) 13 | @Composable 14 | fun SkipNextActionPreview() { 15 | RadioTokTheme { 16 | SkipNextAction {} 17 | } 18 | } 19 | 20 | @Composable 21 | fun SkipNextAction( 22 | modifier: Modifier = Modifier, 23 | onClick: () -> Unit 24 | ) { 25 | IconButton( 26 | modifier = modifier, 27 | painter = painterResource(R.drawable.ic_skip_next), 28 | contentDescription = stringResource(id = R.string.cc_skip_next) 29 | ) { 30 | onClick() 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/foundation/header/SectionHeader.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.foundation.header 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | 11 | @Composable 12 | fun SectionHeader( 13 | title: String, 14 | content: @Composable ColumnScope.() -> Unit = {}, 15 | ) { 16 | Row( 17 | modifier = Modifier 18 | .fillMaxWidth() 19 | .padding(vertical = 16.dp), 20 | horizontalArrangement = Arrangement.SpaceBetween, 21 | verticalAlignment = Alignment.CenterVertically 22 | ) { 23 | Text( 24 | modifier = Modifier.padding(start = 16.dp), 25 | text = title, 26 | style = MaterialTheme.typography.h6 27 | ) 28 | } 29 | Column(modifier = Modifier.fillMaxWidth()) { 30 | content() 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/foundation/header/ScreenHeader.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.foundation.header 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.text.font.FontWeight 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun ScreenHeader( 14 | title: String, 15 | actions: @Composable RowScope.() -> Unit = {} 16 | ) { 17 | Row( 18 | modifier = Modifier 19 | .fillMaxWidth() 20 | .padding(vertical = 16.dp), 21 | horizontalArrangement = Arrangement.SpaceBetween, 22 | verticalAlignment = Alignment.CenterVertically 23 | ) { 24 | Text( 25 | modifier = Modifier.padding(start = 16.dp), 26 | text = title, 27 | style = MaterialTheme.typography.h5.copy(fontWeight = FontWeight.Bold) 28 | ) 29 | actions() 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/main/ui/actions/NotInterestedAction.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.main.ui.actions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.res.painterResource 6 | import androidx.compose.ui.res.stringResource 7 | import androidx.compose.ui.tooling.preview.Preview 8 | import com.egoriku.radiotok.R 9 | import com.egoriku.radiotok.foundation.button.IconButton 10 | import com.egoriku.radiotok.presentation.ui.RadioTokTheme 11 | 12 | @Preview(showBackground = true) 13 | @Composable 14 | fun NotInterestedActionPreview() { 15 | RadioTokTheme { 16 | NotInterestedAction {} 17 | } 18 | } 19 | 20 | @Composable 21 | fun NotInterestedAction( 22 | modifier: Modifier = Modifier, 23 | onClick: () -> Unit 24 | ) { 25 | IconButton( 26 | modifier = modifier, 27 | painter = painterResource(R.drawable.ic_not_interested), 28 | contentDescription = stringResource(id = R.string.cc_not_interested) 29 | ) { 30 | onClick() 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/domain/usecase/RadioCacheUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.domain.usecase 2 | 3 | import com.egoriku.radiotok.common.ext.logD 4 | import com.egoriku.radiotok.data.mapper.NetworkStationToDbMapper 5 | import com.egoriku.radiotok.db.RadioTokDb 6 | import com.egoriku.radiotok.domain.repository.IRadioFetchNetworkRepository 7 | 8 | class RadioCacheUseCase( 9 | private val radioTokDb: RadioTokDb, 10 | private val radioFetchNetworkRepository: IRadioFetchNetworkRepository 11 | ) : IRadioCacheUseCase { 12 | 13 | override suspend fun preCacheStations() { 14 | if (radioTokDb.stationDao().getStationsCount() == 0) { 15 | logD("preCacheStations()") 16 | loadAndCache() 17 | } 18 | } 19 | 20 | private suspend fun loadAndCache() { 21 | val stations = radioFetchNetworkRepository 22 | .load() 23 | .map(transform = NetworkStationToDbMapper()) 24 | 25 | radioTokDb.stationDao().insertAll(stations) 26 | } 27 | } 28 | 29 | interface IRadioCacheUseCase { 30 | 31 | suspend fun preCacheStations() 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/RadioApplication.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.egoriku.radiotok 4 | 5 | import android.app.Application 6 | import com.egoriku.radiotok.datasource.koin.dataSourceModule 7 | import com.egoriku.radiotok.db.koin.dbModule 8 | import com.egoriku.radiotok.koin.* 9 | import com.egoriku.radiotok.radioplayer.koin.exoPlayerModule 10 | import com.egoriku.radiotok.radioplayer.koin.radioPlayerModule 11 | import org.koin.android.ext.koin.androidContext 12 | import org.koin.core.context.startKoin 13 | 14 | class RadioApplication : Application() { 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | 19 | startKoin { 20 | androidContext(this@RadioApplication) 21 | modules( 22 | appScope, 23 | dataSourceModule, 24 | dbModule, 25 | exoPlayerModule, 26 | feedScreenModule, 27 | network, 28 | playlistScreenModule, 29 | radioModule, 30 | radioPlayerModule, 31 | settingsScreenModule 32 | ) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/repository/IMediaItemRepository.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.repository 2 | 3 | import android.support.v4.media.MediaBrowserCompat.MediaItem 4 | import com.egoriku.radiotok.datasource.entity.RadioEntity 5 | 6 | interface IMediaItemRepository { 7 | 8 | fun getRootItems(): List 9 | fun getShuffleAndPlayItems(): List 10 | 11 | fun getPersonalPlaylistsItems(): List 12 | fun getLikedItems(): List 13 | fun getLikedItemsTest(): List 14 | fun getRecentlyPlayedItems(): List 15 | fun getDislikedItems(): List 16 | 17 | fun getSmartPlaylistsItems(): List 18 | fun getLocalItems(): List 19 | fun getTopClicksItems(): List 20 | fun getTopVoteItems(): List 21 | fun getChangedLatelyItems(): List 22 | fun getPlayingItems(): List 23 | 24 | fun getCatalogItems(): List 25 | fun getCatalogTags(): List 26 | fun getCatalogCountries(): List 27 | fun getCatalogLanguages(): List 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/data/retrofit/HostSelectionInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.data.retrofit 2 | 3 | import com.egoriku.radiotok.common.ext.logD 4 | import com.egoriku.radiotok.domain.datasource.IRadioDnsServer 5 | import kotlinx.coroutines.runBlocking 6 | import okhttp3.Interceptor 7 | import okhttp3.Response 8 | import java.io.IOException 9 | 10 | class HostSelectionInterceptor( 11 | private val radioDnsServer: IRadioDnsServer 12 | ) : Interceptor { 13 | 14 | private val host: String by lazy { 15 | runBlocking { 16 | radioDnsServer.lookup().random() 17 | } 18 | } 19 | 20 | @Throws(IOException::class) 21 | override fun intercept(chain: Interceptor.Chain): Response { 22 | val request = chain.request() 23 | 24 | return if (request.url.host == "github.com") { 25 | runBlocking { 26 | logD("base host: $host") 27 | val newUrl = request.url.newBuilder().host(host).build() 28 | 29 | chain.proceed(request.newBuilder().url(newUrl).build()) 30 | } 31 | } else { 32 | chain.proceed(request) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/constant/MediaBrowserConstant.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.constant 2 | 3 | object MediaBrowserConstant { 4 | 5 | const val PATH_ROOT = "/" 6 | const val PATH_ROOT_SHUFFLE_AND_PLAY = "/shuffle_and_play" 7 | const val PATH_ROOT_PERSONAL_COLLECTION = "/personal" 8 | const val PATH_ROOT_SMART_COLLECTION = "/smart_collection" 9 | const val PATH_ROOT_SMART_CATALOG = "/catalog" 10 | 11 | const val SUB_PATH_SHUFFLE_RANDOM = "/shuffle_random" 12 | const val SUB_PATH_SHUFFLE_LIKED = "/shuffle_liked" 13 | 14 | const val SUB_PATH_LIKED = "/liked" 15 | const val SUB_PATH_RECENTLY_PLAYED = "/recently_played" 16 | const val SUB_PATH_DISLIKED = "/disliked" 17 | 18 | const val SUB_PATH_LOCAL_STATIONS = "/local_stations" 19 | const val SUB_PATH_TOP_CLICKS = "/top_clicks" 20 | const val SUB_PATH_TOP_VOTE = "/top_vote" 21 | const val SUB_PATH_CHANGED_LATELY = "/changed_lately" 22 | const val SUB_PATH_PLAYING = "/playing" 23 | 24 | const val SUB_PATH_BY_TAGS = "/by_tags" 25 | const val SUB_PATH_BY_COUNTRIES = "/by_countries" 26 | const val SUB_PATH_BY_LANGUAGES = "/by_languages" 27 | 28 | const val PLAY_LIKED = "play_liked" 29 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/notification/listener/NotificationMediaButtonEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.notification.listener 2 | 3 | import android.content.Intent 4 | import android.view.KeyEvent 5 | import com.google.android.exoplayer2.Player 6 | import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector 7 | 8 | class NotificationMediaButtonEventHandler( 9 | private val onNext: () -> Unit 10 | ) : MediaSessionConnector.MediaButtonEventHandler { 11 | 12 | override fun onMediaButtonEvent(player: Player, mediaButtonEvent: Intent): Boolean { 13 | var canHandle = false 14 | 15 | val extras = mediaButtonEvent.extras ?: return canHandle 16 | 17 | if (extras.containsKey(Intent.EXTRA_KEY_EVENT)) { 18 | val keyEvent = extras.getParcelable(Intent.EXTRA_KEY_EVENT) 19 | 20 | if (keyEvent?.action == KeyEvent.ACTION_DOWN) { 21 | when (keyEvent.keyCode) { 22 | KeyEvent.KEYCODE_MEDIA_NEXT -> { 23 | onNext() 24 | canHandle = true 25 | } 26 | } 27 | } 28 | } 29 | return canHandle 30 | } 31 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/foundation/button/IconButton.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.foundation.button 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.material.IconButton 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.painter.Painter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | 10 | @Composable 11 | fun IconButton( 12 | modifier: Modifier = Modifier, 13 | painter: Painter, 14 | contentDescription: String, 15 | onClick: () -> Unit 16 | ) { 17 | IconButton( 18 | onClick = onClick, 19 | modifier = modifier 20 | ) { 21 | Image( 22 | painter = painter, 23 | contentDescription = contentDescription 24 | ) 25 | } 26 | } 27 | 28 | @Composable 29 | fun IconButton( 30 | modifier: Modifier = Modifier, 31 | imageVector: ImageVector, 32 | contentDescription: String? = null, 33 | onClick: () -> Unit 34 | ) { 35 | IconButton( 36 | onClick = onClick, 37 | modifier = modifier 38 | ) { 39 | Image( 40 | imageVector = imageVector, 41 | contentDescription = contentDescription 42 | ) 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/playlist/PlaylistViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.playlist 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.egoriku.radiotok.common.ext.logD 6 | import com.egoriku.radiotok.domain.usecase.PlaylistUseCase 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.asStateFlow 9 | import kotlinx.coroutines.launch 10 | 11 | class PlaylistViewModel( 12 | private val playlistUseCase: PlaylistUseCase 13 | ) : ViewModel() { 14 | 15 | private val _playlistState = MutableStateFlow(PlaylistState.Loading) 16 | val playlistState = _playlistState.asStateFlow() 17 | 18 | init { 19 | logD("PlaylistViewModel created") 20 | } 21 | 22 | fun load(id: String) { 23 | viewModelScope.launch { 24 | _playlistState.emit(PlaylistState.Loading) 25 | 26 | val result = playlistUseCase.loadPlaylist(id) 27 | 28 | when { 29 | result.isFailure -> _playlistState.emit(PlaylistState.Error) 30 | else -> _playlistState.emit(PlaylistState.Success(playlist = result.getOrThrow())) 31 | } 32 | 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/feed/FeedViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.feed 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.egoriku.radiotok.common.ext.logD 6 | import com.egoriku.radiotok.domain.usecase.FeedUseCase 7 | import com.egoriku.radiotok.presentation.IMusicServiceConnection 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.asStateFlow 10 | import kotlinx.coroutines.launch 11 | 12 | class FeedViewModel( 13 | private val serviceConnection: IMusicServiceConnection, 14 | private val feedUseCase: FeedUseCase 15 | ) : ViewModel() { 16 | 17 | private val _feedState = MutableStateFlow(FeedState.Loading) 18 | 19 | val feedState = _feedState.asStateFlow() 20 | 21 | init { 22 | logD("FeedViewModel created") 23 | 24 | viewModelScope.launch { 25 | _feedState.emit(FeedState.Loading) 26 | 27 | val feed = feedUseCase.loadFeed() 28 | 29 | _feedState.emit(FeedState.Success(feed = feed)) 30 | } 31 | } 32 | 33 | fun playFromMediaId(mediaId: String) { 34 | logD("playFromMediaId: $mediaId") 35 | 36 | serviceConnection.transportControls.playFromMediaId(mediaId, null) 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/settings/ui/SettingItem.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.settings.ui 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | 11 | @Composable 12 | fun SettingItem( 13 | modifier: Modifier = Modifier, 14 | title: String, 15 | subtitle: String, 16 | onClick: () -> Unit 17 | ) { 18 | Box( 19 | modifier = modifier 20 | .fillMaxWidth() 21 | .clickable(onClick = onClick) 22 | .padding(vertical = 8.dp), 23 | ) { 24 | Column( 25 | modifier = modifier 26 | .fillMaxWidth() 27 | .padding(horizontal = 16.dp), 28 | verticalArrangement = Arrangement.spacedBy(4.dp) 29 | ) { 30 | Text( 31 | text = title, 32 | style = MaterialTheme.typography.body1 33 | ) 34 | Text( 35 | text = subtitle, 36 | style = MaterialTheme.typography.caption 37 | ) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/data/mapper/MediaMetadataToMediaSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.data.mapper 2 | 3 | import android.support.v4.media.MediaMetadataCompat 4 | import com.egoriku.radiotok.radioplayer.ext.isHls 5 | import com.egoriku.radiotok.radioplayer.ext.mediaUri 6 | import com.google.android.exoplayer2.MediaItem 7 | import com.google.android.exoplayer2.source.MediaSource 8 | import com.google.android.exoplayer2.source.ProgressiveMediaSource 9 | import com.google.android.exoplayer2.source.hls.HlsMediaSource 10 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSource 11 | 12 | fun List.toMediaSource( 13 | defaultHttpDataSourceFactory: DefaultHttpDataSource.Factory 14 | ) = map { metadataCompat -> 15 | metadataCompat.toMediaSource(defaultHttpDataSourceFactory) 16 | } 17 | 18 | private fun MediaMetadataCompat.toMediaSource( 19 | defaultHttpDataSourceFactory: DefaultHttpDataSource.Factory 20 | ): MediaSource { 21 | val mediaItem = MediaItem.fromUri(mediaUri) 22 | 23 | return when { 24 | isHls -> HlsMediaSource.Factory(defaultHttpDataSourceFactory) 25 | .createMediaSource(mediaItem) 26 | else -> ProgressiveMediaSource.Factory(defaultHttpDataSourceFactory) 27 | .createMediaSource(mediaItem) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/data/datasource/RadioDnsServer.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.data.datasource 2 | 3 | import com.egoriku.radiotok.common.ext.logD 4 | import com.egoriku.radiotok.domain.datasource.IRadioDnsServer 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import java.net.InetAddress 8 | 9 | private const val DNS_LOOKUP_SERVER = "all.api.radio-browser.info" 10 | private const val FALLBACK_SERVER = "de1.api.radio-browser.info" 11 | 12 | internal class RadioDnsServer : IRadioDnsServer { 13 | 14 | override suspend fun lookup() = withContext(Dispatchers.IO) { 15 | val listResult = mutableListOf() 16 | 17 | runCatching { 18 | InetAddress 19 | .getAllByName(DNS_LOOKUP_SERVER) 20 | .map { item -> 21 | val currentHostAddress = item.hostAddress 22 | val name = item.canonicalHostName 23 | 24 | if (name != DNS_LOOKUP_SERVER && name != currentHostAddress) { 25 | listResult.add(name) 26 | } 27 | } 28 | } 29 | 30 | if (listResult.isEmpty()) { 31 | listResult.add(FALLBACK_SERVER) 32 | } 33 | 34 | logD("DNS, Found servers: $listResult") 35 | 36 | listResult 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/extension/Bitmap.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.extension 2 | 3 | import android.graphics.* 4 | 5 | fun Bitmap.roundWithBorder( 6 | borderColor: Int = Color.parseColor("#FFEE894F"), 7 | backgroundColor: Int = Color.parseColor("#FF393939"), 8 | borderWidth: Float = 15f, 9 | scaleFactor: Int = 2 10 | ): Bitmap { 11 | val newWidth = width * scaleFactor 12 | val newHeight = height * scaleFactor 13 | 14 | val bitmap = Bitmap.createBitmap( 15 | newWidth, 16 | newHeight, 17 | Bitmap.Config.ARGB_8888 18 | ) 19 | 20 | val paint = Paint().apply { 21 | isAntiAlias = true 22 | color = backgroundColor 23 | } 24 | 25 | Canvas(bitmap).apply { 26 | drawOval(RectF(0f, 0f, newWidth.toFloat(), newHeight.toFloat()), paint) 27 | drawBitmap( 28 | this@roundWithBorder, 29 | width / scaleFactor / 2f, 30 | height / scaleFactor / 2f, 31 | paint 32 | ) 33 | 34 | drawCircle( 35 | width / 2f, 36 | width / 2f, 37 | width / 2f - borderWidth / 2, 38 | paint.apply { 39 | color = borderColor 40 | style = Paint.Style.STROKE 41 | strokeWidth = borderWidth 42 | } 43 | ) 44 | } 45 | recycle() 46 | 47 | return bitmap 48 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/repair/RadioStationRepair.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.repair 2 | 3 | import com.egoriku.radiotok.datasource.entity.RadioEntity 4 | 5 | object RadioStationRepair { 6 | 7 | private val data = listOf( 8 | Metadata( 9 | id = "732bae83-1a25-11e8-a334-52543be04c81", 10 | logoUrl = "https://dbs.radioline.fr/pictures/radio_0f07314cc317183b5441bab508760d9c/logo200.jpg" 11 | ), 12 | Metadata( 13 | id = "10f7b21c-74ed-4527-a59f-958f4d2a62c2", 14 | logoUrl = "https://www.radio1.hu/wp-content/themes/radio1/assets/dist/img/logo.png" 15 | ), 16 | Metadata( 17 | id = "6e433bac-f036-11e8-a471-52543be04c81", 18 | logoUrl = "https://cdn-radiotime-logos.tunein.com/s128283d.png" 19 | ), 20 | Metadata( 21 | id = "06a8e0f6-c453-11e9-8502-52543be04c81", 22 | logoUrl = "https://www.kissfm.de/_nuxt/icons/icon_512.be8y2280000.png" 23 | ) 24 | ) 25 | 26 | fun tryToFix(radioEntity: RadioEntity): RadioEntity { 27 | val metadata = data.find { metadata -> 28 | metadata.id == radioEntity.stationUuid 29 | } 30 | 31 | return when (metadata) { 32 | null -> radioEntity 33 | else -> radioEntity.copy(icon = metadata.logoUrl) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/repository/MediaMetadataRepository.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.repository 2 | 3 | import android.support.v4.media.MediaMetadataCompat 4 | import com.egoriku.radiotok.common.ext.logD 5 | import com.egoriku.radiotok.datasource.IEntitySourceFactory 6 | import com.egoriku.radiotok.db.RadioTokDb 7 | import com.egoriku.radiotok.radioplayer.ext.from 8 | 9 | internal class MediaMetadataRepository( 10 | private val radioTokDb: RadioTokDb, 11 | private val entitySourceFactory: IEntitySourceFactory, 12 | ) : IMediaMetadataRepository { 13 | 14 | private val entityMapper = RadioEntityToModelMapper() 15 | 16 | override suspend fun getRandomItem(): MediaMetadataCompat { 17 | val id = radioTokDb.stationDao().getRandomStationId() 18 | 19 | return loadByStationId(id) 20 | } 21 | 22 | override suspend fun getLikedItem(): MediaMetadataCompat { 23 | val id = radioTokDb.stationDao().getRandomLikedStationId() 24 | 25 | return loadByStationId(id) 26 | } 27 | 28 | override suspend fun loadByStationId(id: String) = runCatching { 29 | logD("loadByStationId: $id") 30 | val stationById = entitySourceFactory.loadByIds(listOf(id)).first() 31 | 32 | MediaMetadataCompat.Builder().from( 33 | itemModel = entityMapper.invoke(stationById) 34 | ).build() 35 | }.getOrNull() ?: getRandomItem() 36 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_changed_lately.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_smart_playlist.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_changed_lately.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_language.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/listener/RadioPlayerEventListener.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.listener 2 | 3 | import com.egoriku.radiotok.common.ext.logD 4 | import com.google.android.exoplayer2.ExoPlaybackException 5 | import com.google.android.exoplayer2.PlaybackException 6 | import com.google.android.exoplayer2.Player 7 | 8 | class RadioPlayerEventListener( 9 | private val onStopForeground: () -> Unit, 10 | private val onError: () -> Unit 11 | ) : Player.Listener { 12 | 13 | override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { 14 | if (playbackState == Player.STATE_READY && !playWhenReady) { 15 | onStopForeground() 16 | } 17 | } 18 | 19 | override fun onPlayerError(error: PlaybackException) { 20 | super.onPlayerError(error) 21 | 22 | onError() 23 | 24 | if (error is ExoPlaybackException) { 25 | when (error.type) { 26 | ExoPlaybackException.TYPE_SOURCE -> logD("onPlayerError, TYPE_SOURCE: " + error.sourceException.toString()) 27 | ExoPlaybackException.TYPE_RENDERER -> logD("onPlayerError, TYPE_RENDERER: " + error.rendererException.toString()) 28 | ExoPlaybackException.TYPE_UNEXPECTED -> logD("onPlayerError, TYPE_UNEXPECTED: " + error.unexpectedException.toString()) 29 | else -> logD("onPlayerError, ETC: " + error.sourceException.toString()) 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_random.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_collection.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_shuffle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_liked.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/notification/actions/DislikeActionProvider.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.notification.actions 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.support.v4.media.session.PlaybackStateCompat 6 | import com.egoriku.radiotok.radioplayer.R 7 | import com.egoriku.radiotok.radioplayer.constant.CustomAction.ACTION_DISLIKE 8 | import com.egoriku.radiotok.radioplayer.data.CurrentRadioQueueHolder 9 | import com.egoriku.radiotok.radioplayer.ext.id 10 | import com.google.android.exoplayer2.Player 11 | import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector 12 | 13 | class DislikeActionProvider( 14 | private val context: Context, 15 | private val currentRadioQueueHolder: CurrentRadioQueueHolder, 16 | private val onDislike: (String) -> Unit 17 | ) : MediaSessionConnector.CustomActionProvider { 18 | 19 | override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction { 20 | return PlaybackStateCompat.CustomAction.Builder( 21 | ACTION_DISLIKE, 22 | context.getString(R.string.custom_action_dislike), 23 | R.drawable.ic_not_interested 24 | ).build() 25 | } 26 | 27 | override fun onCustomAction(player: Player, action: String, extras: Bundle?) { 28 | val currentMediaMetadata = currentRadioQueueHolder.getMediaMetadataOrNull( 29 | position = player.currentMediaItemIndex 30 | ) ?: return 31 | 32 | onDislike(currentMediaMetadata.id) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/main/ui/actions/PlayPauseButton.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.main.ui.actions 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.res.painterResource 8 | import androidx.compose.ui.tooling.preview.Preview 9 | import com.egoriku.radiotok.R 10 | import com.egoriku.radiotok.foundation.button.CircleIconButtonLarge 11 | import com.egoriku.radiotok.presentation.ui.RadioTokTheme 12 | 13 | @Preview(showBackground = true) 14 | @Composable 15 | private fun PlayPauseActionPreview() { 16 | RadioTokTheme { 17 | Row { 18 | PlayPauseAction(isPlaying = true, enable = true) {} 19 | PlayPauseAction(isPlaying = false, enable = true) {} 20 | PlayPauseAction(isPlaying = true, enable = false) {} 21 | PlayPauseAction(isPlaying = false, enable = false) {} 22 | } 23 | } 24 | } 25 | 26 | @Composable 27 | fun PlayPauseAction( 28 | isPlaying: Boolean, 29 | enable: Boolean = true, 30 | onClick: () -> Unit 31 | ) { 32 | val icon = when { 33 | isPlaying -> painterResource(id = R.drawable.ic_pause) 34 | else -> painterResource(id = R.drawable.ic_play) 35 | } 36 | 37 | CircleIconButtonLarge( 38 | background = if (enable) MaterialTheme.colors.primary else Color.Gray.copy(alpha = 0.2f), 39 | icon = icon, 40 | onClick = onClick 41 | ) 42 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/notification/listener/RadioPlayerNotificationListener.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.notification.listener 2 | 3 | import android.app.Notification 4 | import android.content.Intent 5 | import androidx.core.content.ContextCompat 6 | import com.egoriku.radiotok.radioplayer.RadioService 7 | import com.egoriku.radiotok.radioplayer.constant.PlayerConstants.NOTIFICATION_ID 8 | import com.google.android.exoplayer2.ui.PlayerNotificationManager 9 | 10 | class RadioPlayerNotificationListener( 11 | private val radioService: RadioService 12 | ) : PlayerNotificationManager.NotificationListener { 13 | 14 | override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) { 15 | super.onNotificationCancelled(notificationId, dismissedByUser) 16 | radioService.apply { 17 | stopForeground(true) 18 | isForegroundService = false 19 | stopSelf() 20 | } 21 | } 22 | 23 | override fun onNotificationPosted( 24 | notificationId: Int, 25 | notification: Notification, 26 | ongoing: Boolean 27 | ) { 28 | super.onNotificationPosted(notificationId, notification, ongoing) 29 | radioService.apply { 30 | if (ongoing && !isForegroundService) { 31 | ContextCompat.startForegroundService( 32 | this, 33 | Intent(applicationContext, this::class.java) 34 | ) 35 | startForeground(NOTIFICATION_ID, notification) 36 | isForegroundService = true 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/playlist/components/CircleResourceImage.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.playlist.components 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.ColorFilter 15 | import androidx.compose.ui.graphics.painter.Painter 16 | import androidx.compose.ui.unit.Dp 17 | import androidx.compose.ui.unit.dp 18 | 19 | @Composable 20 | fun CircleResourceImage( 21 | modifier: Modifier = Modifier, 22 | painter: Painter, 23 | size: Dp, 24 | iconSize: Dp = size, 25 | tintColor: Color 26 | ) { 27 | Box( 28 | contentAlignment = Alignment.Center, 29 | modifier = modifier 30 | .size(size) 31 | .background( 32 | color = MaterialTheme.colors.secondary, 33 | shape = CircleShape 34 | ) 35 | .border(5.dp, MaterialTheme.colors.primary, CircleShape) 36 | 37 | ) { 38 | Image( 39 | painter = painter, 40 | contentDescription = null, 41 | modifier = Modifier.size(size = iconSize), 42 | colorFilter = ColorFilter.tint(tintColor) 43 | ) 44 | } 45 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/api/Api.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.api 2 | 3 | import com.egoriku.radiotok.datasource.entity.MetadataEntity 4 | import com.egoriku.radiotok.datasource.entity.RadioEntity 5 | import com.egoriku.radiotok.datasource.intertal.datasource.random.entity.ServerStats 6 | import retrofit2.http.GET 7 | import retrofit2.http.Path 8 | import retrofit2.http.Query 9 | 10 | interface Api { 11 | 12 | @GET("json/stations/byuuid") 13 | suspend fun stationsById(@Query("uuids") ids: String): List 14 | 15 | @GET("json/stations") 16 | suspend fun allStations(): List 17 | 18 | @GET("json/countries") 19 | suspend fun allCountries(): List 20 | 21 | @GET("json/stations/topclick") 22 | suspend fun topClicks(@Query("limit") limit: Int): List 23 | 24 | @GET("json/stations/bycountrycodeexact/{countryCode}") 25 | suspend fun byCountyCode(@Path("countryCode") countryCode: String): List 26 | 27 | @GET("json/stations/lastchange") 28 | suspend fun changedLately(@Query("limit") limit: Int): List 29 | 30 | @GET("json/stations/lastclick") 31 | suspend fun playingNow(@Query("limit") limit: Int): List 32 | 33 | @GET("json/stations/topvote") 34 | suspend fun topVote(@Query("limit") limit: Int): List 35 | 36 | @GET("json/languages") 37 | suspend fun allLanguages( 38 | @Query("hidebroken") isHideBroken: Boolean = true 39 | ): List 40 | 41 | @GET("json/tags") 42 | suspend fun allTags(): List 43 | 44 | @GET("json/stats") 45 | suspend fun stats(): ServerStats 46 | } -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_genres.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/data/CurrentRadioQueueHolder.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.data 2 | 3 | import android.support.v4.media.MediaMetadataCompat 4 | import com.egoriku.radiotok.common.ext.logD 5 | import com.egoriku.radiotok.radioplayer.data.mapper.toMediaSource 6 | import com.egoriku.radiotok.radioplayer.model.MediaPath 7 | import com.egoriku.radiotok.radioplayer.model.MediaPath.ShuffleAndPlayRoot 8 | import com.google.android.exoplayer2.source.ConcatenatingMediaSource 9 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSource 10 | 11 | class CurrentRadioQueueHolder( 12 | private val defaultHttpDataSourceFactory: DefaultHttpDataSource.Factory 13 | ) { 14 | var currentMediaSource = ConcatenatingMediaSource() 15 | private set 16 | 17 | var currentMediaPath: MediaPath = MediaPath.Root 18 | private set 19 | 20 | var radioStations = mutableListOf() 21 | private set 22 | 23 | fun isSingle() = currentMediaPath == MediaPath.Single 24 | 25 | fun isRandomRadio() = currentMediaPath == ShuffleAndPlayRoot.ShuffleRandom || 26 | currentMediaPath == ShuffleAndPlayRoot.ShuffleLiked 27 | 28 | fun getMediaMetadataOrNull(position: Int): MediaMetadataCompat? = 29 | radioStations.getOrNull(position) 30 | 31 | fun updateQueue(mediaPath: MediaPath, stations: List) { 32 | logD("updateQueue: size = ${stations.size}") 33 | 34 | currentMediaPath = mediaPath 35 | 36 | radioStations.clear() 37 | radioStations.addAll(stations) 38 | 39 | currentMediaSource.clear() 40 | currentMediaSource.addMediaSources(stations.toMediaSource(defaultHttpDataSourceFactory)) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/feed/ui/FeedRow.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.feed.ui 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.LazyListScope 5 | import androidx.compose.foundation.lazy.LazyRow 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.unit.dp 13 | import com.egoriku.radiotok.R 14 | import com.egoriku.radiotok.domain.model.Lane 15 | import com.egoriku.radiotok.extension.noRippleClickable 16 | 17 | @Composable 18 | fun FeedRow( 19 | lane: Lane, 20 | onMoreActionClick: () -> Unit = {}, 21 | feedItems: LazyListScope.() -> Unit 22 | ) { 23 | Row( 24 | modifier = Modifier 25 | .fillMaxWidth() 26 | .padding(start = 18.dp, top = 8.dp, end = 16.dp), 27 | horizontalArrangement = Arrangement.SpaceBetween, 28 | verticalAlignment = Alignment.CenterVertically 29 | ) { 30 | Text( 31 | text = stringResource(id = lane.titleRes), 32 | style = MaterialTheme.typography.h6 33 | ) 34 | if (lane.showMore) { 35 | Text( 36 | modifier = Modifier.noRippleClickable { onMoreActionClick() }, 37 | text = stringResource(id = R.string.show_all), 38 | style = MaterialTheme.typography.button 39 | ) 40 | } 41 | } 42 | 43 | LazyRow( 44 | horizontalArrangement = Arrangement.spacedBy(8.dp), 45 | contentPadding = PaddingValues(16.dp), 46 | content = feedItems 47 | ) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/feed/ui/MoreItem.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.feed.ui 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyListScope 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.material.Icon 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.ArrowForward 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.unit.dp 17 | 18 | fun LazyListScope.MoreItem( 19 | enabled: Boolean, 20 | onClick: () -> Unit 21 | ) { 22 | if (enabled) { 23 | item { 24 | LastItem(onClick = onClick) 25 | } 26 | } 27 | } 28 | 29 | @Composable 30 | fun LastItem(onClick: () -> Unit) { 31 | Box( 32 | modifier = Modifier 33 | .height(150.dp) 34 | .width(100.dp), 35 | contentAlignment = Alignment.Center 36 | ) { 37 | Box( 38 | modifier = Modifier 39 | .wrapContentSize() 40 | .clip(CircleShape) 41 | .background(MaterialTheme.colors.secondary) 42 | .clickable(onClick = onClick) 43 | .padding(16.dp), 44 | contentAlignment = Alignment.Center 45 | ) { 46 | Icon( 47 | imageVector = Icons.Default.ArrowForward, 48 | tint = MaterialTheme.colors.onSecondary, 49 | contentDescription = null 50 | ) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /mediaItemDsl/src/main/java/com/egoriku/mediaitemdsl/appearance/AppearanceBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.mediaitemdsl.appearance 2 | 3 | import android.os.Bundle 4 | import android.support.v4.media.MediaBrowserCompat.MediaItem 5 | import androidx.media.utils.MediaConstants.* 6 | 7 | @AppearanceMarker 8 | class AppearanceBuilder @PublishedApi internal constructor( 9 | private val flag: Int, 10 | private val extras: Bundle 11 | ) { 12 | var showAsList: Boolean = false 13 | set(value) { 14 | field = value 15 | 16 | if (value) { 17 | when (flag) { 18 | MediaItem.FLAG_BROWSABLE -> extras.putInt( 19 | DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, 20 | DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM 21 | ) 22 | MediaItem.FLAG_PLAYABLE -> extras.putInt( 23 | DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, 24 | DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM 25 | ) 26 | } 27 | } 28 | } 29 | 30 | var showAsGrid: Boolean = false 31 | set(value) { 32 | field = value 33 | 34 | if (value) { 35 | when (flag) { 36 | MediaItem.FLAG_BROWSABLE -> extras.putInt( 37 | DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, 38 | DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM 39 | ) 40 | MediaItem.FLAG_PLAYABLE -> extras.putInt( 41 | DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, 42 | DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM 43 | ) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/foundation/button/CircleIconButtonLarge.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.foundation.button 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.material.Icon 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.draw.clipToBounds 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.painter.Painter 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.dp 20 | import com.egoriku.radiotok.R 21 | import com.egoriku.radiotok.presentation.ui.RadioTokTheme 22 | 23 | @Preview(showBackground = true) 24 | @Composable 25 | private fun CircleIconButtonLargePreview() { 26 | RadioTokTheme { 27 | CircleIconButtonLarge( 28 | background = MaterialTheme.colors.primary, 29 | icon = painterResource(R.drawable.ic_play) 30 | ) {} 31 | } 32 | } 33 | 34 | @Composable 35 | fun CircleIconButtonLarge( 36 | background: Color, 37 | icon: Painter, 38 | onClick: () -> Unit 39 | ) { 40 | Box( 41 | contentAlignment = Alignment.Center, 42 | modifier = Modifier 43 | .size(72.dp) 44 | .clip(CircleShape) 45 | .background(background, CircleShape) 46 | .clickable(onClick = onClick) 47 | .clipToBounds() 48 | ) { 49 | Icon(painter = icon, contentDescription = null) 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/main/ui/RadioLogo.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.main.ui 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.layout.BoxWithConstraints 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.shadow 13 | import androidx.compose.ui.unit.dp 14 | import coil.transform.CircleCropTransformation 15 | 16 | @Composable 17 | fun RadioLogo( 18 | modifier: Modifier = Modifier, 19 | url: String, 20 | borderSize: Int 21 | ) { 22 | BoxWithConstraints( 23 | contentAlignment = Alignment.Center, 24 | modifier = modifier 25 | .size(300.dp) 26 | .shadow(30.dp, CircleShape) 27 | .background( 28 | color = MaterialTheme.colors.secondary, 29 | shape = CircleShape 30 | ) 31 | .then( 32 | when { 33 | borderSize > 0 -> Modifier.border(borderSize.dp, MaterialTheme.colors.primary, CircleShape) 34 | else -> Modifier 35 | } 36 | ) 37 | 38 | 39 | ) { 40 | RadioLogoImage( 41 | modifier = Modifier 42 | .size(width = maxWidth / 2f, height = maxHeight / 2) 43 | .background( 44 | color = MaterialTheme.colors.secondary, 45 | shape = CircleShape 46 | ), 47 | data = url, 48 | transformations = listOf(CircleCropTransformation()) 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | applicationId "com.egoriku.radiotok" 11 | minSdk 21 12 | targetSdk 31 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | buildFeatures { 24 | compose true 25 | } 26 | 27 | composeOptions { 28 | kotlinCompilerExtensionVersion libs.versions.compose.get() 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation projects.common 34 | implementation projects.datasource 35 | implementation projects.db 36 | implementation projects.mediaItemDsl 37 | implementation projects.radioPlayer 38 | 39 | implementation libs.bundles.compose 40 | implementation libs.compose.activity 41 | 42 | implementation libs.bundles.retrofit 43 | 44 | implementation libs.accompanist.insets 45 | implementation libs.accompanist.navigation.animation 46 | implementation libs.accompanist.placeholder 47 | implementation libs.appcompat 48 | implementation libs.coil 49 | implementation libs.constraintlayout.compose 50 | implementation libs.core 51 | implementation libs.gson 52 | implementation libs.koin.android 53 | implementation libs.koin.compose 54 | implementation libs.lifecycle.runtime 55 | implementation libs.media 56 | implementation libs.material 57 | implementation libs.okhttp 58 | implementation libs.storage 59 | implementation libs.toolbar.compose 60 | implementation libs.voyager.androidx 61 | implementation libs.voyager.navigator 62 | 63 | debugImplementation libs.compose.tooling 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/main/ui/RadioLogoImage.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.main.ui 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.material.Icon 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.layout.ContentScale 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.res.stringResource 12 | import coil.compose.SubcomposeAsyncImage 13 | import coil.request.ImageRequest 14 | import coil.size.Size 15 | import coil.transform.Transformation 16 | import com.egoriku.radiotok.R 17 | 18 | @Composable 19 | fun RadioLogoImage( 20 | modifier: Modifier = Modifier, 21 | data: Any, 22 | crossfade: Boolean = false, 23 | contentScale: ContentScale = ContentScale.Fit, 24 | contentDescription: String = stringResource(id = R.string.cc_radio_logo_success), 25 | transformations: List = emptyList() 26 | ) { 27 | SubcomposeAsyncImage( 28 | model = ImageRequest.Builder(LocalContext.current) 29 | .size(size = Size.ORIGINAL) 30 | .data(data = data) 31 | .crossfade(crossfade) 32 | .transformations(transformations) 33 | .build(), 34 | contentDescription = contentDescription, 35 | modifier = modifier, 36 | contentScale = contentScale, 37 | error = { 38 | Icon( 39 | modifier = Modifier.fillMaxSize(), 40 | painter = painterResource(id = R.drawable.ic_radio), 41 | tint = MaterialTheme.colors.onPrimary, 42 | contentDescription = stringResource(id = R.string.cc_radio_logo_error) 43 | ) 44 | } 45 | ) 46 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | app/google-services.json 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | .idea 41 | out 42 | /.idea/ 43 | .idea/ 44 | *.iml 45 | .idea/workspace.xml 46 | .idea/tasks.xml 47 | .idea/gradle.xml 48 | .idea/assetWizardSettings.xml 49 | .idea/dictionaries 50 | .idea/libraries 51 | # Android Studio 3 in .gitignore file. 52 | .idea/caches 53 | .idea/modules.xml 54 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 55 | .idea/navEditor.xml 56 | 57 | # Keystore files 58 | # Uncomment the following lines if you do not want to check your keystore files in. 59 | #*.jks 60 | #*.keystore 61 | 62 | # External native build folder generated in Android Studio 2.2 and later 63 | .externalNativeBuild 64 | .cxx/ 65 | 66 | # Google Services (e.g. APIs or Firebase) 67 | # google-services.json 68 | 69 | # Freeline 70 | freeline.py 71 | freeline/ 72 | freeline_project_description.json 73 | 74 | # fastlane 75 | fastlane/report.xml 76 | fastlane/Preview.html 77 | fastlane/screenshots 78 | fastlane/test_output 79 | fastlane/readme.md 80 | 81 | # Version control 82 | vcs.xml 83 | 84 | # lint 85 | lint/intermediates/ 86 | lint/generated/ 87 | lint/outputs/ 88 | lint/tmp/ 89 | # lint/reports/ 90 | output-metadata.json -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/feed/ui/PlaylistWithIcon.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.feed.ui 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material.Card 10 | import androidx.compose.material.Icon 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.painterResource 17 | import androidx.compose.ui.unit.dp 18 | import com.egoriku.radiotok.domain.model.section.FeedType 19 | import com.egoriku.radiotok.foundation.HSpacer 20 | 21 | @Composable 22 | fun PlaylistWithIcon( 23 | modifier: Modifier = Modifier, 24 | collection: FeedType.Playlist, 25 | onClick: (String) -> Unit 26 | ) { 27 | Card( 28 | shape = RoundedCornerShape(20.dp), 29 | backgroundColor = MaterialTheme.colors.secondary, 30 | modifier = modifier.size(150.dp) 31 | ) { 32 | Column( 33 | modifier = Modifier 34 | .fillMaxSize() 35 | .clickable { onClick(collection.id) }, 36 | verticalArrangement = Arrangement.Center, 37 | horizontalAlignment = Alignment.CenterHorizontally 38 | ) { 39 | Icon( 40 | painter = painterResource(id = collection.icon), 41 | contentDescription = null 42 | ) 43 | HSpacer(16.dp) 44 | Text(text = collection.name, style = MaterialTheme.typography.body1) 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/main/ui/actions/LikeAction.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.main.ui.actions 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.material.Icon 5 | import androidx.compose.material.IconButton 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import com.egoriku.radiotok.R 14 | import com.egoriku.radiotok.presentation.ui.RadioTokTheme 15 | 16 | @Preview(showBackground = true) 17 | @Composable 18 | private fun LikeActionPreview() { 19 | RadioTokTheme { 20 | Row { 21 | LikeAction( 22 | tint = MaterialTheme.colors.secondary, 23 | isLiked = true 24 | ) {} 25 | LikeAction( 26 | tint = MaterialTheme.colors.secondary, 27 | isLiked = false 28 | ) {} 29 | } 30 | } 31 | } 32 | 33 | @Composable 34 | fun LikeAction( 35 | modifier: Modifier = Modifier, 36 | tint: Color = MaterialTheme.colors.onPrimary, 37 | isLiked: Boolean, 38 | onClick: () -> Unit, 39 | ) { 40 | IconButton( 41 | onClick = onClick, 42 | modifier = modifier 43 | ) { 44 | when { 45 | isLiked -> Icon( 46 | painter = painterResource(R.drawable.ic_favorite), 47 | tint = tint, 48 | contentDescription = stringResource(id = R.string.cc_remove_favorite) 49 | ) 50 | else -> Icon( 51 | painter = painterResource(R.drawable.ic_favorite_border), 52 | tint = tint, 53 | contentDescription = stringResource(id = R.string.cc_add_favorite) 54 | ) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_top_clicks.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_top_clicks.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /db/src/main/kotlin/com/egoriku/radiotok/db/dao/StationDao.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.egoriku.radiotok.db.entity.StationDbEntity 8 | 9 | @Dao 10 | interface StationDao { 11 | 12 | @Query("SELECT stationUuid FROM stationdbentity WHERE isExcluded = 0 ORDER BY RANDOM() LIMIT 1") 13 | suspend fun getRandomStationId(): String 14 | 15 | @Query("SELECT stationUuid FROM stationdbentity WHERE isExcluded = 0 AND isLiked = 1 ORDER BY RANDOM() LIMIT 1") 16 | suspend fun getRandomLikedStationId(): String 17 | 18 | @Query("SELECT stationUuid FROM stationdbentity WHERE isExcluded = 0 AND isLiked = 1") 19 | suspend fun getLikedStationsIds(): List 20 | 21 | @Query("SELECT stationUuid FROM stationdbentity WHERE isExcluded = 1") 22 | suspend fun getDislikedStationsIds(): List 23 | 24 | @Query("SELECT COUNT(*) FROM stationdbentity") 25 | suspend fun getStationsCount(): Int 26 | 27 | @Query("SELECT COUNT(*) FROM stationdbentity WHERE isLiked = 1") 28 | suspend fun likedStationsCount(): Int 29 | 30 | @Query("SELECT isLiked FROM stationdbentity WHERE stationUuid = :stationId") 31 | suspend fun isStationLiked(stationId: String): Boolean 32 | 33 | @Query("UPDATE stationdbentity SET isLiked = NOT isLiked WHERE stationUuid = :stationId") 34 | suspend fun toggleLikedState(stationId: String) 35 | 36 | @Query("UPDATE stationdbentity SET isExcluded = NOT isExcluded WHERE stationUuid = :stationId") 37 | suspend fun toggleExcludedState(stationId: String) 38 | 39 | @Query("UPDATE stationdbentity SET isLiked = 1 WHERE stationUuid IN (:liked) ") 40 | fun updateLiked(liked: List) 41 | 42 | @Query("UPDATE stationdbentity SET isExcluded = 1 WHERE stationUuid IN (:disliked) ") 43 | fun updateDisliked(disliked: List) 44 | 45 | @Insert(onConflict = OnConflictStrategy.REPLACE) 46 | suspend fun insertAll(stations: List) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/domain/model/Feed.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.domain.model 2 | 3 | import com.egoriku.radiotok.R 4 | import com.egoriku.radiotok.domain.model.section.FeedType 5 | 6 | data class Feed( 7 | val shuffleAndPlay: Lane.ShuffleAndPlay, 8 | val forYou: Lane.ForYou, 9 | val smartPlaylists: Lane.SmartPlaylist, 10 | val byTags: Lane.ByTag, 11 | val byCountry: Lane.ByCountry, 12 | val byLanguage: Lane.ByLanguage 13 | ) 14 | 15 | sealed class Lane { 16 | abstract val titleRes: Int 17 | abstract val showMore: Boolean 18 | abstract val items: List 19 | 20 | data class ShuffleAndPlay( 21 | override val titleRes: Int = R.string.media_item_path_shuffle_and_play, 22 | override val showMore: Boolean = false, 23 | override val items: List 24 | ) : Lane() 25 | 26 | data class ForYou( 27 | override val titleRes: Int = R.string.media_item_path_personal_playlists, 28 | override val showMore: Boolean = false, 29 | override val items: List 30 | ) : Lane() 31 | 32 | data class SmartPlaylist( 33 | override val titleRes: Int = R.string.media_item_path_smart_playlists, 34 | override val showMore: Boolean = false, 35 | override val items: List 36 | ) : Lane() 37 | 38 | data class ByTag( 39 | override val titleRes: Int = R.string.media_item_path_by_tags, 40 | override val showMore: Boolean = true, 41 | override val items: List 42 | ) : Lane() 43 | 44 | data class ByCountry( 45 | override val titleRes: Int = R.string.media_item_path_by_country, 46 | override val showMore: Boolean = true, 47 | override val items: List 48 | ): Lane() 49 | 50 | data class ByLanguage( 51 | override val titleRes: Int = R.string.media_item_path_by_language, 52 | override val showMore: Boolean = true, 53 | override val items: List 54 | ): Lane() 55 | } -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_radio_waves.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_personal.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/koin/RadioPlayerModule.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.koin 2 | 3 | import com.egoriku.radiotok.radioplayer.data.CurrentRadioQueueHolder 4 | import com.egoriku.radiotok.radioplayer.data.RadioStateMediator 5 | import com.egoriku.radiotok.radioplayer.repository.IMediaItemRepository 6 | import com.egoriku.radiotok.radioplayer.repository.IMediaMetadataRepository 7 | import com.egoriku.radiotok.radioplayer.repository.MediaItemRepository 8 | import com.egoriku.radiotok.radioplayer.repository.MediaMetadataRepository 9 | import com.google.android.exoplayer2.C 10 | import com.google.android.exoplayer2.ExoPlayer 11 | import com.google.android.exoplayer2.audio.AudioAttributes 12 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSource 13 | import org.koin.android.ext.koin.androidContext 14 | import org.koin.dsl.module 15 | 16 | val exoPlayerModule = module { 17 | single { 18 | AudioAttributes.Builder() 19 | .setContentType(C.CONTENT_TYPE_MUSIC) 20 | .setUsage(C.USAGE_MEDIA) 21 | .build() 22 | } 23 | 24 | single { 25 | DefaultHttpDataSource.Factory() 26 | } 27 | 28 | single { 29 | ExoPlayer.Builder(androidContext()) 30 | .setAudioAttributes(get(), true) 31 | .setHandleAudioBecomingNoisy(true) 32 | .setWakeMode(C.WAKE_MODE_NETWORK) 33 | .build() 34 | } 35 | } 36 | 37 | val radioPlayerModule = module { 38 | factory { 39 | MediaItemRepository( 40 | bitmapProvider = get(), 41 | stringResource = get(), 42 | radioTokDb = get(), 43 | entitySourceFactory = get() 44 | ) 45 | } 46 | factory { 47 | MediaMetadataRepository( 48 | radioTokDb = get(), 49 | entitySourceFactory = get() 50 | ) 51 | } 52 | 53 | single { 54 | RadioStateMediator(radioTokDb = get()) 55 | } 56 | 57 | single { 58 | CurrentRadioQueueHolder( 59 | defaultHttpDataSourceFactory = get() 60 | ) 61 | } 62 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/datasource/playlist/LocalStationsDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal.datasource.playlist 2 | 3 | import android.content.Context 4 | import com.egoriku.radiotok.common.ext.logD 5 | import com.egoriku.radiotok.common.ext.telephonyManager 6 | import com.egoriku.radiotok.datasource.datasource.playlist.ILocalStationsDataSource 7 | import com.egoriku.radiotok.datasource.entity.RadioEntity 8 | import com.egoriku.radiotok.datasource.intertal.api.Api 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.withContext 11 | 12 | internal class LocalStationsDataSource( 13 | context: Context, 14 | private val api: Api 15 | ) : ILocalStationsDataSource { 16 | 17 | private val telephonyManager = context.telephonyManager 18 | 19 | private var result: List? = null 20 | 21 | override suspend fun load(): List { 22 | val countryCode = getCountryCode() 23 | 24 | return when { 25 | countryCode.isNullOrEmpty() -> emptyList() 26 | else -> runCatching { 27 | withContext(Dispatchers.IO) { 28 | result ?: api.byCountyCode(countryCode = countryCode).also { 29 | result = it 30 | } 31 | } 32 | }.getOrDefault(emptyList()) 33 | } 34 | } 35 | 36 | private fun getCountryCode(): String? { 37 | var countryCode = telephonyManager.networkCountryIso 38 | 39 | if (countryCode == null) { 40 | countryCode = telephonyManager.simCountryIso 41 | } 42 | 43 | return when { 44 | countryCode != null -> { 45 | if (countryCode.length == 2) { 46 | logD("countrycode: $countryCode") 47 | countryCode 48 | } else { 49 | logD("countrycode length != 2") 50 | null 51 | } 52 | } 53 | else -> { 54 | logD("device countrycode and sim countrycode are null") 55 | null 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/util/BackupManager.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.util 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import com.anggrayudi.storage.extension.fromSingleUri 6 | import com.anggrayudi.storage.file.openInputStream 7 | import com.anggrayudi.storage.file.openOutputStream 8 | import com.egoriku.radiotok.db.RadioTokDb 9 | import com.google.gson.Gson 10 | import com.google.gson.annotations.SerializedName 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.withContext 13 | import java.text.SimpleDateFormat 14 | import java.util.* 15 | 16 | class BackupManager( 17 | private val context: Context, 18 | private val db: RadioTokDb 19 | ) { 20 | private val gson = Gson() 21 | 22 | val backupFileName: String 23 | get() = SimpleDateFormat( 24 | "'radiotok_db_'yyyyMMddHHmm'.bin'", Locale.getDefault() 25 | ).format(Date()) 26 | 27 | suspend fun backup(outputFile: Uri) { 28 | withContext(Dispatchers.IO) { 29 | context.fromSingleUri(outputFile) 30 | ?.openOutputStream(context) 31 | ?.use { stream -> 32 | val backup = Backup( 33 | likedIds = db.stationDao().getLikedStationsIds(), 34 | dislikedIds = db.stationDao().getDislikedStationsIds() 35 | ) 36 | 37 | stream.write(gson.toJson(backup).toByteArray()) 38 | } 39 | } 40 | } 41 | 42 | suspend fun restore(inputFile: Uri) = withContext(Dispatchers.IO) { 43 | context.fromSingleUri(inputFile) 44 | ?.openInputStream(context) 45 | ?.use { 46 | val backup = gson.fromJson(it.bufferedReader(), Backup::class.java) 47 | 48 | db.stationDao().updateLiked(backup.likedIds) 49 | db.stationDao().updateDisliked(backup.dislikedIds) 50 | } 51 | ?: error("") 52 | } 53 | 54 | data class Backup( 55 | @SerializedName("likedIds") 56 | val likedIds: List, 57 | 58 | @SerializedName("dislikedIds") 59 | val dislikedIds: List 60 | ) 61 | } -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/intertal/EntitySourceFactory.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.intertal 2 | 3 | import com.egoriku.radiotok.datasource.IEntitySourceFactory 4 | import com.egoriku.radiotok.datasource.datasource.IRadioInfoDataSource 5 | import com.egoriku.radiotok.datasource.datasource.metadata.ICountriesDataSource 6 | import com.egoriku.radiotok.datasource.datasource.metadata.ILanguagesDataSource 7 | import com.egoriku.radiotok.datasource.datasource.metadata.ITagsDataSource 8 | import com.egoriku.radiotok.datasource.datasource.playlist.* 9 | import com.egoriku.radiotok.datasource.entity.params.MetadataParams 10 | import com.egoriku.radiotok.datasource.entity.params.MetadataParams.* 11 | import com.egoriku.radiotok.datasource.entity.params.PlaylistParams 12 | import com.egoriku.radiotok.datasource.entity.params.PlaylistParams.* 13 | 14 | internal class EntitySourceFactory( 15 | private val changedLatelyDataSource: IChangedLatelyDataSource, 16 | private val localStationsDataSource: ILocalStationsDataSource, 17 | private val playingNowDataSource: IPlayingNowDataSource, 18 | private val topClicksDataSource: ITopClicksDataSource, 19 | private val topVoteDataSource: ITopVoteDataSource, 20 | private val tagsDataSource: ITagsDataSource, 21 | private val languagesDataSource: ILanguagesDataSource, 22 | private val countriesDataSource: ICountriesDataSource, 23 | private val radioInfoDataSource: IRadioInfoDataSource, 24 | ) : IEntitySourceFactory { 25 | 26 | override suspend fun loadBy(params: PlaylistParams) = when (params) { 27 | ChangedLately -> changedLatelyDataSource.load() 28 | LocalStations -> localStationsDataSource.load() 29 | PlayingNow -> playingNowDataSource.load() 30 | TopClicks -> topClicksDataSource.load() 31 | TopVote -> topVoteDataSource.load() 32 | } 33 | 34 | override suspend fun loadBy(params: MetadataParams) = when (params) { 35 | Tags -> tagsDataSource.load() 36 | Languages -> languagesDataSource.load() 37 | Countries -> countriesDataSource.load() 38 | } 39 | 40 | override suspend fun loadByIds(ids: List) = radioInfoDataSource.loadByIds(ids) 41 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_top_vote.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_top_vote.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /mediaItemDsl/src/main/java/com/egoriku/mediaitemdsl/mediaitem/MediaItemBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.mediaitemdsl.mediaitem 2 | 3 | import android.graphics.Bitmap 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.support.v4.media.MediaBrowserCompat 7 | import android.support.v4.media.MediaDescriptionCompat 8 | import androidx.annotation.RestrictTo 9 | import androidx.core.os.bundleOf 10 | import com.egoriku.mediaitemdsl.appearance.AppearanceBuilder 11 | 12 | @MediaItemMarker 13 | class MediaItemBuilder @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @PublishedApi internal constructor( 14 | private val flag: Int, 15 | @PublishedApi internal val mediaDescriptionBuilder: MediaDescriptionCompat.Builder 16 | ) { 17 | 18 | @PublishedApi 19 | internal constructor(flag: Int) : this( 20 | flag = flag, 21 | mediaDescriptionBuilder = MediaDescriptionCompat.Builder() 22 | ) 23 | 24 | private val appearance: AppearanceBuilder 25 | get() = AppearanceBuilder(flag = flag, extras = extras) 26 | 27 | fun appearance(body: @MediaItemMarker AppearanceBuilder.() -> Unit) { 28 | appearance.body() 29 | } 30 | 31 | var extras: Bundle = bundleOf() 32 | set(value) { 33 | field.putAll(value) 34 | } 35 | 36 | var iconBitmap: Bitmap? = null 37 | set(value) { 38 | field = value 39 | mediaDescriptionBuilder.setIconBitmap(value) 40 | } 41 | 42 | var iconUri: Uri? = null 43 | set(value) { 44 | field = value 45 | mediaDescriptionBuilder.setIconUri(value) 46 | } 47 | 48 | var id: String? = null 49 | set(value) { 50 | field = value 51 | mediaDescriptionBuilder.setMediaId(value) 52 | } 53 | 54 | var title: String? = null 55 | set(value) { 56 | field = value 57 | mediaDescriptionBuilder.setTitle(value) 58 | } 59 | 60 | var subTitle: String? = null 61 | set(value) { 62 | field = value 63 | mediaDescriptionBuilder.setSubtitle(value) 64 | } 65 | 66 | fun build() = MediaBrowserCompat.MediaItem( 67 | mediaDescriptionBuilder 68 | .setExtras(extras) 69 | .build(), 70 | flag 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/main/ui/PlayerControls.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.main.ui 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | import com.egoriku.radiotok.presentation.screen.main.PlayerControlsActions 13 | import com.egoriku.radiotok.presentation.screen.main.ui.actions.* 14 | 15 | @Composable 16 | fun PlayerControls( 17 | modifier: Modifier = Modifier, 18 | isPlaying: Boolean, 19 | isLiked: Boolean, 20 | isError: Boolean, 21 | playerControlsActions: PlayerControlsActions 22 | ) { 23 | Row( 24 | modifier = modifier.fillMaxWidth(), 25 | verticalAlignment = Alignment.CenterVertically 26 | ) { 27 | NotInterestedAction( 28 | onClick = playerControlsActions.dislikeRadioStationEvent, 29 | modifier = Modifier.padding(start = 16.dp) 30 | ) 31 | 32 | Row( 33 | modifier = Modifier.weight(1f), 34 | verticalAlignment = Alignment.CenterVertically, 35 | horizontalArrangement = Arrangement.Center 36 | ) { 37 | TuneAction( 38 | onClick = playerControlsActions.tuneRadiosEvent, 39 | modifier = Modifier.padding(end = 16.dp) 40 | ) 41 | 42 | PlayPauseAction( 43 | enable = !isError, 44 | isPlaying = isPlaying, 45 | onClick = playerControlsActions.playPauseEvent 46 | ) 47 | 48 | SkipNextAction( 49 | modifier = Modifier.padding(start = 16.dp), 50 | onClick = playerControlsActions.nextRadioEvent 51 | ) 52 | } 53 | 54 | LikeAction( 55 | modifier = Modifier.padding(end = 16.dp), 56 | tint = MaterialTheme.colors.secondary, 57 | onClick = { playerControlsActions.toggleFavoriteEvent() }, 58 | isLiked = isLiked 59 | ) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/foundation/NetworkImage.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.foundation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.layout.ContentScale 6 | import androidx.compose.ui.platform.LocalContext 7 | import androidx.compose.ui.res.stringResource 8 | import coil.compose.AsyncImage 9 | import coil.compose.AsyncImagePainter 10 | import coil.compose.SubcomposeAsyncImage 11 | import coil.compose.SubcomposeAsyncImageScope 12 | import coil.request.ImageRequest 13 | import coil.transform.Transformation 14 | import com.egoriku.radiotok.R 15 | 16 | @Composable 17 | fun NetworkImage( 18 | modifier: Modifier = Modifier, 19 | data: Any, 20 | crossfade: Boolean = false, 21 | contentScale: ContentScale = ContentScale.Fit, 22 | contentDescription: String = stringResource(id = R.string.cc_radio_logo_success), 23 | transformations: List = emptyList(), 24 | loading: @Composable SubcomposeAsyncImageScope.(AsyncImagePainter.State.Loading) -> Unit = { }, 25 | error: @Composable SubcomposeAsyncImageScope.(AsyncImagePainter.State.Error) -> Unit = { }, 26 | ) { 27 | SubcomposeAsyncImage( 28 | model = ImageRequest.Builder(LocalContext.current) 29 | .data(data = data) 30 | .crossfade(crossfade) 31 | .transformations(transformations) 32 | .build(), 33 | contentDescription = contentDescription, 34 | modifier = modifier, 35 | contentScale = contentScale, 36 | loading = loading, 37 | error = error 38 | ) 39 | } 40 | 41 | @Composable 42 | fun NetworkImage( 43 | modifier: Modifier = Modifier, 44 | data: Any, 45 | crossfade: Boolean = false, 46 | contentScale: ContentScale = ContentScale.Fit, 47 | contentDescription: String = stringResource(id = R.string.cc_radio_logo_success), 48 | transformations: List = emptyList() 49 | ) { 50 | AsyncImage( 51 | model = ImageRequest.Builder(LocalContext.current) 52 | .data(data = data) 53 | .crossfade(crossfade) 54 | .transformations(transformations) 55 | .build(), 56 | contentDescription = contentDescription, 57 | modifier = modifier, 58 | contentScale = contentScale 59 | ) 60 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/domain/common/internal/StringResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.domain.common.internal 2 | 3 | import android.content.Context 4 | import com.egoriku.radiotok.R 5 | import com.egoriku.radiotok.common.provider.IStringResourceProvider 6 | import kotlin.properties.ReadOnlyProperty 7 | import kotlin.reflect.KProperty 8 | 9 | internal class StringResourceProvider( 10 | private val context: Context 11 | ) : IStringResourceProvider { 12 | 13 | class ResourceDelegate( 14 | private val key: Int 15 | ) : ReadOnlyProperty { 16 | 17 | override fun getValue( 18 | thisRef: StringResourceProvider, 19 | property: KProperty<*> 20 | ): String = thisRef.context.getString(key) 21 | } 22 | 23 | override val shuffleAndPlay by ResourceDelegate(R.string.media_item_path_shuffle_and_play) 24 | override val personalPlaylists by ResourceDelegate(R.string.media_item_path_personal_playlists) 25 | override val smartPlaylists by ResourceDelegate(R.string.media_item_path_smart_playlists) 26 | override val byTags by ResourceDelegate(R.string.media_item_path_by_tags) 27 | override val byCountry by ResourceDelegate(R.string.media_item_path_by_country) 28 | override val byLanguage by ResourceDelegate(R.string.media_item_path_by_language) 29 | override val catalog by ResourceDelegate(R.string.media_item_path_catalog) 30 | 31 | override val likedRadio by ResourceDelegate(R.string.media_item_path_liked_radio) 32 | override val randomRadio by ResourceDelegate(R.string.media_item_path_random_radio) 33 | 34 | override val liked: String by ResourceDelegate(R.string.media_item_path_liked) 35 | override val recentlyPlayed: String by ResourceDelegate(R.string.media_item_path_recently_played) 36 | override val disliked: String by ResourceDelegate(R.string.media_item_path_disliked) 37 | 38 | override val localStations: String by ResourceDelegate(R.string.media_item_path_local_stations) 39 | override val topClicks: String by ResourceDelegate(R.string.media_item_path_top_clicks) 40 | override val topVote: String by ResourceDelegate(R.string.media_item_path_top_vote) 41 | override val changedLately: String by ResourceDelegate(R.string.media_item_path_changed_lately) 42 | override val playing: String by ResourceDelegate(R.string.media_item_path_playing) 43 | 44 | override fun getStationsCount(count: Int) = 45 | context.resources.getQuantityString(R.plurals.radio_station_count, count, count) 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/RadioViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation 2 | 3 | import android.support.v4.media.MediaBrowserCompat 4 | import android.support.v4.media.session.MediaControllerCompat 5 | import androidx.lifecycle.ViewModel 6 | import com.egoriku.radiotok.common.ext.logD 7 | import com.egoriku.radiotok.radioplayer.constant.MediaBrowserConstant.SUB_PATH_SHUFFLE_RANDOM 8 | import com.egoriku.radiotok.radioplayer.ext.sendDislikeAction 9 | import com.egoriku.radiotok.radioplayer.ext.sendLikeAction 10 | 11 | class RadioViewModel( 12 | private val serviceConnection: IMusicServiceConnection 13 | ) : ViewModel(), IMusicServiceConnection by serviceConnection { 14 | 15 | private val _transportControls: MediaControllerCompat.TransportControls 16 | get() = serviceConnection.transportControls 17 | 18 | private val subscriptionCallback = object : MediaBrowserCompat.SubscriptionCallback() { 19 | override fun onChildrenLoaded( 20 | parentId: String, 21 | children: List 22 | ) { 23 | logD("parentId=$parentId, children size = ${children.size}") 24 | val itemsList = children.map { child -> 25 | val subtitle = child.description.subtitle ?: "" 26 | /* MediaItemData( 27 | child.mediaId!!, 28 | child.description.title.toString(), 29 | subtitle.toString(), 30 | child.description.iconUri!!, 31 | child.isBrowsable, 32 | getResourceForMediaId(child.mediaId!!) 33 | )*/ 34 | } 35 | } 36 | } 37 | 38 | init { 39 | serviceConnection.subscribe(SUB_PATH_SHUFFLE_RANDOM, subscriptionCallback) 40 | } 41 | 42 | fun nextRadioStation() = _transportControls.skipToNext() 43 | 44 | fun dislikeRadioStation() = _transportControls.sendDislikeAction() 45 | 46 | fun likeRadioStation() = _transportControls.sendLikeAction() 47 | 48 | fun togglePlayPause() { 49 | val playbackState = playbackState.value 50 | 51 | if (playbackState.isPrepared) { 52 | when { 53 | playbackState.isPlaying -> serviceConnection.transportControls.pause() 54 | playbackState.isPlayEnabled -> serviceConnection.transportControls.play() 55 | } 56 | } else { 57 | //serviceConnection.transportControls.playFromMediaId(mediaItem.mediaId, null) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /common/src/main/res/drawable/ic_history.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/queue/RadioQueueNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.queue 2 | 3 | import android.support.v4.media.MediaDescriptionCompat 4 | import android.support.v4.media.session.MediaSessionCompat 5 | import android.support.v4.media.session.PlaybackStateCompat 6 | import com.egoriku.radiotok.common.ext.logD 7 | import com.egoriku.radiotok.radioplayer.data.CurrentRadioQueueHolder 8 | import com.google.android.exoplayer2.Player 9 | import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator 10 | 11 | class RadioQueueNavigator( 12 | private val currentRadioQueueHolder: CurrentRadioQueueHolder, 13 | private val onNextRandom: () -> Unit, 14 | mediaSession: MediaSessionCompat 15 | ) : TimelineQueueNavigator(mediaSession) { 16 | 17 | override fun getMediaDescription( 18 | player: Player, 19 | windowIndex: Int 20 | ): MediaDescriptionCompat { 21 | return when (val metadata = currentRadioQueueHolder.getMediaMetadataOrNull(windowIndex)) { 22 | null -> getEmptyMediaDescription() 23 | else -> metadata.description 24 | } 25 | } 26 | 27 | override fun getSupportedQueueNavigatorActions(player: Player): Long { 28 | var actions = 0L 29 | 30 | if (currentRadioQueueHolder.isRandomRadio()) { 31 | logD("getSupportedQueueNavigatorActions: Random") 32 | actions = actions or 33 | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM or 34 | PlaybackStateCompat.ACTION_SKIP_TO_NEXT 35 | } else if (currentRadioQueueHolder.isSingle()) { 36 | logD("getSupportedQueueNavigatorActions: Single") 37 | } else { 38 | logD("getSupportedQueueNavigatorActions: Else") 39 | 40 | actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM 41 | 42 | if (player.hasNextMediaItem()) { 43 | actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_NEXT 44 | } 45 | } 46 | 47 | return actions 48 | } 49 | 50 | override fun onSkipToNext(player: Player) { 51 | when { 52 | currentRadioQueueHolder.isRandomRadio() -> onNextRandom() 53 | else -> { 54 | if (player.hasNextMediaItem()) { 55 | player.seekToNextMediaItem() 56 | } else { 57 | player.stop() 58 | } 59 | } 60 | } 61 | } 62 | 63 | private fun getEmptyMediaDescription() = MediaDescriptionCompat.Builder().build() 64 | } -------------------------------------------------------------------------------- /common/src/main/res/drawable-anydpi/ic_auto_history.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /datasource/src/main/kotlin/com/egoriku/radiotok/datasource/koin/DataSourceModule.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.datasource.koin 2 | 3 | import com.egoriku.radiotok.datasource.IEntitySourceFactory 4 | import com.egoriku.radiotok.datasource.datasource.IAllStationsDataSource 5 | import com.egoriku.radiotok.datasource.datasource.IRadioInfoDataSource 6 | import com.egoriku.radiotok.datasource.datasource.metadata.ICountriesDataSource 7 | import com.egoriku.radiotok.datasource.datasource.metadata.ILanguagesDataSource 8 | import com.egoriku.radiotok.datasource.datasource.metadata.ITagsDataSource 9 | import com.egoriku.radiotok.datasource.datasource.playlist.* 10 | import com.egoriku.radiotok.datasource.intertal.EntitySourceFactory 11 | import com.egoriku.radiotok.datasource.intertal.datasource.AllStationsDataSource 12 | import com.egoriku.radiotok.datasource.intertal.datasource.RadioInfoDataSource 13 | import com.egoriku.radiotok.datasource.intertal.datasource.metadata.CountriesDataSource 14 | import com.egoriku.radiotok.datasource.intertal.datasource.metadata.LanguagesDataSource 15 | import com.egoriku.radiotok.datasource.intertal.datasource.metadata.TagsDataSource 16 | import com.egoriku.radiotok.datasource.intertal.datasource.playlist.* 17 | import org.koin.android.ext.koin.androidApplication 18 | import org.koin.dsl.module 19 | 20 | val dataSourceModule = module { 21 | 22 | factory { 23 | EntitySourceFactory( 24 | changedLatelyDataSource = get(), 25 | localStationsDataSource = get(), 26 | playingNowDataSource = get(), 27 | topClicksDataSource = get(), 28 | topVoteDataSource = get(), 29 | tagsDataSource = get(), 30 | languagesDataSource = get(), 31 | countriesDataSource = get(), 32 | radioInfoDataSource = get() 33 | ) 34 | } 35 | 36 | factory { ChangedLatelyDataSource(api = get()) } 37 | factory { 38 | LocalStationsDataSource( 39 | api = get(), 40 | context = androidApplication() 41 | ) 42 | } 43 | factory { PlayingNowDataSource(api = get()) } 44 | factory { TopClicksDataSource(api = get()) } 45 | factory { TopVoteDataSource(api = get()) } 46 | 47 | factory { TagsDataSource(api = get()) } 48 | factory { LanguagesDataSource(api = get()) } 49 | factory { CountriesDataSource(api = get()) } 50 | 51 | factory { RadioInfoDataSource(api = get()) } 52 | factory { AllStationsDataSource(api = get()) } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/playlist/components/RadioListItem.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.playlist.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.material.Icon 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.MaterialTheme.typography 10 | import androidx.compose.material.Text 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.MoreVert 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.text.style.TextOverflow 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import com.egoriku.radiotok.R 23 | import com.egoriku.radiotok.common.model.RadioItemModel 24 | import com.egoriku.radiotok.foundation.NetworkImage 25 | 26 | @Composable 27 | fun RadioListItem(radioItemModel: RadioItemModel) { 28 | Row( 29 | modifier = Modifier.padding(8.dp), 30 | verticalAlignment = Alignment.CenterVertically, 31 | ) { 32 | NetworkImage( 33 | modifier = Modifier 34 | .size(48.dp) 35 | .padding(4.dp), 36 | data = radioItemModel.icon, 37 | error = { 38 | Icon( 39 | painter = painterResource(id = R.drawable.ic_radio), 40 | tint = MaterialTheme.colors.onPrimary, 41 | contentDescription = stringResource(id = R.string.cc_radio_logo_error) 42 | ) 43 | } 44 | ) 45 | 46 | Column( 47 | modifier = Modifier 48 | .padding(horizontal = 4.dp) 49 | .weight(1f) 50 | ) { 51 | Text( 52 | text = radioItemModel.title, 53 | style = typography.h6.copy(fontSize = 16.sp), 54 | color = MaterialTheme.colors.onSurface 55 | ) 56 | Text( 57 | text = radioItemModel.metadata, 58 | style = typography.subtitle2, 59 | maxLines = 1, 60 | overflow = TextOverflow.Ellipsis 61 | ) 62 | } 63 | Icon( 64 | imageVector = Icons.Default.MoreVert, 65 | contentDescription = null, 66 | tint = Color.LightGray, 67 | modifier = Modifier.padding(4.dp) 68 | ) 69 | } 70 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/notification/actions/FavoriteActionProvider.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.notification.actions 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.support.v4.media.session.PlaybackStateCompat 6 | import androidx.core.os.bundleOf 7 | import com.egoriku.radiotok.radioplayer.R 8 | import com.egoriku.radiotok.radioplayer.constant.CustomAction.ACTION_TOGGLE_FAVORITE 9 | import com.egoriku.radiotok.radioplayer.data.CurrentRadioQueueHolder 10 | import com.egoriku.radiotok.radioplayer.data.RadioStateMediator 11 | import com.egoriku.radiotok.radioplayer.ext.id 12 | import com.google.android.exoplayer2.Player 13 | import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector 14 | 15 | class FavoriteActionProvider( 16 | private val context: Context, 17 | private val radioStateMediator: RadioStateMediator, 18 | private val currentRadioQueueHolder: CurrentRadioQueueHolder, 19 | private val onInvalidateNotification: () -> Unit 20 | ) : MediaSessionConnector.CustomActionProvider { 21 | 22 | override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { 23 | val mediaItem = currentRadioQueueHolder.getMediaMetadataOrNull( 24 | position = player.currentMediaItemIndex 25 | ) 26 | 27 | return if (mediaItem == null) { 28 | null 29 | } else { 30 | if (radioStateMediator.isLiked(mediaItem.id)) { 31 | PlaybackStateCompat.CustomAction 32 | .Builder( 33 | ACTION_TOGGLE_FAVORITE, 34 | context.getString(R.string.custom_action_favorite_remove), 35 | R.drawable.ic_favorite 36 | ).setExtras( 37 | bundleOf("IS_LIKED" to true) 38 | ).build() 39 | } else { 40 | PlaybackStateCompat.CustomAction 41 | .Builder( 42 | ACTION_TOGGLE_FAVORITE, 43 | context.getString(R.string.custom_action_favorite_add), 44 | R.drawable.ic_favorite_border 45 | ) 46 | .setExtras( 47 | bundleOf("IS_LIKED" to false) 48 | ) 49 | .build() 50 | } 51 | } 52 | } 53 | 54 | override fun onCustomAction(player: Player, action: String, extras: Bundle?) { 55 | val currentMediaMetadata = currentRadioQueueHolder.getMediaMetadataOrNull( 56 | position = player.currentMediaItemIndex 57 | ) ?: return 58 | 59 | radioStateMediator.toggleLiked(id = currentMediaMetadata.id) 60 | 61 | onInvalidateNotification() 62 | } 63 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/listener/RadioPlaybackPreparer.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.listener 2 | 3 | import android.net.Uri 4 | import android.os.Bundle 5 | import android.os.ResultReceiver 6 | import android.support.v4.media.session.PlaybackStateCompat 7 | import com.egoriku.radiotok.common.ext.logD 8 | import com.egoriku.radiotok.radioplayer.data.mediator.IRadioCacheMediator 9 | import com.egoriku.radiotok.radioplayer.model.MediaPath 10 | import com.egoriku.radiotok.radioplayer.model.MediaPath.PlayLiked 11 | import com.egoriku.radiotok.radioplayer.model.MediaPath.ShuffleAndPlayRoot.ShuffleLiked 12 | import com.egoriku.radiotok.radioplayer.model.MediaPath.ShuffleAndPlayRoot.ShuffleRandom 13 | import com.google.android.exoplayer2.Player 14 | import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector 15 | import kotlinx.coroutines.runBlocking 16 | 17 | class RadioPlaybackPreparer( 18 | private val radioCacheMediator: IRadioCacheMediator, 19 | private val onPlayerPrepared: () -> Unit 20 | ) : MediaSessionConnector.PlaybackPreparer { 21 | 22 | override fun onCommand( 23 | player: Player, 24 | command: String, 25 | extras: Bundle?, 26 | cb: ResultReceiver? 27 | ) = false 28 | 29 | override fun getSupportedPrepareActions() = 30 | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or 31 | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or 32 | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH or 33 | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH 34 | 35 | override fun onPrepare(playWhenReady: Boolean) = Unit 36 | 37 | override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { 38 | logD("onPrepareFromMediaId = $mediaId") 39 | 40 | runBlocking { 41 | when (val mediaPath = MediaPath.fromParentIdOrNull(mediaId)) { 42 | is ShuffleLiked -> { 43 | radioCacheMediator.updatePlaylist(mediaPath = mediaPath) 44 | onPlayerPrepared() 45 | } 46 | is ShuffleRandom -> { 47 | radioCacheMediator.updatePlaylist(mediaPath = mediaPath) 48 | onPlayerPrepared() 49 | } 50 | is PlayLiked -> { 51 | radioCacheMediator.updatePlaylist(mediaPath = mediaPath) 52 | onPlayerPrepared() 53 | } 54 | else -> { 55 | radioCacheMediator.playSingle(id = mediaId) 56 | onPlayerPrepared() 57 | } 58 | } 59 | } 60 | } 61 | 62 | override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) { 63 | logD("onPrepareFromSearch: $query") 64 | } 65 | 66 | override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) = Unit 67 | } -------------------------------------------------------------------------------- /radioPlayer/src/main/java/com/egoriku/radiotok/radioplayer/notification/description/DescriptionAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.radioplayer.notification.description 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.graphics.Bitmap 6 | import android.graphics.BitmapFactory 7 | import android.net.Uri 8 | import android.support.v4.media.session.MediaControllerCompat 9 | import com.bumptech.glide.Glide 10 | import com.egoriku.radiotok.radioplayer.R 11 | import com.egoriku.radiotok.radioplayer.data.CurrentRadioQueueHolder 12 | import com.google.android.exoplayer2.Player 13 | import com.google.android.exoplayer2.ui.PlayerNotificationManager 14 | import kotlinx.coroutines.* 15 | 16 | const val NOTIFICATION_LARGE_ICON_SIZE = 144 17 | 18 | class DescriptionAdapter( 19 | private val context: Context, 20 | private val mediaController: MediaControllerCompat, 21 | private val currentRadioQueueHolder: CurrentRadioQueueHolder 22 | ) : PlayerNotificationManager.MediaDescriptionAdapter { 23 | 24 | private val serviceJob = SupervisorJob() 25 | private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob) 26 | 27 | private var currentIconUri: Uri? = null 28 | private var currentBitmap: Bitmap? = null 29 | 30 | override fun getCurrentContentTitle(player: Player) = 31 | getDescription(index = player.currentMediaItemIndex)?.title.toString() 32 | 33 | override fun createCurrentContentIntent(player: Player): PendingIntent? = 34 | mediaController.sessionActivity 35 | 36 | override fun getCurrentContentText(player: Player) = 37 | getDescription(index = player.currentMediaItemIndex)?.subtitle.toString() 38 | 39 | override fun getCurrentLargeIcon( 40 | player: Player, 41 | callback: PlayerNotificationManager.BitmapCallback 42 | ): Bitmap? { 43 | val iconUri = getDescription(index = player.currentMediaItemIndex)?.iconUri 44 | 45 | return if (currentIconUri != iconUri || currentBitmap == null) { 46 | currentBitmap = null 47 | currentIconUri = iconUri 48 | 49 | serviceScope.launch { 50 | currentBitmap = loadBitmap(iconUri) 51 | currentBitmap?.let { callback.onBitmap(it) } 52 | } 53 | 54 | null 55 | } else { 56 | currentBitmap 57 | } 58 | } 59 | 60 | private fun getDescription(index: Int) = 61 | currentRadioQueueHolder.getMediaMetadataOrNull(index)?.description 62 | 63 | private suspend fun loadBitmap(iconUri: Uri?): Bitmap? = withContext(Dispatchers.IO) { 64 | runCatching { 65 | Glide.with(context) 66 | .asBitmap() 67 | .load(iconUri) 68 | .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE) 69 | .get() 70 | }.getOrDefault( 71 | BitmapFactory.decodeResource(context.resources, R.drawable.ic_radio_round) 72 | ) 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/settings/SettingScreen.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.settings 2 | 3 | import androidx.activity.compose.rememberLauncherForActivityResult 4 | import androidx.activity.result.contract.ActivityResultContracts 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.stringResource 11 | import cafe.adriel.voyager.core.screen.Screen 12 | import com.egoriku.radiotok.R 13 | import com.egoriku.radiotok.foundation.header.ScreenHeader 14 | import com.egoriku.radiotok.foundation.header.SectionHeader 15 | import com.egoriku.radiotok.presentation.screen.settings.ui.SettingItem 16 | import com.google.accompanist.insets.systemBarsPadding 17 | import org.koin.androidx.compose.getViewModel 18 | 19 | object SettingScreen : Screen { 20 | 21 | @Composable 22 | override fun Content() { 23 | Column( 24 | modifier = Modifier 25 | .fillMaxSize() 26 | .systemBarsPadding() 27 | ) { 28 | val settingsViewModel = getViewModel() 29 | 30 | val createBackupFileLauncher = rememberLauncherForActivityResult( 31 | contract = ActivityResultContracts.CreateDocument() 32 | ) { 33 | if (it != null) { 34 | settingsViewModel.createBackup(it) 35 | } 36 | } 37 | 38 | val openBackup = rememberLauncherForActivityResult( 39 | contract = ActivityResultContracts.GetContent() 40 | ) { 41 | if (it != null) { 42 | settingsViewModel.restoreBackup(it) 43 | } 44 | } 45 | 46 | ScreenHeader(title = stringResource(R.string.settings_screen_header)) 47 | 48 | LazyColumn { 49 | item { 50 | SectionHeader(title = stringResource(R.string.settings_section_backup_header)) { 51 | SettingItem( 52 | title = stringResource(R.string.settings_section_backup_title), 53 | subtitle = stringResource(R.string.settings_section_backup_description), 54 | ) { 55 | createBackupFileLauncher.launch(settingsViewModel.backupFileName) 56 | } 57 | SettingItem( 58 | title = stringResource(R.string.settings_section_restore_title), 59 | subtitle = stringResource(R.string.settings_section_restore_description) 60 | ) { 61 | openBackup.launch(("*/*")) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/egoriku/radiotok/presentation/screen/feed/ui/InstantRadio.kt: -------------------------------------------------------------------------------- 1 | package com.egoriku.radiotok.presentation.screen.feed.ui 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.aspectRatio 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material.Card 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.MaterialTheme 14 | import androidx.compose.material.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.text.style.TextAlign 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | import com.egoriku.radiotok.R 23 | import com.egoriku.radiotok.domain.model.section.FeedType.InstantPlay 24 | import com.egoriku.radiotok.presentation.ui.RadioTokTheme 25 | 26 | @Preview(showBackground = true) 27 | @Composable 28 | private fun InstantRadioPreview() { 29 | RadioTokTheme { 30 | InstantRadio( 31 | feed = InstantPlay( 32 | mediaId = "Test", 33 | name = "Liked", 34 | icon = R.drawable.ic_favorite 35 | ) 36 | ) {} 37 | } 38 | } 39 | 40 | @Composable 41 | fun InstantRadio( 42 | modifier: Modifier = Modifier, 43 | feed: InstantPlay, 44 | onClick: () -> Unit 45 | ) { 46 | Card( 47 | shape = RoundedCornerShape(16.dp), 48 | backgroundColor = MaterialTheme.colors.secondary, 49 | modifier = modifier 50 | .height(100.dp) 51 | .aspectRatio(2.5f) 52 | ) { 53 | Row( 54 | verticalAlignment = Alignment.CenterVertically, 55 | modifier = Modifier.clickable(onClick = onClick), 56 | ) { 57 | Box( 58 | contentAlignment = Alignment.Center, 59 | modifier = Modifier 60 | .aspectRatio(1f) 61 | .background(MaterialTheme.colors.onSecondary) 62 | .border( 63 | width = 3.dp, 64 | color = MaterialTheme.colors.secondary, 65 | shape = RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp) 66 | ) 67 | ) { 68 | Icon( 69 | painter = painterResource(id = feed.icon), 70 | tint = MaterialTheme.colors.secondary, 71 | contentDescription = null 72 | ) 73 | } 74 | Text( 75 | modifier = Modifier.weight(1f), 76 | textAlign = TextAlign.Center, 77 | text = feed.name, 78 | style = MaterialTheme.typography.body1 79 | ) 80 | } 81 | } 82 | } --------------------------------------------------------------------------------