├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml └── workflows │ ├── ci.yml │ └── unsigned_release.yml ├── .gitignore ├── LICENSE ├── README.de-DE.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── schemas │ └── app.suhasdissa.vibeyou.data.database.SongDatabase │ │ ├── 1.json │ │ └── 2.json └── src │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── app │ │ └── suhasdissa │ │ └── vibeyou │ │ ├── AppContainer.kt │ │ ├── MainActivity.kt │ │ ├── MellowMusicApplication.kt │ │ ├── data │ │ ├── api │ │ │ ├── HyperpipeApi.kt │ │ │ └── PipedApi.kt │ │ └── database │ │ │ ├── SongDatabase.kt │ │ │ ├── dao │ │ │ ├── PlaylistDao.kt │ │ │ ├── RawDao.kt │ │ │ ├── SearchDao.kt │ │ │ └── SongsDao.kt │ │ │ └── entities │ │ │ ├── PlaylistEntity.kt │ │ │ ├── PlaylistWithSongs.kt │ │ │ ├── SearchQuery.kt │ │ │ ├── SongEntity.kt │ │ │ └── SongPlaylistMap.kt │ │ ├── domain │ │ ├── models │ │ │ ├── Login.kt │ │ │ ├── PipedInstance.kt │ │ │ ├── PipedVIdeoResponse.kt │ │ │ ├── PlayerRepeatMode.kt │ │ │ ├── PlayerState.kt │ │ │ ├── SearchFilter.kt │ │ │ ├── Token.kt │ │ │ ├── artists │ │ │ │ ├── Artist.kt │ │ │ │ ├── Artists.kt │ │ │ │ ├── Channel.kt │ │ │ │ └── ChannelTabResponse.kt │ │ │ ├── hyper │ │ │ │ ├── MediaSession.kt │ │ │ │ ├── NextSongsResponse.kt │ │ │ │ ├── Song.kt │ │ │ │ └── Thumbnail.kt │ │ │ ├── playlists │ │ │ │ ├── Playlist.kt │ │ │ │ ├── PlaylistInfo.kt │ │ │ │ └── Playlists.kt │ │ │ ├── primary │ │ │ │ ├── Album.kt │ │ │ │ ├── Artist.kt │ │ │ │ ├── EqualizerData.kt │ │ │ │ └── Song.kt │ │ │ └── songs │ │ │ │ ├── SongItem.kt │ │ │ │ └── Songs.kt │ │ └── repository │ │ │ ├── AuthRepository.kt │ │ │ ├── LocalMusicRepository.kt │ │ │ ├── PipedMusicRepository.kt │ │ │ ├── PlaylistRepository.kt │ │ │ ├── SongDatabaseRepository.kt │ │ │ └── SongDatabaseRepositoryImpl.kt │ │ ├── navigation │ │ ├── CustomNavTypes.kt │ │ ├── Destination.kt │ │ └── NavHost.kt │ │ ├── presentation │ │ ├── components │ │ │ ├── ChipSelect.kt │ │ │ ├── IconCard.kt │ │ │ ├── IllustratedMessageScreen.kt │ │ │ ├── LoadingScreen.kt │ │ │ ├── MiniPlayerScaffold.kt │ │ │ ├── ModalNavDrawerContent.kt │ │ │ ├── SongCard.kt │ │ │ ├── SongList.kt │ │ │ ├── SongListView.kt │ │ │ ├── SongSettingsSheet.kt │ │ │ └── VerticalSlider.kt │ │ ├── layout │ │ │ ├── MainAppContent.kt │ │ │ ├── ModelNavDrawerLayout.kt │ │ │ ├── PermanentNavDrawerLayout.kt │ │ │ └── PermanentNavDrawerWithPlayerLayout.kt │ │ └── screens │ │ │ ├── album │ │ │ ├── AlbumScreen.kt │ │ │ ├── components │ │ │ │ ├── AlbumCard.kt │ │ │ │ └── AlbumList.kt │ │ │ └── model │ │ │ │ ├── LocalPlaylistViewModel.kt │ │ │ │ ├── NewPlaylistViewModel.kt │ │ │ │ └── OnlinePlaylistViewModel.kt │ │ │ ├── artist │ │ │ ├── ArtistScreen.kt │ │ │ ├── components │ │ │ │ ├── ArtistCard.kt │ │ │ │ └── ArtistList.kt │ │ │ └── model │ │ │ │ ├── LocalArtistViewModel.kt │ │ │ │ └── OnlineArtistViewModel.kt │ │ │ ├── localmusic │ │ │ ├── LocalMusicScreen.kt │ │ │ ├── components │ │ │ │ └── SortOrderDialog.kt │ │ │ └── model │ │ │ │ └── LocalSongViewModel.kt │ │ │ ├── localsearch │ │ │ ├── LocalSearchScreen.kt │ │ │ └── model │ │ │ │ └── LocalSearchViewModel.kt │ │ │ ├── onlinemusic │ │ │ ├── MusicScreen.kt │ │ │ ├── components │ │ │ │ └── SongsScreen.kt │ │ │ └── model │ │ │ │ ├── SongOptionsViewModel.kt │ │ │ │ └── SongViewModel.kt │ │ │ ├── onlinesearch │ │ │ ├── SearchScreen.kt │ │ │ └── model │ │ │ │ ├── PipedSearchViewModel.kt │ │ │ │ └── state │ │ │ │ ├── AlbumInfoState.kt │ │ │ │ ├── ArtistInfoState.kt │ │ │ │ └── SearchState.kt │ │ │ ├── player │ │ │ ├── FullScreenPlayer.kt │ │ │ ├── MiniPlayer.kt │ │ │ ├── components │ │ │ │ ├── EqualizerSheet.kt │ │ │ │ ├── Queue.kt │ │ │ │ ├── QueueSheet.kt │ │ │ │ └── SongOptionsSheet.kt │ │ │ └── model │ │ │ │ └── PlayerViewModel.kt │ │ │ ├── playlists │ │ │ ├── PlaylistsScreen.kt │ │ │ ├── components │ │ │ │ └── PlaylistOptionsSheet.kt │ │ │ └── model │ │ │ │ ├── PlaylistInfoViewModel.kt │ │ │ │ └── PlaylistViewModel.kt │ │ │ └── settings │ │ │ ├── AboutScreen.kt │ │ │ ├── AppearanceSettingsScreen.kt │ │ │ ├── DatabaseSettingsScreen.kt │ │ │ ├── NetworkSettingsScreen.kt │ │ │ ├── SettingsScreen.kt │ │ │ ├── components │ │ │ ├── ButtonGroupPref.kt │ │ │ ├── CacheSizeDialog.kt │ │ │ ├── ColorPref.kt │ │ │ ├── CustomInstanceOption.kt │ │ │ ├── InstanceSelectDialog.kt │ │ │ ├── LocalMusicPathsDialog.kt │ │ │ ├── SettingItem.kt │ │ │ ├── SwitchPref.kt │ │ │ └── TextFieldPref.kt │ │ │ └── model │ │ │ ├── AuthViewModel.kt │ │ │ ├── CheckUpdateViewModel.kt │ │ │ ├── DatabaseViewModel.kt │ │ │ ├── SettingsModel.kt │ │ │ └── SettingsViewModel.kt │ │ ├── ui │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── utils │ │ ├── Convertions.kt │ │ ├── DynamicDataSource.kt │ │ ├── Mappers.kt │ │ ├── OpenBrowser.kt │ │ ├── PermissionHelper.kt │ │ ├── Player.kt │ │ ├── PlayerState.kt │ │ ├── Pref.kt │ │ ├── Preferences.kt │ │ ├── RetrofitHelper.kt │ │ ├── ThemeUtil.kt │ │ ├── TimeUtil.kt │ │ ├── UpdateUtil.kt │ │ ├── UriSerializer.kt │ │ └── services │ │ └── PlayerService.kt │ └── res │ ├── drawable │ ├── blob.xml │ ├── ic_launcher_foreground.xml │ ├── ic_launcher_monochrome.xml │ ├── ic_piped.xml │ └── music_placeholder.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-ar │ └── strings.xml │ ├── values-az │ └── strings.xml │ ├── values-bn │ └── strings.xml │ ├── values-ca │ └── strings.xml │ ├── values-cs │ └── strings.xml │ ├── values-da │ └── strings.xml │ ├── values-de-rDE │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-et │ └── strings.xml │ ├── values-fa │ └── strings.xml │ ├── values-fi │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-hi │ └── strings.xml │ ├── values-ia │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-iw │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-nb-rNO │ └── strings.xml │ ├── values-night │ └── colors.xml │ ├── values-or │ └── strings.xml │ ├── values-pa │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-sk │ └── strings.xml │ ├── values-sr │ └── strings.xml │ ├── values-sv │ └── strings.xml │ ├── values-ta │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-uk │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ └── 7.jpg │ └── short_description.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Bnyro, SuhasDissa] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Report an issue in the app 3 | labels: [bug] 4 | body: 5 | 6 | - type: textarea 7 | id: reproduce-steps 8 | attributes: 9 | label: Steps to reproduce 10 | description: Provide an example of the issue. 11 | placeholder: | 12 | Example: 13 | 1. First step 14 | 2. Second step 15 | 3. Issue here 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | id: expected-behavior 21 | attributes: 22 | label: Expected behavior 23 | placeholder: | 24 | Example: 25 | "This should happen..." 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: actual-behavior 31 | attributes: 32 | label: Actual behavior 33 | placeholder: | 34 | Example: 35 | "This happened instead..." 36 | validations: 37 | required: true 38 | 39 | - type: input 40 | id: Vibe_You-version 41 | attributes: 42 | label: Vibe You version 43 | description: | 44 | You can find your Vibe You version in **About**. 45 | placeholder: | 46 | Example: "0.5.0" 47 | validations: 48 | required: true 49 | 50 | - type: input 51 | id: android-version 52 | attributes: 53 | label: Android version 54 | description: | 55 | You can find this somewhere in your Android settings. 56 | placeholder: | 57 | Example: "Android 12" 58 | validations: 59 | required: true 60 | 61 | - type: textarea 62 | id: other-details 63 | attributes: 64 | label: Other details 65 | placeholder: | 66 | Additional details and attachments. 67 | 68 | - type: checkboxes 69 | id: acknowledgements 70 | attributes: 71 | label: Acknowledgements 72 | description: Your issue will be closed if you haven't done these steps. 73 | options: 74 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. 75 | required: true 76 | - label: I have written a short but informative title. 77 | required: true 78 | - label: I will fill out all of the requested information in this form. 79 | required: true 80 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: ⭐ Feature request 2 | description: Suggest a feature to improve Vibe You 3 | labels: [enhancement] 4 | body: 5 | 6 | - type: textarea 7 | id: feature-description 8 | attributes: 9 | label: Describe your suggested feature 10 | description: How can an existing source be improved? 11 | placeholder: | 12 | Example: 13 | "It could work like this..." 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | id: other-details 19 | attributes: 20 | label: Other details 21 | placeholder: | 22 | Additional details and attachments. 23 | 24 | - type: checkboxes 25 | id: acknowledgements 26 | attributes: 27 | label: Acknowledgements 28 | description: Your issue will be closed if you haven't done these steps. 29 | options: 30 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. 31 | required: true 32 | - label: I have written a short but informative title. 33 | required: true 34 | - label: I will fill out all of the requested information in this form. 35 | required: true 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Create Debug Apk 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "README*.md" 7 | - "app/src/main/res/**" 8 | - ".github/**" 9 | pull_request: 10 | paths-ignore: 11 | - "README*.md" 12 | - "app/src/main/res/**" 13 | - ".github/**" 14 | 15 | jobs: 16 | debug-builds: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Validate Gradle Wrapper 23 | uses: gradle/wrapper-validation-action@v2 24 | 25 | - name: Setup JDK 26 | uses: actions/setup-java@v4 27 | with: 28 | java-version: 17 29 | distribution: "temurin" 30 | cache: "gradle" 31 | 32 | - name: Build APK 33 | run: bash ./gradlew assembleDebug --stacktrace 34 | 35 | - name: Upload APK 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: app 39 | path: app/build/outputs/apk/debug/*.apk 40 | -------------------------------------------------------------------------------- /.github/workflows/unsigned_release.yml: -------------------------------------------------------------------------------- 1 | name: Create Unsigned Release APK 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | debug-builds: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Validate Gradle Wrapper 14 | uses: gradle/wrapper-validation-action@v2 15 | 16 | - name: Setup JDK 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: 17 20 | distribution: "temurin" 21 | 22 | - name: Build APK 23 | run: bash ./gradlew assembleRelease --stacktrace 24 | 25 | - name: Upload APK 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: app 29 | path: app/build/outputs/apk/release/*.apk 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /README.de-DE.md: -------------------------------------------------------------------------------- 1 |
2 | App icon 3 |

Vibe You

4 | Vibe You (formerly MellowMusic) is a music app that lets you play music from your device storage and Piped Music. 5 |
It has a Material You dynamic theme with dark mode support, offline cached playback, shuffle, queue, and more. 6 |
7 |
8 | 9 |
10 | License 11 | Downloads 12 | Last commit 13 | Repo size 14 | Stars 15 |
16 |
17 | 18 | --- 19 | 20 |
21 | Screenshots 22 |

23 | 24 | 25 | 26 |

27 |

28 | 29 | 30 | 31 |

32 |

33 | 34 |

35 |
36 | 37 | ## Features 38 | 39 | - **Material You dynamic theme with dark mode support:** Enjoy a sleek and modern interface that 40 | matches your device's theme. 41 | - **Play songs from your device storage:** Play your favorite songs, even when you're offline. 42 | - **Listen to online songs from Piped Music:** Access Piped Music's vast library of songs, 43 | without region blocks. 44 | - **Offline cached playback:** Listen to your music offline, even without an internet connection. 45 | - **Add songs to a favorite playlist:** Create playlists of your favorite songs and listen to them 46 | on demand. 47 | - **Shuffle songs:** Listen to your music in random order. 48 | - **Advanced queue management:** Add/Remove and reorder queue to personallize listening experience. 49 | 50 | ## Installation 51 | 52 | [Get it on GitHub](https://github.com/you-apps/VibeYou/releases/latest) 55 | 56 | 57 | ## Feedback and contributions 58 | ***All contributions are very welcome!*** 59 | 60 | * Feel free to join the [Matrix room](https://matrix.to/#/#you-apps:matrix.org) for discussions about the app. 61 | * Bug reports and feature requests can be submitted [here](https://github.com/you-apps/VibeYou/issues) (please make sure to fill out all the requested information properly!). 62 | * If you are a developer and wish to contribute to the app, please **fork** the project and submit a [**pull request**](https://help.github.com/articles/about-pull-requests/). 63 | 64 | ## Translation 65 | 66 | Translation status 67 | 68 | 69 | ## Credits 70 | * Icon design by [M00NJ](https://github.com/M00NJ) 71 | 72 | ## License 73 | 74 | Vibe You is licensed under the [**GNU General Public License**](https://www.gnu.org/licenses/gpl.html): You can use, study and share it as you want. 75 | 76 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation 2 | 3 | -keepattributes RuntimeVisibleAnnotations,AnnotationDefault 4 | 5 | -dontwarn okhttp3.internal.platform.** 6 | -dontwarn org.conscrypt.** 7 | -dontwarn org.bouncycastle.** 8 | -dontwarn org.openjsse.** -------------------------------------------------------------------------------- /app/schemas/app.suhasdissa.vibeyou.data.database.SongDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "ada7c6c2bcd1884e04cc9a6260733689", 6 | "entities": [ 7 | { 8 | "tableName": "song", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT, `thumbnailUrl` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "title", 19 | "columnName": "title", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "artistsText", 25 | "columnName": "artistsText", 26 | "affinity": "TEXT", 27 | "notNull": false 28 | }, 29 | { 30 | "fieldPath": "durationText", 31 | "columnName": "durationText", 32 | "affinity": "TEXT", 33 | "notNull": false 34 | }, 35 | { 36 | "fieldPath": "thumbnailUrl", 37 | "columnName": "thumbnailUrl", 38 | "affinity": "TEXT", 39 | "notNull": false 40 | }, 41 | { 42 | "fieldPath": "likedAt", 43 | "columnName": "likedAt", 44 | "affinity": "INTEGER", 45 | "notNull": false 46 | }, 47 | { 48 | "fieldPath": "totalPlayTimeMs", 49 | "columnName": "totalPlayTimeMs", 50 | "affinity": "INTEGER", 51 | "notNull": true 52 | } 53 | ], 54 | "primaryKey": { 55 | "autoGenerate": false, 56 | "columnNames": [ 57 | "id" 58 | ] 59 | }, 60 | "indices": [], 61 | "foreignKeys": [] 62 | }, 63 | { 64 | "tableName": "SearchQuery", 65 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", 66 | "fields": [ 67 | { 68 | "fieldPath": "id", 69 | "columnName": "id", 70 | "affinity": "INTEGER", 71 | "notNull": true 72 | }, 73 | { 74 | "fieldPath": "query", 75 | "columnName": "query", 76 | "affinity": "TEXT", 77 | "notNull": true 78 | } 79 | ], 80 | "primaryKey": { 81 | "autoGenerate": true, 82 | "columnNames": [ 83 | "id" 84 | ] 85 | }, 86 | "indices": [ 87 | { 88 | "name": "index_SearchQuery_query", 89 | "unique": true, 90 | "columnNames": [ 91 | "query" 92 | ], 93 | "orders": [], 94 | "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" 95 | } 96 | ], 97 | "foreignKeys": [] 98 | } 99 | ], 100 | "views": [], 101 | "setupQueries": [ 102 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 103 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ada7c6c2bcd1884e04cc9a6260733689')" 104 | ] 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vibe You Debug 4 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/AppContainer.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou 2 | 3 | import android.content.ContentResolver 4 | import androidx.media3.session.MediaController 5 | import app.suhasdissa.vibeyou.backend.repository.AuthRepository 6 | import app.suhasdissa.vibeyou.backend.repository.AuthRepositoryImpl 7 | import app.suhasdissa.vibeyou.backend.repository.LocalMusicRepository 8 | import app.suhasdissa.vibeyou.backend.repository.PipedMusicRepository 9 | import app.suhasdissa.vibeyou.backend.repository.PlaylistRepository 10 | import app.suhasdissa.vibeyou.backend.repository.SongDatabaseRepository 11 | import app.suhasdissa.vibeyou.backend.repository.SongDatabaseRepositoryImpl 12 | import app.suhasdissa.vibeyou.data.database.SongDatabase 13 | import com.google.common.util.concurrent.ListenableFuture 14 | 15 | interface AppContainer { 16 | val database: SongDatabase 17 | val songDatabaseRepository: SongDatabaseRepository 18 | val pipedMusicRepository: PipedMusicRepository 19 | val localMusicRepository: LocalMusicRepository 20 | val playlistRepository: PlaylistRepository 21 | val authRepository: AuthRepository 22 | val controllerFuture: ListenableFuture 23 | val contentResolver: ContentResolver 24 | } 25 | 26 | class DefaultAppContainer( 27 | override val database: SongDatabase, 28 | override val controllerFuture: ListenableFuture, 29 | override val contentResolver: ContentResolver 30 | ) : AppContainer { 31 | override val songDatabaseRepository: SongDatabaseRepository by lazy { 32 | SongDatabaseRepositoryImpl(database.songsDao()) 33 | } 34 | override val pipedMusicRepository: PipedMusicRepository by lazy { 35 | PipedMusicRepository(database.songsDao(), database.searchDao()) 36 | } 37 | override val localMusicRepository: LocalMusicRepository by lazy { 38 | LocalMusicRepository(contentResolver, database.searchDao()) 39 | } 40 | override val playlistRepository: PlaylistRepository by lazy { 41 | PlaylistRepository(database.playlistDao(), database.songsDao()) 42 | } 43 | override val authRepository: AuthRepository by lazy { 44 | AuthRepositoryImpl() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/MellowMusicApplication.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou 2 | 3 | import android.app.Application 4 | import android.content.ComponentName 5 | import android.graphics.Color 6 | import androidx.media3.session.MediaController 7 | import androidx.media3.session.SessionToken 8 | import app.suhasdissa.vibeyou.data.database.SongDatabase 9 | import app.suhasdissa.vibeyou.domain.models.primary.EqualizerData 10 | import app.suhasdissa.vibeyou.utils.Pref 11 | import app.suhasdissa.vibeyou.utils.UpdateUtil 12 | import app.suhasdissa.vibeyou.utils.preferences 13 | import app.suhasdissa.vibeyou.utils.services.PlayerService 14 | import coil.ImageLoader 15 | import coil.ImageLoaderFactory 16 | import coil.disk.DiskCache 17 | 18 | class MellowMusicApplication : Application(), ImageLoaderFactory { 19 | 20 | private val database by lazy { SongDatabase.getDatabase(this) } 21 | lateinit var container: AppContainer 22 | var accentColor: Int = Color.TRANSPARENT 23 | 24 | /** 25 | * Data stored here with the details of the equalizer 26 | */ 27 | var supportedEqualizerData: EqualizerData? = null 28 | 29 | override fun onCreate() { 30 | super.onCreate() 31 | val sessionToken = 32 | SessionToken( 33 | this, 34 | ComponentName(this, PlayerService::class.java) 35 | ) 36 | val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync() 37 | container = DefaultAppContainer(database, controllerFuture, contentResolver) 38 | Pref.sharedPreferences = preferences 39 | UpdateUtil.getCurrentVersion(this.applicationContext) 40 | } 41 | 42 | override fun newImageLoader(): ImageLoader { 43 | return ImageLoader.Builder(this) 44 | .crossfade(true) 45 | .respectCacheHeaders(false) 46 | .diskCache( 47 | DiskCache.Builder() 48 | .directory(cacheDir.resolve("image_cache")) 49 | .build() 50 | ).build() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/api/HyperpipeApi.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.api 2 | 3 | import app.suhasdissa.vibeyou.backend.models.hyper.NextSongsResponse 4 | import retrofit2.http.GET 5 | import retrofit2.http.Path 6 | 7 | interface HyperpipeApi { 8 | @GET("https://{instance}/next/{videoId}") 9 | suspend fun getNext( 10 | @Path("instance") instance: String, 11 | @Path("videoId") videoId: String 12 | ): NextSongsResponse 13 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/SongDatabase.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database 2 | 3 | import android.content.Context 4 | import androidx.room.AutoMigration 5 | import androidx.room.Database 6 | import androidx.room.Room 7 | import androidx.room.RoomDatabase 8 | import app.suhasdissa.vibeyou.data.database.dao.PlaylistDao 9 | import app.suhasdissa.vibeyou.data.database.dao.RawDao 10 | import app.suhasdissa.vibeyou.data.database.dao.SearchDao 11 | import app.suhasdissa.vibeyou.data.database.dao.SongsDao 12 | import app.suhasdissa.vibeyou.data.database.entities.PlaylistEntity 13 | import app.suhasdissa.vibeyou.data.database.entities.SearchQuery 14 | import app.suhasdissa.vibeyou.data.database.entities.SongEntity 15 | import app.suhasdissa.vibeyou.data.database.entities.SongPlaylistMap 16 | 17 | @Database( 18 | entities = [ 19 | SongEntity::class, 20 | SearchQuery::class, 21 | PlaylistEntity::class, 22 | SongPlaylistMap::class 23 | ], 24 | version = 2, 25 | exportSchema = true, 26 | autoMigrations = [ 27 | AutoMigration(from = 1, to = 2) 28 | ] 29 | ) 30 | abstract class SongDatabase : RoomDatabase() { 31 | 32 | abstract fun songsDao(): SongsDao 33 | abstract fun playlistDao(): PlaylistDao 34 | abstract fun searchDao(): SearchDao 35 | abstract fun rawDao(): RawDao 36 | 37 | companion object { 38 | @Volatile 39 | private var INSTANCE: SongDatabase? = null 40 | 41 | fun getDatabase(context: Context): SongDatabase { 42 | return INSTANCE ?: synchronized(this) { 43 | val instance = Room.databaseBuilder( 44 | context.applicationContext, 45 | SongDatabase::class.java, 46 | "song_database" 47 | ) 48 | .fallbackToDestructiveMigration() 49 | .allowMainThreadQueries().build() 50 | INSTANCE = instance 51 | instance 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/dao/PlaylistDao.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import androidx.room.Transaction 9 | import app.suhasdissa.vibeyou.data.database.entities.PlaylistEntity 10 | import app.suhasdissa.vibeyou.data.database.entities.PlaylistWithSongs 11 | import app.suhasdissa.vibeyou.data.database.entities.SongPlaylistMap 12 | import kotlinx.coroutines.flow.Flow 13 | 14 | @Dao 15 | interface PlaylistDao { 16 | @Insert(entity = PlaylistEntity::class, onConflict = OnConflictStrategy.REPLACE) 17 | fun addPlaylist(playlist: PlaylistEntity) 18 | 19 | @Insert(entity = SongPlaylistMap::class, onConflict = OnConflictStrategy.REPLACE) 20 | fun addPlaylistMaps(maps: List) 21 | 22 | @Query("SELECT * from playlists") 23 | fun getAllPlaylists(): Flow> 24 | 25 | @Transaction 26 | @Query("SELECT * from playlists WHERE id=:id") 27 | fun getPlaylist(id: String): PlaylistWithSongs 28 | 29 | @Delete(entity = PlaylistEntity::class) 30 | fun removePlaylist(playlist: PlaylistEntity) 31 | 32 | @Query("DELETE FROM playlist_songs WHERE playlistId = :playlistId") 33 | fun clearPlaylist(playlistId: String) 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/dao/RawDao.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.RawQuery 5 | import androidx.sqlite.db.SupportSQLiteQuery 6 | 7 | @Dao 8 | interface RawDao { 9 | @RawQuery 10 | fun raw(supportSQLiteQuery: SupportSQLiteQuery): Int 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/dao/SearchDao.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import app.suhasdissa.vibeyou.data.database.entities.SearchQuery 8 | 9 | @Dao 10 | interface SearchDao { 11 | @Insert(entity = SearchQuery::class, onConflict = OnConflictStrategy.REPLACE) 12 | fun addSearchQuery(query: SearchQuery) 13 | 14 | @Query("SELECT * from SearchQuery") 15 | fun getSearchHistory(): List 16 | 17 | @Query("DELETE from SearchQuery") 18 | fun deleteAll() 19 | 20 | @Query("DELETE from SearchQuery WHERE `query` = :query") 21 | fun deleteQuery(query: String) 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/dao/SongsDao.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import app.suhasdissa.vibeyou.data.database.entities.SongEntity 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | @Dao 12 | interface SongsDao { 13 | @Insert(entity = SongEntity::class, onConflict = OnConflictStrategy.REPLACE) 14 | fun addSong(song: SongEntity) 15 | 16 | @Insert(entity = SongEntity::class, onConflict = OnConflictStrategy.REPLACE) 17 | fun addSongs(songs: List) 18 | 19 | @Delete(entity = SongEntity::class) 20 | fun removeSong(song: SongEntity) 21 | 22 | @Delete(entity = SongEntity::class) 23 | fun removeSongs(song: List) 24 | 25 | @Query("SELECT * from song WHERE id=:id") 26 | fun getSongById(id: String): SongEntity? 27 | 28 | @Query("SELECT * from song") 29 | fun getAllSongs(): List 30 | 31 | @Query("SELECT * from song WHERE likedAt IS NOT NULL") 32 | fun getFavSongs(): List 33 | 34 | @Query("SELECT * from song ORDER BY title ASC") 35 | fun getAllSongsStream(): Flow> 36 | 37 | @Query("SELECT * from song WHERE likedAt IS NOT NULL ORDER BY title ASC") 38 | fun getFavSongsStream(): Flow> 39 | 40 | @Query("SELECT * from song ORDER BY id DESC LIMIT :limit") 41 | fun getRecentSongs(limit: Int): List 42 | 43 | @Query("SELECT * from song WHERE title LIKE :search OR artistsText LIKE :search") 44 | fun search(search: String): List 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/entities/PlaylistEntity.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database.entities 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import app.suhasdissa.vibeyou.domain.models.primary.Album 6 | 7 | @Entity(tableName = "playlists") 8 | data class PlaylistEntity( 9 | @PrimaryKey val id: String, 10 | val title: String, 11 | val type: Album.Type, 12 | val subTitle: String? = null, 13 | val thumbnailUrl: String? = null 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/entities/PlaylistWithSongs.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database.entities 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Junction 5 | import androidx.room.Relation 6 | 7 | data class PlaylistWithSongs( 8 | @Embedded val playlist: PlaylistEntity, 9 | @Relation( 10 | entity = SongEntity::class, 11 | parentColumn = "id", 12 | entityColumn = "id", 13 | associateBy = Junction( 14 | value = SongPlaylistMap::class, 15 | parentColumn = "playlistId", 16 | entityColumn = "songId" 17 | ) 18 | ) 19 | val songs: List = listOf() 20 | ) 21 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/entities/SearchQuery.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database.entities 2 | 3 | import androidx.room.Entity 4 | import androidx.room.Index 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity( 8 | indices = [ 9 | Index( 10 | value = ["query"], 11 | unique = true 12 | ) 13 | ] 14 | ) 15 | data class SearchQuery( 16 | @PrimaryKey(autoGenerate = true) val id: Long = 0, 17 | val query: String 18 | ) 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/entities/SongEntity.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database.entities 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "song") 8 | data class SongEntity( 9 | @PrimaryKey val id: String, 10 | val title: String, 11 | val artistsText: String? = null, 12 | val durationText: String?, 13 | val thumbnailUrl: String?, 14 | val likedAt: Long? = null, 15 | val totalPlayTimeMs: Long = 0, 16 | @ColumnInfo(defaultValue = "0") val isLocal: Boolean = false 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/data/database/entities/SongPlaylistMap.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.data.database.entities 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.ForeignKey 6 | 7 | @Entity( 8 | tableName = "playlist_songs", 9 | primaryKeys = ["playlistId", "songId"], 10 | foreignKeys = [ 11 | ForeignKey( 12 | entity = SongEntity::class, 13 | parentColumns = ["id"], 14 | childColumns = ["songId"], 15 | onDelete = ForeignKey.CASCADE 16 | ), 17 | ForeignKey( 18 | entity = PlaylistEntity::class, 19 | parentColumns = ["id"], 20 | childColumns = ["playlistId"], 21 | onDelete = ForeignKey.CASCADE 22 | ) 23 | ] 24 | ) 25 | data class SongPlaylistMap( 26 | @ColumnInfo(index = true) val playlistId: String, 27 | @ColumnInfo(index = true) val songId: String 28 | ) 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/Login.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Login( 8 | @SerialName("username") val username: String, 9 | @SerialName("password") val password: String 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/PipedInstance.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class PipedInstance( 8 | val name: String, 9 | @SerialName("api_url") val apiUrl: String, 10 | val cache: Boolean = false, 11 | val cdn: Boolean = false, 12 | @SerialName("image_proxy_url") val imageProxyUrl: String = "", 13 | @SerialName("last_checked") val lastChecked: Int = 0, 14 | val locations: String = "", 15 | val registered: Int = 0, 16 | @SerialName("registration_disabled") val registrationDisabled: Boolean = false, 17 | @SerialName("s3_enabled") val s3Enabled: Boolean = false, 18 | @SerialName("up_to_date") val upToDate: Boolean = false, 19 | val version: String = "" 20 | ) { 21 | val netLoc = apiUrl.replace("https://", "") 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/PipedVIdeoResponse.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models 2 | 3 | import app.suhasdissa.vibeyou.backend.models.songs.SongItem 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class PipedSongResponse( 9 | @SerialName("title") var title: String? = null, 10 | @SerialName("description") var description: String? = null, 11 | @SerialName("uploader") var uploader: String? = null, 12 | @SerialName("thumbnailUrl") var thumbnailUrl: String? = null, 13 | @SerialName("hls") var hls: String? = null, 14 | @SerialName("duration") var duration: Int? = null, 15 | @SerialName("audioStreams") var audioStreams: ArrayList = arrayListOf(), 16 | @SerialName("relatedStreams") var relatedStreams: ArrayList = arrayListOf() 17 | 18 | ) 19 | 20 | @Serializable 21 | data class AudioStreams( 22 | 23 | @SerialName("url") var url: String? = null 24 | /* @SerialName("format") var format: String? = null, 25 | @SerialName("quality") var quality: String? = null, 26 | @SerialName("mimeType") var mimeType: String? = null, 27 | @SerialName("codec") var codec: String? = null, 28 | @SerialName("audioTrackId") var audioTrackId: String? = null, 29 | @SerialName("audioTrackName") var audioTrackName: String? = null, 30 | @SerialName("videoOnly") var videoOnly: Boolean? = null, 31 | @SerialName("bitrate") var bitrate: Int? = null, 32 | @SerialName("initStart") var initStart: Int? = null, 33 | @SerialName("initEnd") var initEnd: Int? = null, 34 | @SerialName("indexStart") var indexStart: Int? = null, 35 | @SerialName("indexEnd") var indexEnd: Int? = null, 36 | @SerialName("width") var width: Int? = null, 37 | @SerialName("height") var height: Int? = null, 38 | @SerialName("fps") var fps: Int? = null*/ 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/PlayerRepeatMode.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models 2 | 3 | import androidx.media3.common.Player 4 | 5 | /* 6 | DO not change order 7 | int REPEAT_MODE_OFF = 0; 8 | int REPEAT_MODE_ONE = 1; 9 | int REPEAT_MODE_ALL = 2; 10 | */ 11 | enum class PlayerRepeatMode(val mode: Int) { 12 | OFF(Player.REPEAT_MODE_OFF), 13 | ONE(Player.REPEAT_MODE_ONE), 14 | ALL(Player.REPEAT_MODE_ALL) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/PlayerState.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models 2 | 3 | enum class PlayerState { 4 | Buffer, Play, Pause 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/SearchFilter.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models 2 | 3 | enum class SearchFilter(val value: String) { 4 | Songs("music_songs"), 5 | Videos("music_videos"), 6 | Albums("music_albums"), 7 | Playlists("music_playlists"), 8 | Artists("music_artists") 9 | } 10 | 11 | enum class LocalSearchFilter { 12 | Songs, 13 | Albums, 14 | Artists 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/Token.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Token( 8 | @SerialName("token") val token: String? = null, 9 | @SerialName("error") val error: String? = null 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/artists/Artist.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.artists 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Artist( 8 | @SerialName("url") var url: String, 9 | @SerialName("name") var name: String, 10 | @SerialName("thumbnail") var thumbnail: String? = null, 11 | @SerialName("description") var description: String? = null 12 | ) { 13 | val artistId 14 | get() = url.replace("/channel/", "") 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/artists/Artists.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.artists 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Artists( 8 | @SerialName("items") var items: ArrayList = arrayListOf() 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/artists/Channel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.artists 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Channel( 7 | val id: String? = null, 8 | val name: String? = null, 9 | val avatarUrl: String? = null, 10 | val bannerUrl: String? = null, 11 | val description: String? = null, 12 | val tabs: List = emptyList() 13 | ) 14 | 15 | @Serializable 16 | data class ChannelTab( 17 | val name: String, 18 | val data: String 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/artists/ChannelTabResponse.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.artists 2 | 3 | import app.suhasdissa.vibeyou.backend.models.playlists.Playlist 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class ChannelTabResponse( 9 | @SerialName("content") var content: List = emptyList() 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/hyper/MediaSession.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.hyper 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class MediaSession( 7 | val album: String, 8 | val thumbnails: List 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/hyper/NextSongsResponse.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.hyper 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class NextSongsResponse( 7 | val lyricsId: String, 8 | val mediaSession: MediaSession, 9 | val songs: List 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/hyper/Song.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.hyper 2 | 3 | import androidx.core.net.toUri 4 | import app.suhasdissa.vibeyou.data.database.entities.SongEntity 5 | import app.suhasdissa.vibeyou.domain.models.primary.Song 6 | import kotlinx.serialization.Serializable 7 | 8 | @Serializable 9 | data class Song( 10 | val id: String, 11 | val subtitle: String, 12 | val thumbnails: List, 13 | val title: String 14 | ) { 15 | val asSong: Song 16 | get() { 17 | return Song( 18 | id = id, 19 | title = title, 20 | artistsText = subtitle, 21 | durationText = null, 22 | thumbnailUri = thumbnails.maxByOrNull { it.height }?.url?.toUri() 23 | ) 24 | } 25 | 26 | val asSongEntity: SongEntity 27 | get() { 28 | return SongEntity( 29 | id = id, 30 | title = title, 31 | artistsText = subtitle, 32 | durationText = null, 33 | thumbnailUrl = thumbnails.maxByOrNull { it.height }?.url 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/hyper/Thumbnail.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.hyper 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Thumbnail( 7 | val height: Int, 8 | val url: String, 9 | val width: Int 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/playlists/Playlist.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.playlists 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Playlist( 8 | @SerialName("url") var url: String = "", 9 | @SerialName("type") var type: String = "", 10 | @SerialName("name") var name: String = "", 11 | @SerialName("thumbnail") var thumbnail: String = "", 12 | @SerialName("uploaderName") var uploaderName: String = "" 13 | ) { 14 | val playlistId 15 | get() = url.replace("/playlist?list=", "") 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/playlists/PlaylistInfo.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.playlists 2 | 3 | import app.suhasdissa.vibeyou.backend.models.songs.SongItem 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class PlaylistInfo( 9 | @SerialName("name") var name: String = "", 10 | @SerialName("thumbnailUrl") var thumbnailUrl: String? = null, 11 | @SerialName("relatedStreams") var relatedStreams: ArrayList = arrayListOf() 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/playlists/Playlists.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.playlists 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Playlists( 8 | @SerialName("items") var items: ArrayList = arrayListOf() 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/primary/Album.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.domain.models.primary 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import app.suhasdissa.vibeyou.utils.UriSerializer 6 | import kotlinx.parcelize.Parcelize 7 | import kotlinx.serialization.Serializable 8 | 9 | @Serializable 10 | @Parcelize 11 | data class Album( 12 | val id: String, 13 | val title: String, 14 | @Serializable(with = UriSerializer::class) 15 | val thumbnailUri: Uri? = null, 16 | val artistsText: String, 17 | val numberOfSongs: Int? = null, 18 | val isLocal: Boolean = false, 19 | val type: Type = Type.ALBUM 20 | ) : Parcelable { 21 | enum class Type { 22 | PLAYLIST, ALBUM 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/primary/Artist.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.domain.models.primary 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import app.suhasdissa.vibeyou.utils.UriSerializer 6 | import kotlinx.parcelize.Parcelize 7 | import kotlinx.serialization.Serializable 8 | 9 | @Parcelize 10 | @Serializable 11 | data class Artist( 12 | val id: String, 13 | val artistsText: String, 14 | @Serializable(with = UriSerializer::class) 15 | val thumbnailUri: Uri? = null, 16 | val description: String? = null, 17 | val numberOfTracks: Int? = null, 18 | val numberOfAlbums: Int? = null 19 | ) : Parcelable 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/primary/EqualizerData.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.domain.models.primary 2 | 3 | data class EqualizerBand( 4 | val frequency: Int, 5 | val minLevel: Short, 6 | val maxLevel: Short, 7 | ) 8 | 9 | data class EqualizerData( 10 | val presets: List, 11 | val bands: List 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/primary/Song.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.domain.models.primary 2 | 3 | import android.net.Uri 4 | 5 | data class Song( 6 | val id: String, 7 | val title: String, 8 | val artistsText: String? = null, 9 | val durationText: String?, 10 | val likedAt: Long? = null, 11 | val thumbnailUri: Uri? = null, 12 | val albumId: Long? = null, 13 | val artistId: Long? = null, 14 | val isLocal: Boolean = false, 15 | val creationDate: Long? = null, 16 | val dateAdded: Long? = null, 17 | val trackNumber: Long? = null 18 | ) { 19 | fun toggleLike(): Song { 20 | return copy( 21 | likedAt = if (likedAt == null) System.currentTimeMillis() else null 22 | ) 23 | } 24 | 25 | val isFavourite 26 | get() = likedAt != null 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/songs/SongItem.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.songs 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class SongItem( 8 | @SerialName("url") var url: String = "", 9 | @SerialName("title") var title: String = "", 10 | @SerialName("thumbnail") var thumbnail: String = "", 11 | @SerialName("uploaderName") var uploaderName: String = "", 12 | @SerialName("duration") var duration: Int = 0 13 | ) { 14 | val videoId 15 | get() = url.replace("/watch?v=", "") 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/models/songs/Songs.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.models.songs 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Songs( 8 | @SerialName("items") var items: ArrayList = arrayListOf() 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/repository/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.repository 2 | 3 | import app.suhasdissa.vibeyou.backend.models.Login 4 | import app.suhasdissa.vibeyou.backend.models.Token 5 | import app.suhasdissa.vibeyou.utils.RetrofitHelper 6 | 7 | interface AuthRepository { 8 | suspend fun getAuthToken(login: Login): Token 9 | } 10 | 11 | class AuthRepositoryImpl : AuthRepository { 12 | private val pipedApi = RetrofitHelper.createPipedApi() 13 | override suspend fun getAuthToken(login: Login): Token = pipedApi.login(login) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/repository/PlaylistRepository.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.repository 2 | 3 | import app.suhasdissa.vibeyou.data.database.dao.PlaylistDao 4 | import app.suhasdissa.vibeyou.data.database.dao.SongsDao 5 | import app.suhasdissa.vibeyou.data.database.entities.PlaylistEntity 6 | import app.suhasdissa.vibeyou.data.database.entities.PlaylistWithSongs 7 | import app.suhasdissa.vibeyou.data.database.entities.SongPlaylistMap 8 | import app.suhasdissa.vibeyou.domain.models.primary.Album 9 | import app.suhasdissa.vibeyou.domain.models.primary.Song 10 | import app.suhasdissa.vibeyou.utils.asPlaylistEntity 11 | import app.suhasdissa.vibeyou.utils.asSongEntity 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.withContext 15 | 16 | class PlaylistRepository(private val playlistDao: PlaylistDao, private val songsDao: SongsDao) { 17 | private fun createNew(album: Album) { 18 | playlistDao.addPlaylist( 19 | album.asPlaylistEntity 20 | ) 21 | } 22 | 23 | private fun addSongsToPlaylist(playlistId: String, songs: List) { 24 | songsDao.addSongs(songs.map { it.asSongEntity }) 25 | val songMap = songs.map { SongPlaylistMap(playlistId, it.id) } 26 | playlistDao.addPlaylistMaps(songMap) 27 | } 28 | 29 | suspend fun newPlaylistWithSongs(album: Album, songs: List) = 30 | withContext(Dispatchers.IO) { 31 | createNew(album) 32 | addSongsToPlaylist(album.id, songs) 33 | } 34 | 35 | fun getPlaylists(): Flow> = playlistDao.getAllPlaylists() 36 | 37 | suspend fun getPlaylist(id: String): PlaylistWithSongs = 38 | withContext(Dispatchers.IO) { return@withContext playlistDao.getPlaylist(id) } 39 | 40 | suspend fun deletePlaylist(album: Album) = 41 | withContext(Dispatchers.IO) { playlistDao.removePlaylist(album.asPlaylistEntity) } 42 | 43 | suspend fun clearPlaylist(album: Album) = withContext(Dispatchers.IO) { 44 | playlistDao.clearPlaylist(album.id) 45 | } 46 | 47 | suspend fun deletePlaylistAndSongs(album: Album) = withContext(Dispatchers.IO) { 48 | val songs = playlistDao.getPlaylist(album.id).songs 49 | playlistDao.removePlaylist(album.asPlaylistEntity) 50 | songsDao.removeSongs(songs) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/repository/SongDatabaseRepository.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.repository 2 | 3 | import app.suhasdissa.vibeyou.data.database.entities.SongEntity 4 | import app.suhasdissa.vibeyou.domain.models.primary.Song 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface SongDatabaseRepository { 8 | suspend fun addSong(song: Song) 9 | suspend fun addSongs(songs: List) 10 | suspend fun removeSong(song: Song) 11 | suspend fun getSongById(id: String): Song? 12 | fun getAllSongsStream(): Flow> 13 | fun getFavSongsStream(): Flow> 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/domain/repository/SongDatabaseRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.repository 2 | 3 | import app.suhasdissa.vibeyou.data.database.dao.SongsDao 4 | import app.suhasdissa.vibeyou.data.database.entities.SongEntity 5 | import app.suhasdissa.vibeyou.domain.models.primary.Song 6 | import app.suhasdissa.vibeyou.utils.asSong 7 | import app.suhasdissa.vibeyou.utils.asSongEntity 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | class SongDatabaseRepositoryImpl(private val songsDao: SongsDao) : 11 | SongDatabaseRepository { 12 | override suspend fun addSong(song: Song) = songsDao.addSong(song.asSongEntity) 13 | override suspend fun addSongs(songs: List) = 14 | songsDao.addSongs(songs.map { it.asSongEntity }) 15 | 16 | override suspend fun getSongById(id: String): Song? = songsDao.getSongById(id)?.asSong 17 | override fun getAllSongsStream(): Flow> = songsDao.getAllSongsStream() 18 | override fun getFavSongsStream(): Flow> = songsDao.getFavSongsStream() 19 | override suspend fun removeSong(song: Song) = songsDao.removeSong(song.asSongEntity) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/navigation/CustomNavTypes.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.navigation 2 | 3 | import android.os.Bundle 4 | import androidx.navigation.NavType 5 | import app.suhasdissa.vibeyou.domain.models.primary.Album 6 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 7 | import kotlinx.serialization.json.Json 8 | import java.net.URLDecoder 9 | import java.net.URLEncoder 10 | 11 | val AlbumType = object : NavType( 12 | isNullableAllowed = false 13 | ) { 14 | override fun put(bundle: Bundle, key: String, value: Album) { 15 | bundle.putParcelable(key, value) 16 | } 17 | 18 | override fun get(bundle: Bundle, key: String): Album? { 19 | return bundle.getParcelable(key) 20 | } 21 | 22 | override fun parseValue(value: String): Album { 23 | return Json.decodeFromString(URLDecoder.decode(value, "UTF-8")) 24 | } 25 | 26 | override fun serializeAsValue(value: Album): String { 27 | return URLEncoder.encode(Json.encodeToString(Album.serializer(), value), "UTF-8") 28 | } 29 | } 30 | 31 | val ArtistType = object : NavType( 32 | isNullableAllowed = false 33 | ) { 34 | override fun put(bundle: Bundle, key: String, value: Artist) { 35 | bundle.putParcelable(key, value) 36 | } 37 | 38 | override fun get(bundle: Bundle, key: String): Artist? { 39 | return bundle.getParcelable(key) 40 | } 41 | 42 | override fun parseValue(value: String): Artist { 43 | return Json.decodeFromString(URLDecoder.decode(value, "UTF-8")) 44 | } 45 | 46 | override fun serializeAsValue(value: Artist): String { 47 | return URLEncoder.encode(Json.encodeToString(Artist.serializer(), value), "UTF-8") 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/navigation/Destination.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.navigation 2 | 3 | import app.suhasdissa.vibeyou.domain.models.primary.Album 4 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | sealed class Destination { 9 | @Serializable 10 | object OnlineMusic : Destination() 11 | 12 | @Serializable 13 | object LocalMusic : Destination() 14 | 15 | @Serializable 16 | object OnlineSearch : Destination() 17 | 18 | @Serializable 19 | object LocalSearch : Destination() 20 | 21 | @Serializable 22 | object Settings : Destination() 23 | 24 | @Serializable 25 | object About : Destination() 26 | 27 | @Serializable 28 | object NetworkSettings : Destination() 29 | 30 | @Serializable 31 | object DatabaseSettings : Destination() 32 | 33 | @Serializable 34 | object AppearanceSettings : Destination() 35 | 36 | @Serializable 37 | data class Playlists(val album: Album) : Destination() 38 | 39 | @Serializable 40 | data class LocalPlaylists(val album: Album) : Destination() 41 | 42 | @Serializable 43 | data class SavedPlaylists(val album: Album) : Destination() 44 | 45 | @Serializable 46 | data class OnlineArtist(val artist: Artist) : Destination() 47 | 48 | @Serializable 49 | data class LocalArtist(val artist: Artist) : Destination() 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/components/ChipSelect.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.components 2 | 3 | import android.view.SoundEffectConstants 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.lazy.LazyRow 8 | import androidx.compose.foundation.lazy.items 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.FilterChip 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.platform.LocalView 19 | import androidx.compose.ui.unit.dp 20 | 21 | @OptIn(ExperimentalMaterial3Api::class) 22 | @Composable 23 | inline fun > ChipSelector( 24 | crossinline onItemSelected: (T) -> Unit, 25 | defaultValue: T 26 | ) { 27 | val options = enumValues() 28 | var selectedOption by remember { mutableStateOf(defaultValue) } 29 | val view = LocalView.current 30 | LazyRow( 31 | modifier = Modifier.fillMaxWidth(), 32 | horizontalArrangement = Arrangement.spacedBy(8.dp), 33 | contentPadding = PaddingValues(horizontal = 8.dp) 34 | ) { 35 | items(items = options) { 36 | FilterChip(selected = (selectedOption == it), onClick = { 37 | view.playSoundEffect(SoundEffectConstants.CLICK) 38 | selectedOption = it 39 | onItemSelected(it) 40 | }, label = { Text(it.name.replace("_", " ")) }) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/components/IconCard.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.components 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material3.ElevatedCard 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.vector.ImageVector 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Composable 20 | fun IconCard(onClick: () -> Unit, icon: ImageVector, @StringRes name: Int) { 21 | ElevatedCard( 22 | onClick, 23 | Modifier 24 | .size(150.dp) 25 | .aspectRatio(1f) 26 | ) { 27 | Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 28 | Icon( 29 | modifier = Modifier.size(64.dp), 30 | imageVector = icon, 31 | contentDescription = stringResource(name) 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/components/IllustratedMessageScreen.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.components 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.alpha 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.ColorFilter 21 | import androidx.compose.ui.res.painterResource 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.tooling.preview.Preview 24 | import androidx.compose.ui.unit.dp 25 | import app.suhasdissa.vibeyou.R 26 | 27 | @Composable 28 | fun IllustratedMessageScreen( 29 | @DrawableRes image: Int, 30 | @StringRes message: Int? = null, 31 | messageColor: Color = MaterialTheme.colorScheme.error, 32 | action: @Composable () -> Unit = {} 33 | ) { 34 | Column( 35 | Modifier.fillMaxSize(), 36 | horizontalAlignment = Alignment.CenterHorizontally, 37 | verticalArrangement = Arrangement.Center 38 | ) { 39 | message?.let { 40 | Text( 41 | stringResource(message), 42 | style = MaterialTheme.typography.displaySmall, 43 | color = messageColor, 44 | modifier = Modifier.alpha(0.5f) 45 | ) 46 | } 47 | Box( 48 | contentAlignment = Alignment.Center, 49 | modifier = Modifier 50 | .fillMaxWidth() 51 | .height(350.dp) 52 | .alpha(0.3f) 53 | ) { 54 | Image( 55 | modifier = Modifier.size(350.dp), 56 | painter = painterResource(id = R.drawable.blob), 57 | contentDescription = null, 58 | colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.secondaryContainer) 59 | ) 60 | Image( 61 | modifier = Modifier.size(250.dp), 62 | painter = painterResource(id = image), 63 | contentDescription = null, 64 | colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSecondaryContainer) 65 | ) 66 | } 67 | action() 68 | } 69 | } 70 | 71 | @Composable 72 | @Preview(showBackground = true) 73 | private fun IllustratedMsgScreenPreview() { 74 | IllustratedMessageScreen( 75 | image = R.drawable.ic_launcher_monochrome, 76 | message = R.string.something_went_wrong 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/components/LoadingScreen.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | fun LoadingScreen() { 12 | Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 13 | CircularProgressIndicator() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/components/SongList.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.items 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 app.suhasdissa.vibeyou.R 13 | import app.suhasdissa.vibeyou.domain.models.primary.Song 14 | 15 | @Composable 16 | fun SongList( 17 | items: List, 18 | header: @Composable (() -> Unit)? = null, 19 | onClickCard: (song: Song) -> Unit, 20 | onLongPress: (song: Song) -> Unit 21 | ) { 22 | if (items.isEmpty()) { 23 | IllustratedMessageScreen(image = R.drawable.ic_launcher_monochrome) 24 | } 25 | LazyColumn( 26 | Modifier.fillMaxSize(), 27 | horizontalAlignment = Alignment.CenterHorizontally, 28 | verticalArrangement = Arrangement.spacedBy(8.dp), 29 | contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp) 30 | ) { 31 | if (header != null) { 32 | item { header() } 33 | } 34 | items(items = items) { item -> 35 | SongCard( 36 | song = item, 37 | onClickCard = { 38 | onClickCard(item) 39 | }, 40 | onLongPress = { 41 | onLongPress(item) 42 | } 43 | ) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/components/VerticalSlider.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.components 2 | 3 | import androidx.compose.foundation.interaction.MutableInteractionSource 4 | import androidx.compose.material3.Slider 5 | import androidx.compose.material3.SliderColors 6 | import androidx.compose.material3.SliderDefaults 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.TransformOrigin 11 | import androidx.compose.ui.graphics.graphicsLayer 12 | import androidx.compose.ui.layout.layout 13 | import androidx.compose.ui.unit.Constraints 14 | 15 | @Composable 16 | fun VerticalSlider( 17 | value: Float, 18 | onValueChange: (Float) -> Unit, 19 | modifier: Modifier = Modifier, 20 | enabled: Boolean = true, 21 | valueRange: ClosedFloatingPointRange = 0f..1f, 22 | steps: Int = 0, 23 | onValueChangeFinished: (() -> Unit)? = null, 24 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 25 | colors: SliderColors = SliderDefaults.colors() 26 | ) { 27 | Slider( 28 | colors = colors, 29 | interactionSource = interactionSource, 30 | onValueChangeFinished = onValueChangeFinished, 31 | steps = steps, 32 | valueRange = valueRange, 33 | enabled = enabled, 34 | value = value, 35 | onValueChange = onValueChange, 36 | modifier = Modifier 37 | .graphicsLayer { 38 | rotationZ = 270f 39 | transformOrigin = TransformOrigin(0f, 0f) 40 | } 41 | .layout { measurable, constraints -> 42 | val placeable = measurable.measure( 43 | Constraints( 44 | minWidth = constraints.minHeight, 45 | maxWidth = constraints.maxHeight, 46 | minHeight = constraints.minWidth, 47 | maxHeight = constraints.maxHeight, 48 | ) 49 | ) 50 | layout(placeable.height, placeable.width) { 51 | placeable.place(-placeable.width, 0) 52 | } 53 | } 54 | .then(modifier) 55 | ) 56 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/layout/MainAppContent.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.layout 2 | 3 | import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo 4 | import androidx.compose.runtime.Composable 5 | import androidx.navigation.compose.rememberNavController 6 | import androidx.window.core.layout.WindowHeightSizeClass 7 | import androidx.window.core.layout.WindowSizeClass 8 | import androidx.window.core.layout.WindowWidthSizeClass 9 | import app.suhasdissa.vibeyou.presentation.screens.player.model.PlayerViewModel 10 | import app.suhasdissa.vibeyou.presentation.screens.settings.model.SettingsModel 11 | 12 | @Composable 13 | fun MainAppContent( 14 | playerViewModel: PlayerViewModel, 15 | settingsModel: SettingsModel, 16 | windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass 17 | ) { 18 | val navHostController = rememberNavController() 19 | if (windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT) { 20 | when (windowSizeClass.windowWidthSizeClass) { 21 | WindowWidthSizeClass.COMPACT -> { 22 | ModelNavDrawerLayout( 23 | navHostController, 24 | playerViewModel, 25 | settingsModel 26 | ) 27 | } 28 | 29 | WindowWidthSizeClass.MEDIUM -> { 30 | PermanentNavDrawerLayout( 31 | navHostController, 32 | playerViewModel, 33 | settingsModel 34 | ) 35 | } 36 | 37 | WindowWidthSizeClass.EXPANDED -> { 38 | PermanentNavDrawerWithPlayerLayout( 39 | navHostController, 40 | playerViewModel, 41 | settingsModel 42 | ) 43 | } 44 | } 45 | } else { 46 | PermanentNavDrawerLayout( 47 | navHostController, 48 | playerViewModel, 49 | settingsModel, 50 | horizontalPlayer = true 51 | ) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/layout/ModelNavDrawerLayout.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.layout 2 | 3 | import androidx.compose.foundation.layout.consumeWindowInsets 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.DrawerValue 7 | import androidx.compose.material3.ModalNavigationDrawer 8 | import androidx.compose.material3.rememberDrawerState 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.DisposableEffect 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.rememberCoroutineScope 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Modifier 17 | import androidx.navigation.NavController 18 | import androidx.navigation.NavDestination.Companion.hasRoute 19 | import androidx.navigation.NavHostController 20 | import app.suhasdissa.vibeyou.navigation.AppNavHost 21 | import app.suhasdissa.vibeyou.navigation.Destination 22 | import app.suhasdissa.vibeyou.presentation.components.MiniPlayerScaffold 23 | import app.suhasdissa.vibeyou.presentation.components.ModalNavDrawerContent 24 | import app.suhasdissa.vibeyou.presentation.screens.player.model.PlayerViewModel 25 | import app.suhasdissa.vibeyou.presentation.screens.settings.model.SettingsModel 26 | import kotlinx.coroutines.launch 27 | 28 | @Composable 29 | internal fun ModelNavDrawerLayout( 30 | navHostController: NavHostController, 31 | playerViewModel: PlayerViewModel, 32 | settingsModel: SettingsModel 33 | ) { 34 | val drawerState = rememberDrawerState(DrawerValue.Closed) 35 | val scope = rememberCoroutineScope() 36 | var currentDestination by remember { 37 | mutableStateOf(Destination.LocalMusic) 38 | } 39 | 40 | DisposableEffect(Unit) { 41 | val listener = NavController.OnDestinationChangedListener { _, destination, _ -> 42 | listOf( 43 | Destination.LocalMusic, 44 | Destination.OnlineMusic, 45 | Destination.Settings 46 | ).firstOrNull { destination.hasRoute(it::class) } 47 | ?.let { currentDestination = it } 48 | } 49 | navHostController.addOnDestinationChangedListener(listener) 50 | 51 | onDispose { 52 | navHostController.removeOnDestinationChangedListener(listener) 53 | } 54 | } 55 | 56 | ModalNavigationDrawer( 57 | drawerState = drawerState, 58 | gesturesEnabled = drawerState.isOpen, 59 | drawerContent = { 60 | ModalNavDrawerContent( 61 | currentDestination = currentDestination, 62 | onDestinationSelected = { 63 | scope.launch { 64 | drawerState.close() 65 | } 66 | navHostController.popBackStack() 67 | navHostController.navigate(it) 68 | } 69 | ) 70 | } 71 | ) { 72 | MiniPlayerScaffold(playerViewModel) { pV -> 73 | AppNavHost( 74 | modifier = Modifier 75 | .fillMaxSize() 76 | .consumeWindowInsets(pV) 77 | .padding(pV), 78 | navHostController = navHostController, 79 | onDrawerOpen = { 80 | scope.launch { 81 | drawerState.open() 82 | } 83 | }, 84 | playerViewModel, 85 | settingsModel 86 | ) 87 | } 88 | 89 | } 90 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/layout/PermanentNavDrawerLayout.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.layout 2 | 3 | import androidx.compose.foundation.layout.consumeWindowInsets 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.PermanentNavigationDrawer 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.DisposableEffect 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.navigation.NavController 15 | import androidx.navigation.NavDestination.Companion.hasRoute 16 | import androidx.navigation.NavHostController 17 | import app.suhasdissa.vibeyou.navigation.AppNavHost 18 | import app.suhasdissa.vibeyou.navigation.Destination 19 | import app.suhasdissa.vibeyou.presentation.components.MiniPlayerScaffold 20 | import app.suhasdissa.vibeyou.presentation.components.PermanentNavDrawerContent 21 | import app.suhasdissa.vibeyou.presentation.screens.player.model.PlayerViewModel 22 | import app.suhasdissa.vibeyou.presentation.screens.settings.model.SettingsModel 23 | 24 | @Composable 25 | internal fun PermanentNavDrawerLayout( 26 | navHostController: NavHostController, 27 | playerViewModel: PlayerViewModel, 28 | settingsModel: SettingsModel, 29 | horizontalPlayer: Boolean = false 30 | ) { 31 | var currentDestination by remember { 32 | mutableStateOf(Destination.LocalMusic) 33 | } 34 | 35 | DisposableEffect(Unit) { 36 | val listener = NavController.OnDestinationChangedListener { _, destination, _ -> 37 | listOf( 38 | Destination.LocalMusic, 39 | Destination.OnlineMusic, 40 | Destination.Settings 41 | ).firstOrNull { destination.hasRoute(it::class) } 42 | ?.let { currentDestination = it } 43 | } 44 | navHostController.addOnDestinationChangedListener(listener) 45 | 46 | onDispose { 47 | navHostController.removeOnDestinationChangedListener(listener) 48 | } 49 | } 50 | 51 | PermanentNavigationDrawer(drawerContent = { 52 | PermanentNavDrawerContent( 53 | currentDestination = currentDestination, 54 | onDestinationSelected = { 55 | currentDestination = it 56 | navHostController.popBackStack() 57 | navHostController.navigate(it) 58 | } 59 | ) 60 | }) { 61 | MiniPlayerScaffold(playerViewModel, horizontalPlayer = horizontalPlayer) { pV -> 62 | AppNavHost( 63 | modifier = Modifier 64 | .fillMaxSize() 65 | .consumeWindowInsets(pV) 66 | .padding(pV), 67 | navHostController = navHostController, 68 | onDrawerOpen = null, 69 | playerViewModel, 70 | settingsModel 71 | ) 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/album/components/AlbumCard.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.album.components 2 | 3 | import android.view.SoundEffectConstants 4 | import androidx.compose.foundation.ExperimentalFoundationApi 5 | import androidx.compose.foundation.combinedClickable 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.aspectRatio 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType 20 | import androidx.compose.ui.layout.ContentScale 21 | import androidx.compose.ui.platform.LocalHapticFeedback 22 | import androidx.compose.ui.platform.LocalView 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.res.stringResource 25 | import androidx.compose.ui.text.style.TextAlign 26 | import androidx.compose.ui.text.style.TextOverflow 27 | import androidx.compose.ui.unit.dp 28 | import app.suhasdissa.vibeyou.R 29 | import app.suhasdissa.vibeyou.domain.models.primary.Album 30 | import coil.compose.AsyncImage 31 | 32 | @OptIn(ExperimentalFoundationApi::class) 33 | @Composable 34 | fun AlbumCard( 35 | album: Album, 36 | onClickCard: () -> Unit, 37 | onLongPress: () -> Unit 38 | ) { 39 | val view = LocalView.current 40 | val haptic = LocalHapticFeedback.current 41 | Column( 42 | Modifier 43 | .fillMaxWidth() 44 | .combinedClickable( 45 | onClick = { 46 | view.playSoundEffect(SoundEffectConstants.CLICK) 47 | onClickCard() 48 | }, 49 | onLongClick = { 50 | haptic.performHapticFeedback(HapticFeedbackType.LongPress) 51 | onLongPress() 52 | } 53 | ), 54 | horizontalAlignment = Alignment.CenterHorizontally 55 | ) { 56 | AsyncImage( 57 | modifier = Modifier 58 | .size(148.dp) 59 | .aspectRatio(1f) 60 | .clip(RoundedCornerShape(16.dp)), 61 | model = album.thumbnailUri, 62 | contentDescription = stringResource(R.string.album_art), 63 | contentScale = ContentScale.Crop, 64 | error = painterResource(id = R.drawable.music_placeholder) 65 | ) 66 | Spacer(Modifier.height(8.dp)) 67 | Text( 68 | album.title, 69 | style = MaterialTheme.typography.titleSmall, 70 | maxLines = 1, 71 | overflow = TextOverflow.Ellipsis, 72 | textAlign = TextAlign.Center 73 | ) 74 | Text( 75 | album.artistsText, 76 | style = MaterialTheme.typography.bodyMedium, 77 | maxLines = 1, 78 | overflow = TextOverflow.Ellipsis, 79 | textAlign = TextAlign.Center 80 | ) 81 | Spacer(Modifier.height(8.dp)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/album/components/AlbumList.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.album.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.lazy.grid.GridCells 7 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 8 | import androidx.compose.foundation.lazy.grid.items 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | import app.suhasdissa.vibeyou.R 13 | import app.suhasdissa.vibeyou.domain.models.primary.Album 14 | import app.suhasdissa.vibeyou.presentation.components.IllustratedMessageScreen 15 | 16 | @Composable 17 | fun AlbumList( 18 | items: List, 19 | onClickCard: (playlist: Album) -> Unit, 20 | onLongPress: (playlist: Album) -> Unit 21 | ) { 22 | if (items.isEmpty()) { 23 | IllustratedMessageScreen(image = R.drawable.ic_launcher_monochrome) 24 | } 25 | LazyVerticalGrid( 26 | columns = GridCells.Adaptive(180.dp), 27 | modifier = Modifier.fillMaxSize(), 28 | horizontalArrangement = Arrangement.spacedBy(8.dp), 29 | verticalArrangement = Arrangement.spacedBy(8.dp), 30 | contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp) 31 | ) { 32 | items(items = items) { item -> 33 | AlbumCard( 34 | album = item, 35 | onClickCard = { onClickCard(item) }, 36 | onLongPress = { onLongPress(item) } 37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/album/model/LocalPlaylistViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.album.model 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.SavedStateHandle 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.ViewModelProvider 10 | import androidx.lifecycle.createSavedStateHandle 11 | import androidx.lifecycle.viewModelScope 12 | import androidx.lifecycle.viewmodel.initializer 13 | import androidx.lifecycle.viewmodel.viewModelFactory 14 | import androidx.navigation.toRoute 15 | import app.suhasdissa.vibeyou.MellowMusicApplication 16 | import app.suhasdissa.vibeyou.backend.repository.LocalMusicRepository 17 | import app.suhasdissa.vibeyou.domain.models.primary.Album 18 | import app.suhasdissa.vibeyou.navigation.Destination 19 | import app.suhasdissa.vibeyou.presentation.screens.onlinesearch.model.state.AlbumInfoState 20 | import kotlinx.coroutines.launch 21 | 22 | class LocalPlaylistViewModel( 23 | private val musicRepository: LocalMusicRepository, 24 | savedStateHandle: SavedStateHandle 25 | ) : ViewModel() { 26 | 27 | val playlist = savedStateHandle.toRoute() 28 | 29 | var albumInfoState: AlbumInfoState by mutableStateOf(AlbumInfoState.Loading) 30 | private set 31 | 32 | init { 33 | getAlbumInfo(playlist.album) 34 | } 35 | 36 | private fun getAlbumInfo(album: Album) { 37 | viewModelScope.launch { 38 | albumInfoState = AlbumInfoState.Loading 39 | albumInfoState = try { 40 | AlbumInfoState.Success( 41 | album, 42 | musicRepository.getAlbumInfo(album.id.toLong()) 43 | ) 44 | } catch (e: Exception) { 45 | Log.e("Playlist Info", e.toString()) 46 | AlbumInfoState.Error 47 | } 48 | } 49 | } 50 | 51 | companion object { 52 | val Factory: ViewModelProvider.Factory = viewModelFactory { 53 | initializer { 54 | val application = 55 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 56 | LocalPlaylistViewModel( 57 | application.container.localMusicRepository, 58 | this.createSavedStateHandle() 59 | ) 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/album/model/NewPlaylistViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.album.model 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.lifecycle.viewmodel.initializer 7 | import androidx.lifecycle.viewmodel.viewModelFactory 8 | import app.suhasdissa.vibeyou.MellowMusicApplication 9 | import app.suhasdissa.vibeyou.backend.repository.PlaylistRepository 10 | import app.suhasdissa.vibeyou.domain.models.primary.Album 11 | import app.suhasdissa.vibeyou.domain.models.primary.Song 12 | import kotlinx.coroutines.launch 13 | 14 | class NewPlaylistViewModel(private val playlistRepository: PlaylistRepository) : ViewModel() { 15 | fun newPlaylistWithSongs(album: Album, songs: List) { 16 | viewModelScope.launch { 17 | playlistRepository.newPlaylistWithSongs(album, songs) 18 | } 19 | } 20 | 21 | companion object { 22 | val Factory: ViewModelProvider.Factory = viewModelFactory { 23 | initializer { 24 | val application = 25 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 26 | NewPlaylistViewModel( 27 | application.container.playlistRepository 28 | ) 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/album/model/OnlinePlaylistViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.album.model 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.SavedStateHandle 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.ViewModelProvider 10 | import androidx.lifecycle.createSavedStateHandle 11 | import androidx.lifecycle.viewModelScope 12 | import androidx.lifecycle.viewmodel.initializer 13 | import androidx.lifecycle.viewmodel.viewModelFactory 14 | import androidx.navigation.toRoute 15 | import app.suhasdissa.vibeyou.MellowMusicApplication 16 | import app.suhasdissa.vibeyou.backend.repository.PipedMusicRepository 17 | import app.suhasdissa.vibeyou.domain.models.primary.Album 18 | import app.suhasdissa.vibeyou.navigation.Destination 19 | import app.suhasdissa.vibeyou.presentation.screens.onlinesearch.model.state.AlbumInfoState 20 | import app.suhasdissa.vibeyou.utils.asSong 21 | import kotlinx.coroutines.launch 22 | 23 | class OnlinePlaylistViewModel( 24 | private val musicRepository: PipedMusicRepository, 25 | savedStateHandle: SavedStateHandle 26 | ) : ViewModel() { 27 | 28 | val playlist = savedStateHandle.toRoute() 29 | 30 | var albumInfoState: AlbumInfoState by mutableStateOf(AlbumInfoState.Loading) 31 | private set 32 | 33 | init { 34 | getPlaylistInfo(playlist.album) 35 | } 36 | 37 | private fun getPlaylistInfo(playlist: Album) { 38 | viewModelScope.launch { 39 | albumInfoState = AlbumInfoState.Loading 40 | albumInfoState = try { 41 | val info = musicRepository.getPlaylistInfo(playlist.id) 42 | AlbumInfoState.Success( 43 | playlist, 44 | info.relatedStreams.map { it.asSong } 45 | ) 46 | } catch (e: Exception) { 47 | Log.e("Playlist Info", e.toString()) 48 | AlbumInfoState.Error 49 | } 50 | } 51 | } 52 | 53 | companion object { 54 | val Factory: ViewModelProvider.Factory = viewModelFactory { 55 | initializer { 56 | val application = 57 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 58 | OnlinePlaylistViewModel( 59 | application.container.pipedMusicRepository, 60 | this.createSavedStateHandle() 61 | ) 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/artist/ArtistScreen.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.artist 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.material3.Scaffold 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TopAppBar 9 | import androidx.compose.material3.TopAppBarDefaults 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.text.style.TextOverflow 14 | import app.suhasdissa.vibeyou.R 15 | import app.suhasdissa.vibeyou.domain.models.primary.Album 16 | import app.suhasdissa.vibeyou.presentation.components.IllustratedMessageScreen 17 | import app.suhasdissa.vibeyou.presentation.components.LoadingScreen 18 | import app.suhasdissa.vibeyou.presentation.screens.album.components.AlbumList 19 | import app.suhasdissa.vibeyou.presentation.screens.onlinesearch.model.state.ArtistInfoState 20 | 21 | @OptIn(ExperimentalMaterial3Api::class) 22 | @Composable 23 | fun ArtistScreen( 24 | onClickAlbum: (Album) -> Unit, 25 | state: ArtistInfoState 26 | ) { 27 | Scaffold(topBar = { 28 | TopAppBar(title = { 29 | when (state) { 30 | is ArtistInfoState.Success -> Text( 31 | text = state.artist.artistsText, 32 | maxLines = 1, 33 | overflow = TextOverflow.Ellipsis 34 | ) 35 | 36 | else -> Text(stringResource(R.string.artist)) 37 | } 38 | }, scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()) 39 | }) { pV -> 40 | Column(Modifier.padding(pV)) { 41 | when (state) { 42 | ArtistInfoState.Error -> IllustratedMessageScreen( 43 | image = R.drawable.ic_launcher_monochrome, 44 | message = R.string.something_went_wrong 45 | ) 46 | 47 | ArtistInfoState.Loading -> LoadingScreen() 48 | is ArtistInfoState.Success -> { 49 | AlbumList(items = state.playlists, onClickCard = { 50 | onClickAlbum.invoke(it) 51 | }, onLongPress = { 52 | }) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/artist/components/ArtistCard.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.artist.components 2 | 3 | import android.view.SoundEffectConstants 4 | import androidx.compose.foundation.ExperimentalFoundationApi 5 | import androidx.compose.foundation.combinedClickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.aspectRatio 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType 21 | import androidx.compose.ui.layout.ContentScale 22 | import androidx.compose.ui.platform.LocalHapticFeedback 23 | import androidx.compose.ui.platform.LocalView 24 | import androidx.compose.ui.res.painterResource 25 | import androidx.compose.ui.res.stringResource 26 | import androidx.compose.ui.text.style.TextOverflow 27 | import androidx.compose.ui.unit.dp 28 | import app.suhasdissa.vibeyou.R 29 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 30 | import coil.compose.AsyncImage 31 | 32 | @OptIn(ExperimentalFoundationApi::class) 33 | @Composable 34 | fun ArtistCard( 35 | artist: Artist, 36 | onClickCard: () -> Unit, 37 | onLongPress: () -> Unit 38 | ) { 39 | val view = LocalView.current 40 | val haptic = LocalHapticFeedback.current 41 | Row( 42 | Modifier 43 | .fillMaxWidth() 44 | .combinedClickable( 45 | onClick = { 46 | view.playSoundEffect(SoundEffectConstants.CLICK) 47 | onClickCard() 48 | }, 49 | onLongClick = { 50 | haptic.performHapticFeedback(HapticFeedbackType.LongPress) 51 | onLongPress() 52 | } 53 | ), 54 | verticalAlignment = Alignment.CenterVertically 55 | ) { 56 | AsyncImage( 57 | modifier = Modifier 58 | .size(80.dp) 59 | .padding(8.dp) 60 | .aspectRatio(1f) 61 | .clip(CircleShape), 62 | model = artist.thumbnailUri, 63 | contentDescription = stringResource(R.string.artist_avatar), 64 | contentScale = ContentScale.Crop, 65 | error = painterResource(id = R.drawable.music_placeholder) 66 | ) 67 | Column( 68 | Modifier 69 | .weight(1f) 70 | .padding(8.dp) 71 | ) { 72 | Text( 73 | artist.artistsText, 74 | style = MaterialTheme.typography.titleMedium, 75 | maxLines = 1, 76 | overflow = TextOverflow.Ellipsis 77 | ) 78 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 79 | artist.numberOfAlbums?.let { 80 | Text( 81 | text = "${stringResource(id = R.string.albums)} $it", 82 | style = MaterialTheme.typography.bodyMedium 83 | ) 84 | } 85 | artist.numberOfTracks?.let { 86 | Text( 87 | text = "${stringResource(id = R.string.songs)} $it", 88 | style = MaterialTheme.typography.bodyMedium 89 | ) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/artist/components/ArtistList.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.artist.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.foundation.lazy.rememberLazyListState 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | import app.suhasdissa.vibeyou.R 13 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 14 | import app.suhasdissa.vibeyou.presentation.components.IllustratedMessageScreen 15 | 16 | @Composable 17 | fun ArtistList( 18 | items: List, 19 | onClickCard: (artist: Artist) -> Unit, 20 | onLongPress: (artist: Artist) -> Unit 21 | ) { 22 | if (items.isEmpty()) { 23 | IllustratedMessageScreen(image = R.drawable.ic_launcher_monochrome) 24 | } 25 | val state = rememberLazyListState() 26 | // LazyColumnScrollbar( 27 | // listState = state, 28 | // thumbColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), 29 | // thumbSelectedColor = MaterialTheme.colorScheme.primary, 30 | // thickness = 8.dp 31 | // ) { 32 | LazyColumn( 33 | modifier = Modifier.fillMaxSize(), 34 | state = state, 35 | verticalArrangement = Arrangement.spacedBy(8.dp), 36 | contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp) 37 | ) { 38 | items(items = items) { item -> 39 | ArtistCard( 40 | artist = item, 41 | onClickCard = { onClickCard(item) }, 42 | onLongPress = { onLongPress(item) } 43 | ) 44 | } 45 | } 46 | // } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/artist/model/LocalArtistViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.artist.model 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.SavedStateHandle 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.ViewModelProvider 10 | import androidx.lifecycle.createSavedStateHandle 11 | import androidx.lifecycle.viewModelScope 12 | import androidx.lifecycle.viewmodel.initializer 13 | import androidx.lifecycle.viewmodel.viewModelFactory 14 | import androidx.navigation.toRoute 15 | import app.suhasdissa.vibeyou.MellowMusicApplication 16 | import app.suhasdissa.vibeyou.backend.repository.LocalMusicRepository 17 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 18 | import app.suhasdissa.vibeyou.navigation.Destination 19 | import app.suhasdissa.vibeyou.presentation.screens.onlinesearch.model.state.ArtistInfoState 20 | import kotlinx.coroutines.launch 21 | 22 | class LocalArtistViewModel( 23 | private val musicRepository: LocalMusicRepository, 24 | savedStateHandle: SavedStateHandle 25 | ) : ViewModel() { 26 | 27 | val artist = savedStateHandle.toRoute() 28 | var artistInfoState: ArtistInfoState by mutableStateOf(ArtistInfoState.Loading) 29 | private set 30 | 31 | init { 32 | getArtistInfo(artist.artist) 33 | } 34 | 35 | private fun getArtistInfo(artist: Artist) { 36 | viewModelScope.launch { 37 | artistInfoState = ArtistInfoState.Loading 38 | artistInfoState = try { 39 | ArtistInfoState.Success( 40 | artist, 41 | musicRepository.getArtistInfo(artist.artistsText) 42 | ) 43 | } catch (e: Exception) { 44 | Log.e("Artist Info", e.toString()) 45 | ArtistInfoState.Error 46 | } 47 | } 48 | } 49 | 50 | companion object { 51 | val Factory: ViewModelProvider.Factory = viewModelFactory { 52 | initializer { 53 | val application = 54 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 55 | LocalArtistViewModel( 56 | application.container.localMusicRepository, 57 | this.createSavedStateHandle() 58 | ) 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/artist/model/OnlineArtistViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.artist.model 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.core.net.toUri 8 | import androidx.lifecycle.SavedStateHandle 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.ViewModelProvider 11 | import androidx.lifecycle.createSavedStateHandle 12 | import androidx.lifecycle.viewModelScope 13 | import androidx.lifecycle.viewmodel.initializer 14 | import androidx.lifecycle.viewmodel.viewModelFactory 15 | import androidx.navigation.toRoute 16 | import app.suhasdissa.vibeyou.MellowMusicApplication 17 | import app.suhasdissa.vibeyou.backend.repository.PipedMusicRepository 18 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 19 | import app.suhasdissa.vibeyou.navigation.Destination 20 | import app.suhasdissa.vibeyou.presentation.screens.onlinesearch.model.state.ArtistInfoState 21 | import kotlinx.coroutines.launch 22 | 23 | class OnlineArtistViewModel( 24 | private val musicRepository: PipedMusicRepository, 25 | savedStateHandle: SavedStateHandle 26 | ) : ViewModel() { 27 | val artist = savedStateHandle.toRoute() 28 | var artistInfoState: ArtistInfoState by mutableStateOf(ArtistInfoState.Loading) 29 | private set 30 | 31 | init { 32 | getArtistInfo(artist.artist) 33 | } 34 | 35 | private fun getArtistInfo(artist: Artist) { 36 | viewModelScope.launch { 37 | artistInfoState = ArtistInfoState.Loading 38 | artistInfoState = try { 39 | val info = musicRepository.getChannelInfo(artist.id) 40 | val playlists = musicRepository.getChannelPlaylists(artist.id, info.tabs) 41 | ArtistInfoState.Success( 42 | artist.copy( 43 | thumbnailUri = info.avatarUrl?.toUri(), 44 | description = info.description 45 | ), 46 | playlists 47 | ) 48 | } catch (e: Exception) { 49 | Log.e("Playlist Info", e.toString()) 50 | ArtistInfoState.Error 51 | } 52 | } 53 | } 54 | 55 | companion object { 56 | val Factory: ViewModelProvider.Factory = viewModelFactory { 57 | initializer { 58 | val application = 59 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 60 | OnlineArtistViewModel( 61 | application.container.pipedMusicRepository, 62 | this.createSavedStateHandle() 63 | ) 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/localmusic/components/SortOrderDialog.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.localmusic.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.material3.AlertDialog 8 | import androidx.compose.material3.Checkbox 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextButton 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.stringResource 19 | import app.suhasdissa.vibeyou.R 20 | import app.suhasdissa.vibeyou.presentation.components.ChipSelector 21 | 22 | enum class SortOrder { 23 | Alphabetic, 24 | Creation_Date, 25 | Date_Added, 26 | Artist_Name 27 | } 28 | 29 | @Composable 30 | fun SortOrderDialog( 31 | onDismissRequest: () -> Unit, 32 | onSortOrderChange: (order: SortOrder, reverse: Boolean) -> Unit, 33 | defaultSortOrder: SortOrder, 34 | defaultReverse: Boolean 35 | ) { 36 | var sortOrder by remember { 37 | mutableStateOf(defaultSortOrder) 38 | } 39 | var reverse by remember { 40 | mutableStateOf(defaultReverse) 41 | } 42 | 43 | AlertDialog( 44 | onDismissRequest = onDismissRequest, 45 | title = { 46 | Text(stringResource(R.string.sort_order)) 47 | }, 48 | text = { 49 | Column { 50 | ChipSelector( 51 | onItemSelected = { 52 | sortOrder = it 53 | }, 54 | defaultValue = sortOrder 55 | ) 56 | val interactionSource = remember { MutableInteractionSource() } 57 | Row( 58 | verticalAlignment = Alignment.CenterVertically, 59 | modifier = Modifier 60 | .clickable(interactionSource = interactionSource, indication = null) { 61 | reverse = !reverse 62 | } 63 | ) { 64 | Checkbox(checked = reverse, onCheckedChange = { reverse = it }) 65 | Text(text = stringResource(R.string.reversed)) 66 | } 67 | } 68 | }, 69 | confirmButton = { 70 | TextButton( 71 | onClick = { 72 | onSortOrderChange(sortOrder, reverse) 73 | onDismissRequest() 74 | } 75 | ) { 76 | Text(stringResource(R.string.ok)) 77 | } 78 | }, 79 | dismissButton = { 80 | TextButton(onClick = onDismissRequest) { 81 | Text(stringResource(R.string.cancel)) 82 | } 83 | } 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/localmusic/model/LocalSongViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.localmusic.model 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.ViewModelProvider 9 | import androidx.lifecycle.viewModelScope 10 | import androidx.lifecycle.viewmodel.initializer 11 | import androidx.lifecycle.viewmodel.viewModelFactory 12 | import app.suhasdissa.vibeyou.MellowMusicApplication 13 | import app.suhasdissa.vibeyou.backend.repository.LocalMusicRepository 14 | import app.suhasdissa.vibeyou.domain.models.primary.Album 15 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 16 | import app.suhasdissa.vibeyou.domain.models.primary.Song 17 | import app.suhasdissa.vibeyou.presentation.screens.localmusic.components.SortOrder 18 | import app.suhasdissa.vibeyou.utils.Pref 19 | import kotlinx.coroutines.launch 20 | 21 | class LocalSongViewModel(private val musicRepository: LocalMusicRepository) : ViewModel() { 22 | var songs by mutableStateOf(listOf()) 23 | var albums by mutableStateOf(listOf()) 24 | var artists by mutableStateOf(listOf()) 25 | 26 | var songsSortOrder = Pref.sharedPreferences.getString(Pref.latestSongsSortOrderKey, null)?.let { 27 | runCatching { SortOrder.valueOf(it) }.getOrNull() 28 | } ?: SortOrder.Alphabetic 29 | var reverseSongs = Pref.sharedPreferences.getBoolean(Pref.latestReverseSongsPrefKey, false) 30 | 31 | init { 32 | viewModelScope.launch { 33 | try { 34 | songs = musicRepository.getAllSongs() 35 | updateSongsSortOrder() 36 | } catch (e: Exception) { 37 | Log.e("Get All Songs", e.message, e) 38 | } 39 | } 40 | viewModelScope.launch { 41 | try { 42 | albums = musicRepository.getAllAlbums() 43 | } catch (e: Exception) { 44 | Log.e("Get All Albums", e.message, e) 45 | } 46 | } 47 | viewModelScope.launch { 48 | try { 49 | artists = musicRepository.getAllArtists() 50 | } catch (e: Exception) { 51 | Log.e("Get All Artists", e.message, e) 52 | } 53 | } 54 | } 55 | 56 | fun updateSongsSortOrder() { 57 | val sortedSongs = when (songsSortOrder) { 58 | SortOrder.Alphabetic -> songs.sortedBy { it.title.lowercase() } 59 | SortOrder.Creation_Date -> songs.sortedBy { it.creationDate } 60 | SortOrder.Date_Added -> songs.sortedBy { it.dateAdded } 61 | SortOrder.Artist_Name -> songs.sortedBy { it.artistsText.orEmpty().lowercase() } 62 | } 63 | songs = if (reverseSongs) sortedSongs.reversed() else sortedSongs 64 | } 65 | 66 | companion object { 67 | val Factory: ViewModelProvider.Factory = viewModelFactory { 68 | initializer { 69 | val application = 70 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 71 | LocalSongViewModel( 72 | application.container.localMusicRepository 73 | ) 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/onlinemusic/components/SongsScreen.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.onlinemusic.components 2 | 3 | import android.view.SoundEffectConstants 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.rounded.PlayArrow 12 | import androidx.compose.material.icons.rounded.Shuffle 13 | import androidx.compose.material3.FloatingActionButton 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.Scaffold 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.collectAsState 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.platform.LocalView 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import androidx.lifecycle.viewmodel.compose.viewModel 23 | import app.suhasdissa.vibeyou.R 24 | import app.suhasdissa.vibeyou.presentation.components.SongListView 25 | import app.suhasdissa.vibeyou.presentation.screens.onlinemusic.model.SongViewModel 26 | import app.suhasdissa.vibeyou.presentation.screens.player.model.PlayerViewModel 27 | import app.suhasdissa.vibeyou.utils.asSong 28 | 29 | @Composable 30 | fun SongsScreen( 31 | showFavourites: Boolean, 32 | playerViewModel: PlayerViewModel, 33 | songViewModel: SongViewModel = viewModel(factory = SongViewModel.Factory) 34 | ) { 35 | val view = LocalView.current 36 | val songs = 37 | (if (showFavourites) songViewModel.favSongs.collectAsState() else songViewModel.songs.collectAsState()).value.map { 38 | it.asSong 39 | } 40 | Scaffold(floatingActionButton = { 41 | Row { 42 | FloatingActionButton(onClick = { 43 | view.playSoundEffect(SoundEffectConstants.CLICK) 44 | playerViewModel.playSongs(songs) 45 | }) { 46 | Icon( 47 | imageVector = Icons.Rounded.PlayArrow, 48 | contentDescription = stringResource(R.string.play_all) 49 | ) 50 | } 51 | 52 | Spacer(modifier = Modifier.width(12.dp)) 53 | 54 | FloatingActionButton(onClick = { 55 | view.playSoundEffect(SoundEffectConstants.CLICK) 56 | playerViewModel.playSongs(songs, shuffle = true) 57 | }) { 58 | Icon( 59 | imageVector = Icons.Rounded.Shuffle, 60 | contentDescription = stringResource(R.string.shuffle) 61 | ) 62 | } 63 | } 64 | }) { innerPadding -> 65 | Column( 66 | Modifier 67 | .fillMaxSize() 68 | .padding(innerPadding) 69 | ) { 70 | SongListView(songs, playerViewModel) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/onlinemusic/model/SongOptionsViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.onlinemusic.model 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.lifecycle.viewmodel.initializer 7 | import androidx.lifecycle.viewmodel.viewModelFactory 8 | import app.suhasdissa.vibeyou.MellowMusicApplication 9 | import app.suhasdissa.vibeyou.backend.repository.SongDatabaseRepository 10 | import app.suhasdissa.vibeyou.domain.models.primary.Song 11 | import kotlinx.coroutines.launch 12 | 13 | class SongOptionsViewModel(private val songDatabaseRepository: SongDatabaseRepository) : 14 | ViewModel() { 15 | fun removeSong(song: Song) { 16 | viewModelScope.launch { 17 | songDatabaseRepository.removeSong(song) 18 | } 19 | } 20 | 21 | fun toggleFavourite(id: String) { 22 | viewModelScope.launch { 23 | val song = songDatabaseRepository.getSongById(id) 24 | song ?: return@launch 25 | song.let { 26 | songDatabaseRepository.addSong(it.toggleLike()) 27 | } 28 | } 29 | } 30 | 31 | companion object { 32 | val Factory: ViewModelProvider.Factory = viewModelFactory { 33 | initializer { 34 | val application = 35 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 36 | SongOptionsViewModel( 37 | application.container.songDatabaseRepository 38 | ) 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/onlinemusic/model/SongViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.onlinemusic.model 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.lifecycle.viewmodel.initializer 7 | import androidx.lifecycle.viewmodel.viewModelFactory 8 | import app.suhasdissa.vibeyou.MellowMusicApplication 9 | import app.suhasdissa.vibeyou.backend.repository.SongDatabaseRepository 10 | import kotlinx.coroutines.flow.SharingStarted 11 | import kotlinx.coroutines.flow.stateIn 12 | 13 | class SongViewModel(private val songDatabaseRepository: SongDatabaseRepository) : ViewModel() { 14 | val songs = songDatabaseRepository.getAllSongsStream().stateIn( 15 | scope = viewModelScope, 16 | started = SharingStarted.WhileSubscribed(5000L), 17 | initialValue = listOf() 18 | ) 19 | 20 | val favSongs = songDatabaseRepository.getFavSongsStream().stateIn( 21 | scope = viewModelScope, 22 | started = SharingStarted.WhileSubscribed(5000L), 23 | initialValue = listOf() 24 | ) 25 | 26 | companion object { 27 | val Factory: ViewModelProvider.Factory = viewModelFactory { 28 | initializer { 29 | val application = 30 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 31 | SongViewModel( 32 | application.container.songDatabaseRepository 33 | ) 34 | } 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/onlinesearch/model/state/AlbumInfoState.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.onlinesearch.model.state 2 | 3 | import app.suhasdissa.vibeyou.domain.models.primary.Album 4 | import app.suhasdissa.vibeyou.domain.models.primary.Song 5 | 6 | sealed interface AlbumInfoState { 7 | object Loading : AlbumInfoState 8 | object Error : AlbumInfoState 9 | data class Success( 10 | val album: Album, 11 | val songs: List 12 | 13 | ) : AlbumInfoState 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/onlinesearch/model/state/ArtistInfoState.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.onlinesearch.model.state 2 | 3 | import app.suhasdissa.vibeyou.domain.models.primary.Album 4 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 5 | 6 | sealed interface ArtistInfoState { 7 | object Loading : ArtistInfoState 8 | object Error : ArtistInfoState 9 | data class Success( 10 | val artist: Artist, 11 | val playlists: List 12 | 13 | ) : ArtistInfoState 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/onlinesearch/model/state/SearchState.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.onlinesearch.model.state 2 | 3 | import app.suhasdissa.vibeyou.domain.models.primary.Album 4 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 5 | import app.suhasdissa.vibeyou.domain.models.primary.Song 6 | 7 | sealed interface SearchState { 8 | sealed interface Success : SearchState { 9 | data class Songs(val items: List) : Success 10 | data class Playlists(val items: List) : Success 11 | data class Artists(val items: List) : Success 12 | } 13 | 14 | data class Error(val error: String) : SearchState 15 | object Loading : SearchState 16 | object Empty : SearchState 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/player/components/QueueSheet.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.player.components 2 | 3 | import android.view.SoundEffectConstants 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.rounded.ExpandMore 8 | import androidx.compose.material.icons.rounded.MoreVert 9 | import androidx.compose.material3.CenterAlignedTopAppBar 10 | import androidx.compose.material3.DropdownMenu 11 | import androidx.compose.material3.DropdownMenuItem 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.HorizontalDivider 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.IconButton 16 | import androidx.compose.material3.ModalBottomSheet 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.rememberModalBottomSheetState 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.rememberCoroutineScope 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.platform.LocalView 27 | import androidx.compose.ui.res.stringResource 28 | import androidx.compose.ui.unit.dp 29 | import app.suhasdissa.vibeyou.R 30 | import app.suhasdissa.vibeyou.presentation.screens.player.model.PlayerViewModel 31 | import kotlinx.coroutines.launch 32 | 33 | @OptIn(ExperimentalMaterial3Api::class) 34 | @Composable 35 | fun QueueSheet( 36 | onDismissRequest: () -> Unit, 37 | playerViewModel: PlayerViewModel 38 | ) { 39 | val playerSheetState = rememberModalBottomSheetState( 40 | skipPartiallyExpanded = true 41 | ) 42 | val scope = rememberCoroutineScope() 43 | val view = LocalView.current 44 | ModalBottomSheet( 45 | onDismissRequest = onDismissRequest, 46 | sheetState = playerSheetState, 47 | shape = RoundedCornerShape(8.dp), 48 | tonalElevation = 0.dp, 49 | dragHandle = null 50 | ) { 51 | CenterAlignedTopAppBar(navigationIcon = { 52 | IconButton({ 53 | view.playSoundEffect(SoundEffectConstants.CLICK) 54 | scope.launch { 55 | playerSheetState.hide() 56 | }.invokeOnCompletion { 57 | onDismissRequest() 58 | } 59 | }) { 60 | Icon( 61 | Icons.Rounded.ExpandMore, 62 | contentDescription = stringResource(R.string.close_queue) 63 | ) 64 | } 65 | }, title = { Text(stringResource(R.string.player_queue)) }, actions = { 66 | var showDropdown by remember { mutableStateOf(false) } 67 | IconButton({ 68 | view.playSoundEffect(SoundEffectConstants.CLICK) 69 | showDropdown = true 70 | }) { 71 | Icon( 72 | Icons.Rounded.MoreVert, 73 | contentDescription = stringResource(R.string.close_queue) 74 | ) 75 | } 76 | DropdownMenu(expanded = showDropdown, onDismissRequest = { showDropdown = false }) { 77 | DropdownMenuItem(text = { 78 | Text(text = stringResource(R.string.clear_queue)) 79 | }, onClick = { 80 | playerViewModel.controller?.clearMediaItems() 81 | scope.launch { 82 | playerSheetState.hide() 83 | }.invokeOnCompletion { 84 | onDismissRequest() 85 | } 86 | }) 87 | } 88 | }) 89 | HorizontalDivider(Modifier.fillMaxWidth()) 90 | playerViewModel.controller?.let { controller -> 91 | Queue(controller) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/playlists/PlaylistsScreen.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.playlists 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | import androidx.lifecycle.viewmodel.compose.viewModel 10 | import app.suhasdissa.vibeyou.domain.models.primary.Album 11 | import app.suhasdissa.vibeyou.navigation.Destination 12 | import app.suhasdissa.vibeyou.presentation.screens.album.components.AlbumList 13 | import app.suhasdissa.vibeyou.presentation.screens.playlists.components.PlaylistOptionsSheet 14 | import app.suhasdissa.vibeyou.presentation.screens.playlists.model.PlaylistViewModel 15 | import app.suhasdissa.vibeyou.utils.asAlbum 16 | 17 | @Composable 18 | fun PlaylistsScreen( 19 | onNavigate: (Destination) -> Unit, 20 | playlistViewModel: PlaylistViewModel = viewModel( 21 | factory = PlaylistViewModel.Factory 22 | ) 23 | ) { 24 | val albums by playlistViewModel.albums.collectAsState() 25 | var selectedAlbum: Album? by remember { mutableStateOf(null) } 26 | AlbumList(items = albums.map { it.asAlbum }, onClickCard = { 27 | onNavigate(Destination.SavedPlaylists(it)) 28 | }, onLongPress = { 29 | selectedAlbum = it 30 | }) 31 | selectedAlbum?.let { 32 | PlaylistOptionsSheet( 33 | onDismissRequest = { selectedAlbum = null }, 34 | album = it, 35 | playlistViewModel = playlistViewModel 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/playlists/model/PlaylistInfoViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.playlists.model 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.SavedStateHandle 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.ViewModelProvider 10 | import androidx.lifecycle.createSavedStateHandle 11 | import androidx.lifecycle.viewModelScope 12 | import androidx.lifecycle.viewmodel.initializer 13 | import androidx.lifecycle.viewmodel.viewModelFactory 14 | import androidx.navigation.toRoute 15 | import app.suhasdissa.vibeyou.MellowMusicApplication 16 | import app.suhasdissa.vibeyou.backend.repository.PlaylistRepository 17 | import app.suhasdissa.vibeyou.domain.models.primary.Album 18 | import app.suhasdissa.vibeyou.navigation.Destination 19 | import app.suhasdissa.vibeyou.presentation.screens.onlinesearch.model.state.AlbumInfoState 20 | import app.suhasdissa.vibeyou.utils.asSong 21 | import kotlinx.coroutines.launch 22 | 23 | class PlaylistInfoViewModel( 24 | private val playlistRepository: PlaylistRepository, 25 | savedStateHandle: SavedStateHandle 26 | ) : ViewModel() { 27 | val playlist = savedStateHandle.toRoute() 28 | 29 | var albumInfoState: AlbumInfoState by mutableStateOf(AlbumInfoState.Loading) 30 | private set 31 | 32 | init { 33 | getPlaylistInfo(playlist.album) 34 | } 35 | 36 | private fun getPlaylistInfo(playlist: Album) { 37 | viewModelScope.launch { 38 | albumInfoState = AlbumInfoState.Loading 39 | albumInfoState = try { 40 | Log.e("PlaylistViewModel", "Getting info") 41 | val info = playlistRepository.getPlaylist(playlist.id) 42 | AlbumInfoState.Success( 43 | playlist, 44 | info.songs.map { it.asSong } 45 | ) 46 | } catch (e: Exception) { 47 | Log.e("Playlist Info", e.toString()) 48 | AlbumInfoState.Error 49 | } 50 | } 51 | } 52 | 53 | companion object { 54 | val Factory: ViewModelProvider.Factory = viewModelFactory { 55 | initializer { 56 | val application = 57 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 58 | PlaylistInfoViewModel( 59 | application.container.playlistRepository, 60 | this.createSavedStateHandle() 61 | ) 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/playlists/model/PlaylistViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.playlists.model 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.lifecycle.viewmodel.initializer 7 | import androidx.lifecycle.viewmodel.viewModelFactory 8 | import app.suhasdissa.vibeyou.MellowMusicApplication 9 | import app.suhasdissa.vibeyou.backend.repository.PlaylistRepository 10 | import app.suhasdissa.vibeyou.domain.models.primary.Album 11 | import kotlinx.coroutines.flow.SharingStarted 12 | import kotlinx.coroutines.flow.stateIn 13 | import kotlinx.coroutines.launch 14 | 15 | class PlaylistViewModel( 16 | private val playlistRepository: PlaylistRepository 17 | ) : ViewModel() { 18 | 19 | 20 | var albums = playlistRepository.getPlaylists().stateIn( 21 | scope = viewModelScope, 22 | started = SharingStarted.WhileSubscribed(5000L), 23 | initialValue = listOf() 24 | ) 25 | 26 | fun deletePlaylist(album: Album) { 27 | viewModelScope.launch { 28 | playlistRepository.deletePlaylist(album) 29 | } 30 | } 31 | 32 | fun clearPlaylist(album: Album) { 33 | viewModelScope.launch { 34 | playlistRepository.clearPlaylist(album) 35 | } 36 | } 37 | 38 | fun deletePlaylistAndSongs(album: Album) { 39 | viewModelScope.launch { 40 | playlistRepository.deletePlaylistAndSongs(album) 41 | } 42 | } 43 | 44 | companion object { 45 | val Factory: ViewModelProvider.Factory = viewModelFactory { 46 | initializer { 47 | val application = 48 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 49 | PlaylistViewModel( 50 | application.container.playlistRepository 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/NetworkSettingsScreen.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.rounded.Web 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.LargeTopAppBar 10 | import androidx.compose.material3.Scaffold 11 | import androidx.compose.material3.Text 12 | import androidx.compose.material3.TopAppBarDefaults 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.input.nestedscroll.nestedScroll 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.lifecycle.viewmodel.compose.viewModel 23 | import app.suhasdissa.vibeyou.R 24 | import app.suhasdissa.vibeyou.presentation.screens.settings.components.CustomInstanceOption 25 | import app.suhasdissa.vibeyou.presentation.screens.settings.components.InstanceSelectDialog 26 | import app.suhasdissa.vibeyou.presentation.screens.settings.components.SettingItem 27 | import app.suhasdissa.vibeyou.presentation.screens.settings.components.TextFieldPref 28 | import app.suhasdissa.vibeyou.presentation.screens.settings.model.SettingsViewModel 29 | import app.suhasdissa.vibeyou.utils.Pref 30 | 31 | @OptIn(ExperimentalMaterial3Api::class) 32 | @Composable 33 | fun NetworkSettingsScreen() { 34 | val viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory) 35 | 36 | var showDialog by remember { mutableStateOf(false) } 37 | var currentServer by remember { 38 | mutableStateOf(Pref.currentInstance) 39 | } 40 | val topBarBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() 41 | LaunchedEffect(Unit) { 42 | viewModel.loadInstances() 43 | } 44 | 45 | Scaffold(modifier = Modifier.fillMaxSize(), topBar = { 46 | LargeTopAppBar( 47 | title = { Text(stringResource(R.string.network_settings)) }, 48 | scrollBehavior = topBarBehavior 49 | ) 50 | }) { innerPadding -> 51 | LazyColumn( 52 | Modifier 53 | .fillMaxSize() 54 | .padding(innerPadding) 55 | .nestedScroll(topBarBehavior.nestedScrollConnection) 56 | ) { 57 | item { 58 | SettingItem( 59 | title = stringResource(R.string.change_server), 60 | description = currentServer.name, 61 | icon = Icons.Rounded.Web 62 | ) { 63 | showDialog = true 64 | } 65 | } 66 | 67 | item { 68 | CustomInstanceOption { 69 | currentServer = it 70 | } 71 | } 72 | 73 | item { 74 | TextFieldPref( 75 | key = Pref.hyperpipeApiUrlKey, 76 | defaultValue = Pref.defaultHyperInstance, 77 | title = stringResource(id = R.string.hyperpipe_api_url) 78 | ) 79 | } 80 | } 81 | } 82 | if (showDialog) { 83 | InstanceSelectDialog(onDismissRequest = { 84 | showDialog = false 85 | }, onSelectionChange = { instance -> 86 | currentServer = instance 87 | Pref.setInstance(instance) 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/components/ButtonGroupPref.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.offset 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material3.ButtonDefaults 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.OutlinedButton 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.zIndex 24 | 25 | @Composable 26 | fun ButtonGroupPref( 27 | title: String, 28 | options: List, 29 | values: List, 30 | currentValue: T, 31 | onChange: (T) -> Unit 32 | ) { 33 | Column( 34 | modifier = Modifier 35 | .padding(top = 8.dp) 36 | .padding(horizontal = 12.dp) 37 | ) { 38 | Text(title) 39 | Spacer(modifier = Modifier.height(8.dp)) 40 | Row( 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | ) { 44 | val cornerRadius = 20.dp 45 | var selectedItem by remember { 46 | mutableStateOf( 47 | currentValue 48 | ) 49 | } 50 | 51 | values.forEachIndexed { index, value -> 52 | val startRadius = if (index != 0) 0.dp else cornerRadius 53 | val endRadius = if (index == values.size - 1) cornerRadius else 0.dp 54 | 55 | OutlinedButton( 56 | onClick = { 57 | selectedItem = value 58 | onChange.invoke(values[index]) 59 | }, 60 | modifier = Modifier 61 | .offset(if (index == 0) 0.dp else (-1 * index).dp, 0.dp) 62 | .zIndex(if (selectedItem == value) 1f else 0f), 63 | shape = RoundedCornerShape( 64 | topStart = startRadius, 65 | topEnd = endRadius, 66 | bottomStart = startRadius, 67 | bottomEnd = endRadius 68 | ), 69 | border = BorderStroke( 70 | 1.dp, 71 | if (selectedItem == value) { 72 | MaterialTheme.colorScheme.primary 73 | } else { 74 | MaterialTheme.colorScheme.primary.copy(alpha = 0.75f) 75 | } 76 | ), 77 | colors = if (selectedItem == value) { 78 | ButtonDefaults.outlinedButtonColors( 79 | containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), 80 | contentColor = MaterialTheme.colorScheme.primary 81 | ) 82 | } else { 83 | ButtonDefaults.outlinedButtonColors( 84 | containerColor = MaterialTheme.colorScheme.surface, 85 | contentColor = MaterialTheme.colorScheme.onSurface 86 | ) 87 | } 88 | ) { 89 | Text(options[index]) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/components/CacheSizeDialog.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.lazy.grid.GridCells 5 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 6 | import androidx.compose.foundation.lazy.grid.items 7 | import androidx.compose.material3.AlertDialog 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.FilterChip 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import app.suhasdissa.vibeyou.R 18 | import app.suhasdissa.vibeyou.utils.Pref 19 | import app.suhasdissa.vibeyou.utils.formatMB 20 | import app.suhasdissa.vibeyou.utils.rememberPreference 21 | 22 | @OptIn(ExperimentalMaterial3Api::class) 23 | @Composable 24 | fun CacheSizeDialog(onDismissRequest: () -> Unit) { 25 | val cacheSizes = listOf(0, 512, 1024, 1024 * 2, 1024 * 4, 1024 * 6) 26 | var prefSize by rememberPreference(key = Pref.exoCacheKey, defaultValue = 0) 27 | AlertDialog( 28 | onDismissRequest, 29 | title = { Text(stringResource(R.string.change_music_cache_size)) }, 30 | confirmButton = { 31 | Button(onClick = { 32 | onDismissRequest.invoke() 33 | }) { 34 | Text(text = stringResource(R.string.ok)) 35 | } 36 | }, 37 | text = { 38 | LazyVerticalGrid( 39 | columns = GridCells.Fixed(3), 40 | verticalArrangement = Arrangement.spacedBy(8.dp), 41 | horizontalArrangement = Arrangement.spacedBy(8.dp) 42 | ) { 43 | items(items = cacheSizes) { 44 | FilterChip( 45 | selected = prefSize == it, 46 | onClick = { prefSize = it }, 47 | label = { 48 | Text( 49 | if (it == 0) { 50 | stringResource( 51 | R.string.unlimited 52 | ) 53 | } else { 54 | formatMB(it) 55 | } 56 | ) 57 | } 58 | ) 59 | } 60 | } 61 | } 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/components/ColorPref.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.lazy.LazyRow 11 | import androidx.compose.foundation.lazy.items 12 | import androidx.compose.foundation.shape.CircleShape 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.rounded.Check 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.unit.dp 22 | import app.suhasdissa.vibeyou.utils.catpucchinLatte 23 | 24 | @Composable 25 | fun ColorPref(selectedColor: Int, onSelect: (Int) -> Unit) { 26 | LazyRow( 27 | modifier = Modifier 28 | .fillMaxWidth() 29 | .padding(top = 8.dp) 30 | .padding(horizontal = 12.dp), 31 | horizontalArrangement = Arrangement.spacedBy(8.dp) 32 | ) { 33 | items(catpucchinLatte) { color -> 34 | Box( 35 | modifier = Modifier 36 | .size(48.dp) 37 | .clip(CircleShape) 38 | .background(Color(color)) 39 | .clickable { onSelect(color) }, 40 | contentAlignment = Alignment.Center 41 | ) { 42 | if (color == selectedColor) { 43 | Icon(imageVector = Icons.Rounded.Check, contentDescription = null) 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/components/InstanceSelectDialog.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.components 2 | 3 | import android.view.SoundEffectConstants 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.foundation.lazy.items 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.AlertDialog 14 | import androidx.compose.material3.RadioButton 15 | import androidx.compose.material3.Surface 16 | import androidx.compose.material3.Text 17 | import androidx.compose.material3.TextButton 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.draw.clip 26 | import androidx.compose.ui.platform.LocalView 27 | import androidx.compose.ui.res.stringResource 28 | import androidx.compose.ui.unit.dp 29 | import androidx.lifecycle.viewmodel.compose.viewModel 30 | import app.suhasdissa.vibeyou.R 31 | import app.suhasdissa.vibeyou.backend.models.PipedInstance 32 | import app.suhasdissa.vibeyou.presentation.screens.settings.model.SettingsViewModel 33 | import app.suhasdissa.vibeyou.utils.Pref 34 | 35 | @Composable 36 | fun InstanceSelectDialog( 37 | onDismissRequest: () -> Unit, 38 | onSelectionChange: (instance: PipedInstance) -> Unit 39 | ) { 40 | val viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory) 41 | 42 | var selectedInstance by remember { 43 | mutableStateOf(Pref.currentInstance) 44 | } 45 | val view = LocalView.current 46 | 47 | AlertDialog( 48 | onDismissRequest = { onDismissRequest.invoke() }, 49 | confirmButton = { 50 | TextButton(onClick = { onDismissRequest.invoke() }) { 51 | Text(text = stringResource(android.R.string.cancel)) 52 | } 53 | }, 54 | title = { 55 | Text(stringResource(R.string.select_server)) 56 | }, 57 | text = { 58 | Surface( 59 | modifier = Modifier.width(300.dp), 60 | shape = RoundedCornerShape(10.dp) 61 | ) { 62 | LazyColumn(modifier = Modifier.height(500.dp)) { 63 | items(items = viewModel.instances) { item -> 64 | Row( 65 | Modifier 66 | .fillMaxWidth() 67 | .clip(RoundedCornerShape(12.dp)) 68 | .clickable(onClick = { 69 | view.playSoundEffect(SoundEffectConstants.CLICK) 70 | selectedInstance = item 71 | onSelectionChange(item) 72 | onDismissRequest.invoke() 73 | }) 74 | .padding(horizontal = 6.dp, vertical = 12.dp), 75 | verticalAlignment = Alignment.CenterVertically 76 | ) { 77 | RadioButton( 78 | selected = (selectedInstance.name == item.name), 79 | onClick = null 80 | ) 81 | Text( 82 | text = item.name, 83 | modifier = Modifier.padding(start = 16.dp) 84 | ) 85 | } 86 | } 87 | } 88 | } 89 | } 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/components/SettingItem.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.components 2 | 3 | import android.view.SoundEffectConstants 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.vector.ImageVector 18 | import androidx.compose.ui.platform.LocalView 19 | import androidx.compose.ui.unit.dp 20 | 21 | @Composable 22 | fun SettingItem(title: String, description: String, icon: ImageVector?, onClick: () -> Unit) { 23 | val view = LocalView.current 24 | Surface( 25 | modifier = Modifier.clickable { 26 | view.playSoundEffect(SoundEffectConstants.CLICK) 27 | onClick() 28 | } 29 | ) { 30 | Row( 31 | modifier = Modifier 32 | .fillMaxWidth() 33 | .padding(4.dp, 12.dp), 34 | verticalAlignment = Alignment.CenterVertically 35 | ) { 36 | icon?.let { 37 | Icon( 38 | imageVector = icon, 39 | contentDescription = null, 40 | modifier = Modifier 41 | .padding(start = 8.dp, end = 16.dp) 42 | .size(24.dp), 43 | tint = MaterialTheme.colorScheme.secondary 44 | ) 45 | } 46 | Column( 47 | modifier = Modifier 48 | .weight(1f) 49 | .padding(start = if (icon == null) 14.dp else 0.dp) 50 | ) { 51 | Text( 52 | text = title, 53 | maxLines = 1, 54 | style = MaterialTheme.typography.titleLarge, 55 | color = MaterialTheme.colorScheme.onSurface 56 | ) 57 | Text( 58 | text = description, 59 | color = MaterialTheme.colorScheme.onSurfaceVariant, 60 | maxLines = 1, 61 | style = MaterialTheme.typography.bodyMedium 62 | ) 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/components/SwitchPref.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.material3.Switch 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import app.suhasdissa.vibeyou.utils.rememberPreference 23 | 24 | @Composable 25 | fun SwitchPref( 26 | prefKey: String, 27 | title: String, 28 | summary: String? = null, 29 | defaultValue: Boolean = false, 30 | onCheckedChange: (Boolean) -> Unit = {} 31 | ) { 32 | var checked by rememberPreference(key = prefKey, defaultValue = defaultValue) 33 | val interactionSource = remember { MutableInteractionSource() } 34 | 35 | Row( 36 | modifier = Modifier 37 | .fillMaxWidth() 38 | .padding(horizontal = 12.dp) 39 | .clickable( 40 | interactionSource = interactionSource, 41 | indication = null 42 | ) { 43 | checked = !checked 44 | onCheckedChange.invoke(checked) 45 | }, 46 | horizontalArrangement = Arrangement.SpaceBetween, 47 | verticalAlignment = Alignment.CenterVertically 48 | ) { 49 | Column( 50 | modifier = Modifier.weight(1f), 51 | verticalArrangement = Arrangement.Center 52 | ) { 53 | Text(fontSize = 18.sp, text = title) 54 | if (summary != null) { 55 | Text(modifier = Modifier.padding(top = 6.dp), text = summary) 56 | } 57 | } 58 | Spacer(modifier = Modifier.width(6.dp)) 59 | Switch( 60 | checked = checked, 61 | onCheckedChange = { 62 | checked = it 63 | onCheckedChange.invoke(it) 64 | } 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/components/TextFieldPref.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.components 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.OutlinedTextField 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.core.content.edit 15 | import app.suhasdissa.vibeyou.utils.Pref 16 | 17 | @Composable 18 | fun TextFieldPref( 19 | key: String, 20 | defaultValue: String, 21 | title: String, 22 | onValueChange: (String) -> Unit = {} 23 | ) { 24 | var value by remember { 25 | mutableStateOf(Pref.sharedPreferences.getString(key, defaultValue).orEmpty()) 26 | } 27 | 28 | OutlinedTextField( 29 | modifier = Modifier 30 | .fillMaxWidth() 31 | .padding(horizontal = 8.dp), 32 | value = value, 33 | onValueChange = { 34 | value = it 35 | Pref.sharedPreferences.edit(true) { putString(key, it) } 36 | onValueChange(it) 37 | }, 38 | label = { 39 | Text(text = title) 40 | } 41 | ) 42 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/model/AuthViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.backend.viewmodel 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import android.widget.Toast 6 | import androidx.core.content.edit 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.ViewModelProvider 9 | import androidx.lifecycle.viewModelScope 10 | import androidx.lifecycle.viewmodel.initializer 11 | import androidx.lifecycle.viewmodel.viewModelFactory 12 | import app.suhasdissa.vibeyou.MellowMusicApplication 13 | import app.suhasdissa.vibeyou.backend.models.Login 14 | import app.suhasdissa.vibeyou.backend.repository.AuthRepository 15 | import app.suhasdissa.vibeyou.utils.Pref 16 | import app.suhasdissa.vibeyou.utils.preferences 17 | import kotlinx.coroutines.launch 18 | 19 | class AuthViewModel(private val authRepository: AuthRepository) : ViewModel() { 20 | fun login(context: Context, login: Login) { 21 | viewModelScope.launch { 22 | Toast.makeText(context, "Authenticating..", Toast.LENGTH_SHORT).show() 23 | val token = try { 24 | authRepository.getAuthToken(login) 25 | } catch (e: Exception) { 26 | Log.e("Playlist Info", e.toString()) 27 | Toast.makeText(context, "Login Failed", Toast.LENGTH_SHORT).show() 28 | null 29 | } 30 | token?.token?.let { 31 | context.preferences.edit { 32 | putString(Pref.authTokenKey, it) 33 | } 34 | } ?: return@launch 35 | Toast.makeText(context, "Login Success", Toast.LENGTH_SHORT).show() 36 | } 37 | } 38 | 39 | companion object { 40 | val Factory: ViewModelProvider.Factory = viewModelFactory { 41 | initializer { 42 | val application = 43 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 44 | AuthViewModel( 45 | application.container.authRepository 46 | ) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/model/CheckUpdateViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.model 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import app.suhasdissa.vibeyou.utils.UpdateUtil 9 | import kotlinx.coroutines.launch 10 | 11 | class CheckUpdateViewModel : ViewModel() { 12 | var latestVersion: Float? by mutableStateOf(null) 13 | val currentVersion = UpdateUtil.currentVersion 14 | 15 | init { 16 | getLatestRelease() 17 | } 18 | 19 | private fun getLatestRelease() { 20 | viewModelScope.launch { 21 | latestVersion = UpdateUtil.getLatestVersion() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/model/DatabaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.model 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.ViewModelProvider 8 | import androidx.lifecycle.viewModelScope 9 | import androidx.lifecycle.viewmodel.initializer 10 | import androidx.lifecycle.viewmodel.viewModelFactory 11 | import androidx.sqlite.db.SimpleSQLiteQuery 12 | import app.suhasdissa.vibeyou.MellowMusicApplication 13 | import app.suhasdissa.vibeyou.data.database.SongDatabase 14 | import app.suhasdissa.vibeyou.utils.services.PlayerService 15 | import kotlinx.coroutines.launch 16 | import java.io.FileInputStream 17 | import java.io.FileOutputStream 18 | import kotlin.system.exitProcess 19 | 20 | class DatabaseViewModel(private val database: SongDatabase) : ViewModel() { 21 | 22 | init { 23 | } 24 | 25 | fun backupDatabase(uri: Uri, context: Context) { 26 | viewModelScope.launch { 27 | database.rawDao().raw(SimpleSQLiteQuery("PRAGMA wal_checkpoint(FULL)")) 28 | 29 | context.applicationContext.contentResolver.openOutputStream(uri) 30 | ?.use { outputStream -> 31 | FileInputStream(database.openHelper.writableDatabase.path).use { inputStream -> 32 | inputStream.copyTo(outputStream) 33 | } 34 | } 35 | } 36 | } 37 | 38 | fun restoreDatabase(uri: Uri, context: Context) { 39 | viewModelScope.launch { 40 | database.rawDao().raw(SimpleSQLiteQuery("PRAGMA wal_checkpoint(FULL)")) 41 | database.close() 42 | 43 | context.applicationContext.contentResolver.openInputStream(uri) 44 | ?.use { inputStream -> 45 | FileOutputStream( 46 | database.openHelper.writableDatabase.path 47 | ).use { outputStream -> 48 | inputStream.copyTo(outputStream) 49 | } 50 | } 51 | context.stopService(Intent(context, PlayerService::class.java)) 52 | exitProcess(0) 53 | } 54 | } 55 | 56 | companion object { 57 | val Factory: ViewModelProvider.Factory = viewModelFactory { 58 | initializer { 59 | val application = 60 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 61 | DatabaseViewModel( 62 | application.container.database 63 | ) 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/model/SettingsModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.model 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.annotation.StringRes 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.setValue 8 | import androidx.core.content.edit 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY 11 | import androidx.lifecycle.viewmodel.initializer 12 | import androidx.lifecycle.viewmodel.viewModelFactory 13 | import app.suhasdissa.vibeyou.R 14 | import app.suhasdissa.vibeyou.utils.Pref 15 | import app.suhasdissa.vibeyou.utils.catpucchinLatte 16 | import app.suhasdissa.vibeyou.utils.mutableStatePreferenceOf 17 | import app.suhasdissa.vibeyou.utils.preferences 18 | 19 | class SettingsModel(preferences: SharedPreferences) : ViewModel() { 20 | enum class Theme(@StringRes val resId: Int) { 21 | SYSTEM(R.string.system), LIGHT(R.string.light), DARK(R.string.dark), 22 | AMOLED(R.string.amoled) 23 | } 24 | 25 | enum class ColorTheme(@StringRes val resId: Int) { 26 | SYSTEM(R.string.system), 27 | CATPPUCCIN(R.string.catppuccin) 28 | } 29 | 30 | private val themeModePref = 31 | preferences.getString(Pref.themeKey, Theme.SYSTEM.name) ?: Theme.SYSTEM.name 32 | 33 | var themeMode: Theme by mutableStatePreferenceOf( 34 | Theme.valueOf(themeModePref.uppercase()) 35 | ) { 36 | preferences.edit { putString(Pref.themeKey, it.name) } 37 | } 38 | 39 | private val colorThemePref = 40 | preferences.getString(Pref.colorThemeKey, ColorTheme.SYSTEM.name) 41 | ?: ColorTheme.SYSTEM.name 42 | 43 | var colorTheme: ColorTheme by mutableStatePreferenceOf( 44 | ColorTheme.valueOf(colorThemePref.uppercase()) 45 | ) { 46 | preferences.edit { putString(Pref.colorThemeKey, it.name) } 47 | } 48 | 49 | var customColor by mutableStatePreferenceOf( 50 | preferences.getInt( 51 | Pref.customColorKey, 52 | catpucchinLatte.first() 53 | ) 54 | ) { 55 | preferences.edit { putInt(Pref.customColorKey, it) } 56 | } 57 | 58 | companion object { 59 | val Factory = viewModelFactory { 60 | initializer { 61 | val context = this[APPLICATION_KEY] as Context 62 | SettingsModel(preferences = context.preferences) 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/presentation/screens/settings/model/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.presentation.screens.settings.model 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.ViewModelProvider 8 | import androidx.lifecycle.viewmodel.initializer 9 | import androidx.lifecycle.viewmodel.viewModelFactory 10 | import app.suhasdissa.vibeyou.MellowMusicApplication 11 | import app.suhasdissa.vibeyou.backend.repository.PipedMusicRepository 12 | import app.suhasdissa.vibeyou.utils.Pref 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.withContext 15 | 16 | class SettingsViewModel( 17 | private val pipedMusicRepository: PipedMusicRepository 18 | ) : ViewModel() { 19 | var instances by mutableStateOf(Pref.pipedInstances) 20 | 21 | suspend fun loadInstances() = runCatching { 22 | instances = withContext(Dispatchers.IO) { 23 | pipedMusicRepository.pipedApi.getInstanceList() 24 | } 25 | } 26 | 27 | companion object { 28 | val Factory: ViewModelProvider.Factory = viewModelFactory { 29 | initializer { 30 | val application = 31 | (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) 32 | SettingsViewModel(application.container.pipedMusicRepository) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.ColorScheme 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.SideEffect 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.core.view.WindowCompat 16 | 17 | @Composable 18 | fun VibeYouTheme( 19 | darkTheme: Boolean = isSystemInDarkTheme(), 20 | customColorScheme: ColorScheme, 21 | dynamicColor: Boolean = true, 22 | amoledDark: Boolean = false, 23 | content: @Composable () -> Unit 24 | ) { 25 | val colorScheme = when { 26 | amoledDark -> { 27 | if (dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 28 | val context = LocalContext.current 29 | dynamicDarkColorScheme(context).copy( 30 | background = Color.Black, 31 | surface = Color.Black 32 | ) 33 | } else { 34 | customColorScheme.copy(background = Color.Black, surface = Color.Black) 35 | } 36 | } 37 | 38 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 39 | val context = LocalContext.current 40 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 41 | } 42 | 43 | else -> customColorScheme 44 | } 45 | val view = LocalView.current 46 | if (!view.isInEditMode) { 47 | SideEffect { 48 | val activity = view.context as Activity 49 | val insetsController = WindowCompat.getInsetsController( 50 | activity.window, 51 | view 52 | ) 53 | insetsController.isAppearanceLightStatusBars = !darkTheme 54 | insetsController.isAppearanceLightNavigationBars = !darkTheme 55 | } 56 | } 57 | 58 | MaterialTheme( 59 | colorScheme = colorScheme, 60 | typography = Typography, 61 | content = content 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) 35 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/Convertions.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import kotlin.math.ln 4 | import kotlin.math.pow 5 | 6 | fun formatMB(mb: Int): String { 7 | val index = (ln(mb.toDouble()) / ln(1024.0)).toInt() 8 | return "${mb.div(1024f.pow(index)).toInt()} ${arrayOf("M", "G", "T")[index]}B" 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/DynamicDataSource.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import android.net.Uri 4 | import androidx.media3.common.util.UnstableApi 5 | import androidx.media3.datasource.DataSource 6 | import androidx.media3.datasource.DataSpec 7 | import androidx.media3.datasource.DefaultDataSource 8 | import androidx.media3.datasource.ResolvingDataSource 9 | import androidx.media3.datasource.TransferListener 10 | 11 | /** 12 | * A dynamic data source, which attempts to play from the cache if the source is online 13 | * Else, if the source uri is not online, it doesn't cache and plays directly from the device storage 14 | */ 15 | @UnstableApi 16 | class DynamicDataSource( 17 | private val resolvingDataSource: ResolvingDataSource, 18 | private val defaultDataSource: DefaultDataSource 19 | ) : DataSource { 20 | private var isOnline = false 21 | private val dataSource get() = if (isOnline) resolvingDataSource else defaultDataSource 22 | 23 | override fun read(buffer: ByteArray, offset: Int, length: Int): Int { 24 | return dataSource.read(buffer, offset, length) 25 | } 26 | 27 | override fun addTransferListener(transferListener: TransferListener) { 28 | dataSource.addTransferListener(transferListener) 29 | } 30 | 31 | override fun open(dataSpec: DataSpec): Long { 32 | isOnline = dataSpec.uri.scheme != "content" 33 | return dataSource.open(dataSpec) 34 | } 35 | 36 | override fun getUri(): Uri? = dataSource.uri 37 | 38 | override fun close() { 39 | dataSource.close() 40 | } 41 | 42 | companion object { 43 | class Factory( 44 | private val resolvingDataSource: ResolvingDataSource.Factory, 45 | private val defaultDataSource: DefaultDataSource.Factory 46 | ) : DataSource.Factory { 47 | override fun createDataSource(): DataSource { 48 | return DynamicDataSource( 49 | resolvingDataSource.createDataSource(), 50 | defaultDataSource.createDataSource() 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/Mappers.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.text.format.DateUtils 5 | import androidx.core.net.toUri 6 | import androidx.core.os.bundleOf 7 | import androidx.media3.common.MediaItem 8 | import androidx.media3.common.MediaMetadata 9 | import app.suhasdissa.vibeyou.backend.models.PipedSongResponse 10 | import app.suhasdissa.vibeyou.backend.models.playlists.Playlist 11 | import app.suhasdissa.vibeyou.backend.models.songs.SongItem 12 | import app.suhasdissa.vibeyou.data.database.entities.PlaylistEntity 13 | import app.suhasdissa.vibeyou.data.database.entities.SongEntity 14 | import app.suhasdissa.vibeyou.domain.models.primary.Album 15 | import app.suhasdissa.vibeyou.domain.models.primary.Artist 16 | import app.suhasdissa.vibeyou.domain.models.primary.Song 17 | 18 | val SongItem.asSong: Song 19 | get() = Song( 20 | id = videoId, 21 | title = title, 22 | artistsText = uploaderName, 23 | durationText = DateUtils.formatElapsedTime(duration.toLong()), 24 | thumbnailUri = thumbnail.toUri() 25 | ) 26 | 27 | val Song.asSongEntity: SongEntity 28 | get() = SongEntity( 29 | id = id, 30 | title = title, 31 | artistsText = artistsText, 32 | durationText = durationText, 33 | thumbnailUrl = thumbnailUri.toString(), 34 | likedAt = likedAt 35 | ) 36 | 37 | val SongEntity.asSong: Song 38 | get() = Song( 39 | id = id, 40 | title = title, 41 | artistsText = artistsText, 42 | durationText = durationText, 43 | thumbnailUri = thumbnailUrl?.toUri(), 44 | likedAt = likedAt 45 | ) 46 | 47 | fun PipedSongResponse.asSong(id: String): Song { 48 | return Song( 49 | id = id, 50 | title = title!!, 51 | artistsText = uploader ?: "", 52 | durationText = DateUtils.formatElapsedTime(duration?.toLong() ?: 0L), 53 | thumbnailUri = thumbnailUrl?.toUri() 54 | ) 55 | } 56 | 57 | val Playlist.asAlbum: Album 58 | get() = Album( 59 | id = playlistId, 60 | title = name, 61 | artistsText = uploaderName, 62 | thumbnailUri = thumbnail.toUri() 63 | ) 64 | 65 | val Album.asPlaylistEntity: PlaylistEntity 66 | get() = PlaylistEntity( 67 | id = id, 68 | title = title, 69 | type = type, 70 | subTitle = artistsText, 71 | thumbnailUrl = thumbnailUri.toString() 72 | ) 73 | val PlaylistEntity.asAlbum: Album 74 | get() = Album( 75 | id = id, 76 | title = title, 77 | thumbnailUri = thumbnailUrl?.toUri(), 78 | artistsText = subTitle ?: "", 79 | isLocal = true, 80 | type = type 81 | ) 82 | val app.suhasdissa.vibeyou.backend.models.artists.Artist.asArtist: Artist 83 | get() = Artist( 84 | id = artistId, 85 | thumbnailUri = thumbnail?.toUri(), 86 | description = description, 87 | artistsText = name 88 | ) 89 | 90 | val Song.asMediaItem: MediaItem 91 | @SuppressLint("UnsafeOptInUsageError") 92 | get() = MediaItem.Builder() 93 | .setMediaMetadata( 94 | MediaMetadata.Builder() 95 | .setTitle(title) 96 | .setArtist(artistsText) 97 | .setArtworkUri(thumbnailUri) 98 | .setExtras( 99 | bundleOf(IS_LOCAL_KEY to isLocal) 100 | ) 101 | .build() 102 | ) 103 | .setUri(id) 104 | .setMediaId(id) 105 | .setCustomCacheKey(id) 106 | .build() 107 | 108 | val MediaItem.maxResThumbnail: String 109 | get() { 110 | if (mediaId.startsWith("content")) return "" 111 | val pipedProxyUrl = 112 | Pref.currentInstance.imageProxyUrl.ifEmpty { "https://pipedproxy.kavin.rocks" } 113 | return "$pipedProxyUrl/vi_webp/$mediaId/maxresdefault.webp?host=i.ytimg.com" 114 | } 115 | 116 | const val IS_LOCAL_KEY = "isLocal" 117 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/OpenBrowser.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | 7 | fun openBrowser(context: Context, url: String) { 8 | val viewIntent: Intent = Intent().apply { 9 | action = Intent.ACTION_VIEW 10 | data = Uri.parse(url) 11 | } 12 | context.startActivity(viewIntent) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/PermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import androidx.core.app.ActivityCompat 7 | 8 | object PermissionHelper { 9 | fun checkPermissions(context: Context, permissions: Array): Boolean { 10 | if (permissions.isEmpty()) return true 11 | if (!hasPermission(context, permissions.first())) { 12 | ActivityCompat.requestPermissions( 13 | context as Activity, 14 | permissions, 15 | 1 16 | ) 17 | return false 18 | } 19 | return true 20 | } 21 | 22 | private fun hasPermission(context: Context, permission: String): Boolean { 23 | return ActivityCompat.checkSelfPermission( 24 | context, 25 | permission 26 | ) == PackageManager.PERMISSION_GRANTED 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/Player.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import androidx.media3.common.C 4 | import androidx.media3.common.MediaItem 5 | import androidx.media3.common.Player 6 | import androidx.media3.common.Timeline 7 | 8 | inline val Timeline.windows: List 9 | get() = List(windowCount) { 10 | getWindow(it, Timeline.Window()) 11 | } 12 | 13 | inline val Timeline.queue: List> 14 | get() = List(windowCount) { 15 | it to getWindow(it, Timeline.Window()).mediaItem 16 | } 17 | 18 | inline val Timeline.mediaIdList: List 19 | get() = List(windowCount) { 20 | getWindow(it, Timeline.Window()).mediaItem.mediaId 21 | } 22 | 23 | fun Player.playPause() { 24 | if (isPlaying) { 25 | pause() 26 | } else { 27 | if (playbackState == Player.STATE_IDLE) { 28 | prepare() 29 | } 30 | play() 31 | } 32 | } 33 | 34 | fun Player.forcePlay(mediaItem: MediaItem) { 35 | setMediaItem(mediaItem, true) 36 | playWhenReady = true 37 | prepare() 38 | } 39 | 40 | fun Player.playGracefully(mediaItem: MediaItem) { 41 | if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { 42 | forcePlay(mediaItem) 43 | } else { 44 | val newIndex = currentPeriodIndex + 1 45 | addMediaItem(newIndex, mediaItem) 46 | seekTo(newIndex, C.TIME_UNSET) 47 | } 48 | } 49 | 50 | fun Player.seekNext() { 51 | if (playbackState == Player.STATE_IDLE) { 52 | prepare() 53 | } 54 | seekToNext() 55 | 56 | } 57 | 58 | fun Player.seek(position: Long) { 59 | if (playbackState == Player.STATE_IDLE) { 60 | prepare() 61 | } 62 | seekTo(position) 63 | } 64 | 65 | fun Player.enqueue(mediaItem: MediaItem) { 66 | if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { 67 | forcePlay(mediaItem) 68 | } else { 69 | addMediaItem(mediaItemCount, mediaItem) 70 | } 71 | } 72 | 73 | fun Player.forcePlayAtIndex(mediaItems: List, mediaItemIndex: Int) { 74 | if (mediaItems.isEmpty()) return 75 | 76 | setMediaItems(mediaItems, mediaItemIndex, C.TIME_UNSET) 77 | playWhenReady = true 78 | prepare() 79 | } 80 | 81 | fun Player.forcePlayFromBeginning(mediaItems: List) = 82 | forcePlayAtIndex(mediaItems, 0) 83 | 84 | fun Player.enqueue(mediaItems: List) { 85 | if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { 86 | forcePlayFromBeginning(mediaItems) 87 | } else { 88 | addMediaItems(mediaItemCount, mediaItems) 89 | } 90 | } 91 | 92 | fun Player.addNext(mediaItem: MediaItem) { 93 | if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { 94 | forcePlay(mediaItem) 95 | } else { 96 | addMediaItem(currentMediaItemIndex + 1, mediaItem) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/Pref.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import android.content.SharedPreferences 4 | import androidx.core.content.edit 5 | import app.suhasdissa.vibeyou.backend.models.PipedInstance 6 | import kotlinx.serialization.encodeToString 7 | import kotlinx.serialization.json.Json 8 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull 9 | 10 | object Pref { 11 | private const val pipedInstanceKey = "SelectedPipedInstanceKey" 12 | const val authTokenKey = "AuthTokenKey" 13 | const val exoCacheKey = "ExoCacheKey" 14 | const val thumbnailColorFallbackKey = "ThumbnailColorFallbackef" 15 | const val latestSongsSortOrderKey = "LatestSongsSortOrderKey" 16 | const val latestReverseSongsPrefKey = "LatestReverseSongsPrefKey" 17 | const val customPipedInstanceKey = "CustomPipedInstanceKey" 18 | const val disableSearchHistoryKey = "DisableSearchHistory" 19 | const val hyperpipeApiUrlKey = "HyperpipeApiUrl" 20 | const val customColorKey = "customColor" 21 | const val themeKey = "theme" 22 | const val colorThemeKey = "colorTheme" 23 | const val equalizerKey = "equalizer" 24 | const val equalizerPresetKey = "equalizerPreset" 25 | const val equalizerBandsKey = "equalizerBands" 26 | const val showAllMusicKey = "showAllMusicKey" 27 | const val musicDirectoriesKey = "musicDirectoriesKey" 28 | 29 | lateinit var sharedPreferences: SharedPreferences 30 | 31 | val defaultHyperInstance = "https://hyperpipeapi.onrender.com" 32 | 33 | val pipedInstances = listOf( 34 | PipedInstance( 35 | "kavin.rocks Libre", 36 | "https://pipedapi-libre.kavin.rocks" 37 | ), 38 | PipedInstance( 39 | "kavin.rocks", 40 | "https://pipedapi.kavin.rocks" 41 | ), 42 | PipedInstance( 43 | "lunar.icu", 44 | "https://piped-api.lunar.icu" 45 | ), 46 | PipedInstance( 47 | "whatever.social", 48 | "https://watchapi.whatever.social" 49 | ), 50 | PipedInstance( 51 | "tokhmi.xyz", 52 | "https://pipedapi.tokhmi.xyz" 53 | ), 54 | PipedInstance( 55 | "mha.fi", 56 | "https://api-piped.mha.fi" 57 | ), 58 | PipedInstance( 59 | "garudalinux.org", 60 | "https://piped-api.garudalinux.org" 61 | ), 62 | PipedInstance( 63 | "piped.yt", 64 | "https://api.piped.yt" 65 | ) 66 | ) 67 | 68 | val currentInstance 69 | get() = run { 70 | runCatching { 71 | val instanceJsonStr = sharedPreferences.getString(pipedInstanceKey, "").orEmpty() 72 | return@run json.decodeFromString(instanceJsonStr) 73 | } 74 | pipedInstances.first() 75 | } 76 | 77 | val hyperInstance: String? 78 | get() { 79 | val url = sharedPreferences.getString(hyperpipeApiUrlKey, defaultHyperInstance) 80 | ?: defaultHyperInstance 81 | return url.toHttpUrlOrNull()?.host 82 | } 83 | 84 | private val json = Json { ignoreUnknownKeys = true } 85 | 86 | fun setInstance(instance: PipedInstance) { 87 | val instanceJson = json.encodeToString(instance) 88 | sharedPreferences.edit(commit = true) { putString(pipedInstanceKey, instanceJson) } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/Preferences.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.SnapshotMutationPolicy 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.platform.LocalContext 11 | import androidx.core.content.edit 12 | 13 | inline fun > SharedPreferences.getEnum( 14 | key: String, 15 | defaultValue: T 16 | ): T = 17 | getString(key, null)?.let { 18 | try { 19 | enumValueOf(it) 20 | } catch (e: IllegalArgumentException) { 21 | null 22 | } 23 | } ?: defaultValue 24 | 25 | inline fun > SharedPreferences.Editor.putEnum( 26 | key: String, 27 | value: T 28 | ): SharedPreferences.Editor = 29 | putString(key, value.name) 30 | 31 | val Context.preferences: SharedPreferences 32 | get() = getSharedPreferences("preferences", Context.MODE_PRIVATE) 33 | 34 | @Composable 35 | fun rememberPreference(key: String, defaultValue: Boolean): MutableState { 36 | val context = LocalContext.current 37 | return remember { 38 | mutableStatePreferenceOf(context.preferences.getBoolean(key, defaultValue)) { 39 | context.preferences.edit { putBoolean(key, it) } 40 | } 41 | } 42 | } 43 | 44 | @Composable 45 | fun rememberPreference(key: String, defaultValue: Int): MutableState { 46 | val context = LocalContext.current 47 | return remember { 48 | mutableStatePreferenceOf(context.preferences.getInt(key, defaultValue)) { 49 | context.preferences.edit { putInt(key, it) } 50 | } 51 | } 52 | } 53 | 54 | @Composable 55 | fun rememberPreference(key: String, defaultValue: String): MutableState { 56 | val context = LocalContext.current 57 | return remember { 58 | mutableStatePreferenceOf(context.preferences.getString(key, null) ?: defaultValue) { 59 | context.preferences.edit { putString(key, it) } 60 | } 61 | } 62 | } 63 | 64 | @Composable 65 | inline fun > rememberPreference(key: String, defaultValue: T): MutableState { 66 | val context = LocalContext.current 67 | return remember { 68 | mutableStatePreferenceOf(context.preferences.getEnum(key, defaultValue)) { 69 | context.preferences.edit { putEnum(key, it) } 70 | } 71 | } 72 | } 73 | 74 | inline fun mutableStatePreferenceOf( 75 | value: T, 76 | crossinline onStructuralInequality: (newValue: T) -> Unit 77 | ) = 78 | mutableStateOf( 79 | value = value, 80 | policy = object : SnapshotMutationPolicy { 81 | override fun equivalent(a: T, b: T): Boolean { 82 | val areEquals = a == b 83 | if (!areEquals) onStructuralInequality(b) 84 | return areEquals 85 | } 86 | } 87 | ) 88 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/RetrofitHelper.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import app.suhasdissa.vibeyou.data.api.HyperpipeApi 4 | import app.suhasdissa.vibeyou.data.api.PipedApi 5 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 6 | import kotlinx.serialization.json.Json 7 | import okhttp3.MediaType.Companion.toMediaType 8 | import retrofit2.Retrofit 9 | import retrofit2.create 10 | 11 | object RetrofitHelper { 12 | val json by lazy { 13 | Json { ignoreUnknownKeys = true } 14 | } 15 | 16 | fun createPipedApi(): PipedApi { 17 | val retrofit: Retrofit = Retrofit.Builder() 18 | .baseUrl("https://pipedapi.kavin.rocks/") 19 | .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) 20 | .build() 21 | 22 | return retrofit.create() 23 | } 24 | 25 | fun createHyperpipeApi(): HyperpipeApi { 26 | val retrofit = Retrofit.Builder() 27 | .baseUrl("https://hyperpipeapi.onrender.com") 28 | .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) 29 | .build() 30 | 31 | return retrofit.create() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/ThemeUtil.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.material3.ColorScheme 5 | import androidx.compose.ui.graphics.Color 6 | import com.google.android.material.color.utilities.Scheme 7 | 8 | object ThemeUtil { 9 | @SuppressLint("RestrictedApi") 10 | fun getSchemeFromSeed(color: Int, dark: Boolean): ColorScheme { 11 | return if (dark) { 12 | Scheme.dark(color).toColorScheme() 13 | } else { 14 | Scheme.light(color).toColorScheme() 15 | } 16 | } 17 | } 18 | 19 | val catpucchinLatte = arrayOf( 20 | android.graphics.Color.rgb(220, 138, 120), 21 | android.graphics.Color.rgb(221, 120, 120), 22 | android.graphics.Color.rgb(234, 118, 203), 23 | android.graphics.Color.rgb(136, 57, 239), 24 | android.graphics.Color.rgb(210, 15, 57), 25 | android.graphics.Color.rgb(230, 69, 83), 26 | android.graphics.Color.rgb(254, 100, 11), 27 | android.graphics.Color.rgb(223, 142, 29), 28 | android.graphics.Color.rgb(64, 160, 43), 29 | android.graphics.Color.rgb(23, 146, 153), 30 | android.graphics.Color.rgb(4, 165, 229), 31 | android.graphics.Color.rgb(32, 159, 181), 32 | android.graphics.Color.rgb(30, 102, 245), 33 | android.graphics.Color.rgb(114, 135, 253) 34 | ) 35 | 36 | @SuppressLint("RestrictedApi") 37 | fun Scheme.toColorScheme() = ColorScheme( 38 | primary = Color(primary), 39 | onPrimary = Color(onPrimary), 40 | primaryContainer = Color(primaryContainer), 41 | onPrimaryContainer = Color(onPrimaryContainer), 42 | inversePrimary = Color(inversePrimary), 43 | secondary = Color(secondary), 44 | onSecondary = Color(onSecondary), 45 | secondaryContainer = Color(secondaryContainer), 46 | onSecondaryContainer = Color(onSecondaryContainer), 47 | tertiary = Color(tertiary), 48 | onTertiary = Color(onTertiary), 49 | tertiaryContainer = Color(tertiaryContainer), 50 | onTertiaryContainer = Color(onTertiaryContainer), 51 | background = Color(background), 52 | onBackground = Color(onBackground), 53 | surface = Color(surface), 54 | onSurface = Color(onSurface), 55 | surfaceVariant = Color(surfaceVariant), 56 | onSurfaceVariant = Color(onSurfaceVariant), 57 | surfaceTint = Color(primary), 58 | inverseSurface = Color(inverseSurface), 59 | inverseOnSurface = Color(inverseOnSurface), 60 | error = Color(error), 61 | onError = Color(onError), 62 | errorContainer = Color(errorContainer), 63 | onErrorContainer = Color(onErrorContainer), 64 | outline = Color(outline), 65 | outlineVariant = Color(outlineVariant), 66 | scrim = Color(scrim) 67 | ) 68 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/TimeUtil.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import java.time.Instant 4 | import java.time.ZoneId 5 | 6 | object TimeUtil { 7 | fun getYear(timestamp: Long): Int { 8 | val instant = Instant.ofEpochSecond(timestamp) 9 | return instant.atZone(ZoneId.systemDefault()).year 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/UpdateUtil.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.os.Build 6 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 7 | import kotlinx.serialization.SerialName 8 | import kotlinx.serialization.Serializable 9 | import kotlinx.serialization.json.Json 10 | import okhttp3.MediaType.Companion.toMediaType 11 | import retrofit2.Retrofit 12 | import retrofit2.http.GET 13 | import retrofit2.http.Headers 14 | import java.util.regex.Pattern 15 | 16 | object UpdateUtil { 17 | var currentVersion = 0f 18 | 19 | private suspend fun getLatestRelease(): LatestRelease? { 20 | return try { 21 | UpdateApi.retrofitService.getLatestRelease() 22 | } catch (e: Exception) { 23 | null 24 | } 25 | } 26 | 27 | suspend fun getLatestVersion(): Float? { 28 | return getLatestRelease()?.let { 29 | it.tagName.toVersion() 30 | } 31 | } 32 | 33 | fun getCurrentVersion(context: Context) { 34 | currentVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 35 | context.packageManager.getPackageInfo( 36 | context.packageName, 37 | PackageManager.PackageInfoFlags.of(0) 38 | ).versionName.toFloat() 39 | } else { 40 | context.packageManager.getPackageInfo( 41 | context.packageName, 42 | 0 43 | ).versionName.toFloat() 44 | } 45 | } 46 | 47 | private val pattern = Pattern.compile("""v(.+)""") 48 | 49 | private fun String?.toVersion(): Float = this?.run { 50 | val matcher = pattern.matcher(this) 51 | if (matcher.find()) { 52 | matcher.group(1)?.toFloat() ?: 0f 53 | } else { 54 | 0f 55 | } 56 | } ?: 0f 57 | } 58 | 59 | @Serializable 60 | data class LatestRelease( 61 | @SerialName("tag_name") val tagName: String? = null 62 | ) 63 | 64 | private val jsonFormat = Json { ignoreUnknownKeys = true } 65 | 66 | private val retrofitVideo = Retrofit.Builder() 67 | .baseUrl("https://api.github.com/") 68 | .addConverterFactory(jsonFormat.asConverterFactory("application/json".toMediaType())) 69 | .build() 70 | 71 | interface UpdateApiService { 72 | @Headers( 73 | "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" 74 | ) 75 | @GET("repos/you-apps/VibeYou/releases/latest") 76 | suspend fun getLatestRelease(): LatestRelease 77 | } 78 | 79 | object UpdateApi { 80 | val retrofitService: UpdateApiService by lazy { 81 | retrofitVideo.create(UpdateApiService::class.java) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/vibeyou/utils/UriSerializer.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.vibeyou.utils 2 | 3 | import android.net.Uri 4 | import androidx.core.net.toUri 5 | import kotlinx.serialization.KSerializer 6 | import kotlinx.serialization.descriptors.PrimitiveKind 7 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 8 | import kotlinx.serialization.descriptors.SerialDescriptor 9 | import kotlinx.serialization.encoding.Decoder 10 | import kotlinx.serialization.encoding.Encoder 11 | 12 | class UriSerializer : KSerializer { 13 | 14 | override val descriptor: SerialDescriptor = 15 | PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) 16 | 17 | override fun deserialize(decoder: Decoder): Uri { 18 | return decoder.decodeString().toUri() 19 | } 20 | 21 | override fun serialize(encoder: Encoder, value: Uri) { 22 | encoder.encodeString(value.toString()) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/blob.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_piped.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/music_placeholder.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-bn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | সেটিংস 4 | আমাকে পড়ুন 5 | সম্পর্কে 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | آواها 4 | مرابخوان 5 | جدیدترین رهانش 6 | مورد علاقه ها 7 | پخش آوا 8 | تنظیمات 9 | پخش 10 | حذف مورد از صف 11 | صف پخش 12 | تنظیمات شبکه 13 | تغییر سرور 14 | انتخاب سرور 15 | دیدن صف 16 | هنرمند 17 | رمز عبور 18 | آواهای اخیر 19 | بستن جستجو 20 | آلبوم ها 21 | هنرمندان 22 | باشه 23 | درباره 24 | بی نهایت 25 | سرعت پخش 26 | پخش همه 27 | لغو 28 | تنظیمات ظاهر 29 | سیستم 30 | روشن 31 | تاریک 32 | فعال 33 | تنظیمات 34 | نسخه فعلی 35 | اضافه کردن به صف 36 | حذف آوا 37 | در حال پخش 38 | پاک کردن جستجو 39 | ورود 40 | نام کاربری 41 | جستجوی آواها 42 | جستجو برای یک آوا 43 | تنظیمات آوا 44 | بستن صف 45 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 設定 4 | 最新リリース 5 | 現在のバージョン 6 | GitHub Issue 7 | お気に入り 8 | キューに追加 9 | 曲を再生 10 | 曲を削除 11 | 全てリピート 12 | 再生中 13 | 詳細 14 | リポジトリとREADMEを確認 15 | バグ報告や機能リクエストを送信 16 | バージョン、連絡先 17 | README 18 | 次に再生 19 | 曲を検索 20 | 曲を検索 21 | 設定 22 | 繰り返しオフ 23 | -------------------------------------------------------------------------------- /app/src/main/res/values-nb-rNO/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Favoritter 4 | Lukk avspiller 5 | Lukk kø 6 | Opprett en sikkerhetskopifil 7 | Nyeste utgave 8 | Passord 9 | Albumsomslag 10 | Nettverksinnstillinger 11 | Fjern element fra kø 12 | Nylige spor 13 | Gjenta alt 14 | Send inn feilrapporter og funksjonsforespørsler 15 | Hopp over neste 16 | Musikkhurtiglagringsgrense 17 | Endre tjener 18 | Pause 19 | Gjenopprett database 20 | Ubegrenset 21 | Spor-albumskunst 22 | Fjern spor 23 | Spill spor 24 | Gjenta én 25 | Innstillinger 26 | Brukernavn 27 | Gjenopprett en tidligere sikkerhetskopifil 28 | Sikkerhetskopi og gjenoppretting 29 | LESMEG 30 | Nåværende versjon 31 | Versjon, kontakt 32 | OK 33 | Artister 34 | Noe gikk galt 35 | Søk etter et spor 36 | Album 37 | Sikkerhetskopier database 38 | Artist 39 | Spillerkø 40 | Sporalternativer 41 | Vis kø 42 | Artistavatar 43 | Omstokking 44 | Sjekk kodelageret og LESMEG 45 | Velg tjener 46 | Spilles nå 47 | Spor 48 | Endre musikkhurtiglagringsstørrelse 49 | Logg inn 50 | Piped-musikk 51 | Endre tjener for å fikse avspillingsproblemer 52 | Tøm søk 53 | Innstillinger 54 | Spill 55 | Gjentagelse av 56 | Lokal musikk 57 | Legg til i kø 58 | GitHub-problem 59 | Sikkerhetskopier din sporliste, eller gjenopprett en gammel sikkerhetskopi. 60 | Lukk søk 61 | Søk etter spor 62 | Om 63 | Spill neste 64 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-pa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ਸੈਟਿੰਗਾਂ 4 | ਮੈਨੂੰ ਪੜ੍ਹੋ 5 | ਗੀਤ 6 | ਦੇ ਬਾਰੇ 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-sv/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pausa 4 | Återställ databas 5 | Inställningar 6 | Avbryt 7 | Låtar 8 | Inställningar 9 | Om 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 歌曲 4 | 關於 5 | 設定 6 | 目前版本 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #EFF1F5 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id("com.android.application") version "8.4.0" apply false 4 | id("com.android.library") version "8.4.0" apply false 5 | id("org.jetbrains.kotlin.android") version "1.9.23" apply false 6 | id("com.google.devtools.ksp") version "1.9.23-1.0.20" apply false 7 | } 8 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Vibe You is a music app that lets you play music from your device storage or from Piped Music. 2 | It has a Material You dynamic theme with dark mode support, offline cached playback, shuffle, queue, and more. 3 | 4 | With the added benefit of playing region blocked music, This app also has advanced queue management and offline caching for songs to play them later without data consumption. 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Music app supporting playback via local storage and Piped 2 | -------------------------------------------------------------------------------- /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 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/you-apps/VibeYou/c5c7eb3e9a2228b715689d9524cdc98db139287d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu May 02 18:14:03 IST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven("https://jitpack.io") 14 | } 15 | } 16 | rootProject.name = "VibeYou" 17 | include(":app") 18 | --------------------------------------------------------------------------------