├── 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 |

3 |
4 |
5 |
6 | RadioTok - listen to the radio from over the world
7 |
8 |
9 |
10 | ## Android phone UI
11 |
12 |
13 | ## Android Auto UI
14 |
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 | }
--------------------------------------------------------------------------------