├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug-ui-bug.md │ ├── feature_request.md │ └── logic-bug.md ├── .gitignore ├── GET_STARTED.md ├── LICENSE ├── README.md ├── README_ES.MD ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── release │ ├── baselineProfiles │ │ ├── 0 │ │ │ └── CM_2.8.0.dm │ │ └── 1 │ │ │ └── CM_2.8.0.dm │ └── output-metadata.json └── src │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── sosauce │ │ │ └── cutemusic │ │ │ ├── data │ │ │ ├── RoomConverters.kt │ │ │ ├── actions │ │ │ │ ├── MediaItemActions.kt │ │ │ │ ├── MetadataActions.kt │ │ │ │ ├── PlayerActions.kt │ │ │ │ └── PlaylistActions.kt │ │ │ ├── datastore │ │ │ │ ├── DataStore.kt │ │ │ │ └── SettingsExt.kt │ │ │ ├── playlist │ │ │ │ ├── PlaylistDao.kt │ │ │ │ ├── PlaylistDatabase.kt │ │ │ │ └── PlaylistState.kt │ │ │ └── states │ │ │ │ └── MusicState.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ ├── domain │ │ │ ├── model │ │ │ │ ├── Album.kt │ │ │ │ ├── Artist.kt │ │ │ │ ├── Folder.kt │ │ │ │ ├── Lyrics.kt │ │ │ │ └── Playlist.kt │ │ │ └── repository │ │ │ │ ├── MediaStoreHelper.kt │ │ │ │ ├── MediaStoreHelperImpl.kt │ │ │ │ └── SafManager.kt │ │ │ ├── main │ │ │ ├── App.kt │ │ │ ├── AutoPlaybackService.kt │ │ │ ├── MainActivity.kt │ │ │ ├── PlaybackService.kt │ │ │ └── quickplay │ │ │ │ ├── QuickPlayActivity.kt │ │ │ │ ├── QuickPlayUiState.kt │ │ │ │ └── QuickPlayViewModel.kt │ │ │ ├── ui │ │ │ ├── navigation │ │ │ │ ├── Navigation.kt │ │ │ │ └── Screen.kt │ │ │ ├── screens │ │ │ │ ├── album │ │ │ │ │ ├── AlbumDetailsLandscape.kt │ │ │ │ │ ├── AlbumDetailsScreen.kt │ │ │ │ │ └── AlbumScreen.kt │ │ │ │ ├── artist │ │ │ │ │ ├── ArtistDetails.kt │ │ │ │ │ ├── ArtistDetailsLandscape.kt │ │ │ │ │ └── ArtistsScreen.kt │ │ │ │ ├── lyrics │ │ │ │ │ └── LyricsView.kt │ │ │ │ ├── main │ │ │ │ │ ├── MainScreen.kt │ │ │ │ │ └── components │ │ │ │ │ │ └── SortingDropdownMenu.kt │ │ │ │ ├── metadata │ │ │ │ │ ├── MetadataEditor.kt │ │ │ │ │ ├── MetadataState.kt │ │ │ │ │ └── MetadataViewModel.kt │ │ │ │ ├── playing │ │ │ │ │ ├── NowPlaying.kt │ │ │ │ │ ├── NowPlayingLandscape.kt │ │ │ │ │ └── components │ │ │ │ │ │ ├── Artwork.kt │ │ │ │ │ │ ├── Buttons.kt │ │ │ │ │ │ ├── CuteSlider.kt │ │ │ │ │ │ ├── CuteSquigglySlider.kt │ │ │ │ │ │ ├── CuteTimePicker.kt │ │ │ │ │ │ ├── QueueSheet.kt │ │ │ │ │ │ ├── QuickActionsRow.kt │ │ │ │ │ │ ├── RateAdjustmentDialog.kt │ │ │ │ │ │ ├── SliderStyle.kt │ │ │ │ │ │ └── SpeedCard.kt │ │ │ │ ├── playlists │ │ │ │ │ ├── CreatePlaylistDialog.kt │ │ │ │ │ ├── PlaylistDetailsScreen.kt │ │ │ │ │ ├── PlaylistItem.kt │ │ │ │ │ ├── PlaylistPicker.kt │ │ │ │ │ └── PlaylistsScreen.kt │ │ │ │ └── settings │ │ │ │ │ ├── SettingsLibrary.kt │ │ │ │ │ ├── SettingsLookAndFeel.kt │ │ │ │ │ ├── SettingsNowPlaying.kt │ │ │ │ │ ├── SettingsScreen.kt │ │ │ │ │ └── compenents │ │ │ │ │ ├── AboutCard.kt │ │ │ │ │ ├── FolderItem.kt │ │ │ │ │ ├── SettingsCategoryCard.kt │ │ │ │ │ ├── SettingsComponents.kt │ │ │ │ │ ├── SettingsScreens.kt │ │ │ │ │ └── Switches.kt │ │ │ ├── shared_components │ │ │ │ ├── AnimatedIconButton.kt │ │ │ │ ├── CuteDropdownMenuItem.kt │ │ │ │ ├── CuteNavigationButton.kt │ │ │ │ ├── CuteText.kt │ │ │ │ ├── LazyRowWithScrollButton.kt │ │ │ │ ├── MusicDetailsDialog.kt │ │ │ │ ├── MusicListItem.kt │ │ │ │ ├── MusicViewModel.kt │ │ │ │ ├── PlaylistViewModel.kt │ │ │ │ ├── ScreenSelection.kt │ │ │ │ ├── Searchbar.kt │ │ │ │ ├── SelectedBar.kt │ │ │ │ └── ThreadDivider.kt │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ └── Theme.kt │ │ │ └── widgets │ │ │ │ ├── MusicWidget.kt │ │ │ │ ├── WidgetActions.kt │ │ │ │ └── WidgetBroadcastReceiver.kt │ │ │ └── utils │ │ │ ├── Constants.kt │ │ │ ├── Extensions.kt │ │ │ └── ImageUtils.kt │ └── res │ │ ├── drawable │ │ ├── add_emoji_rounded.xml │ │ ├── add_photo_rounded.xml │ │ ├── artist_rounded.xml │ │ ├── bedtime_outlined.xml │ │ ├── carousel.xml │ │ ├── classic_slider.xml │ │ ├── dark_mode.xml │ │ ├── edit_rounded.xml │ │ ├── export.xml │ │ ├── folder_rounded.xml │ │ ├── grid_view.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── icon_splash.xml │ │ ├── image.xml │ │ ├── info_rounded.xml │ │ ├── library.xml │ │ ├── lyrics_rounded.xml │ │ ├── music_note_rounded.xml │ │ ├── queue_music_rounded.xml │ │ ├── reset.xml │ │ ├── resource_import.xml │ │ ├── saf.xml │ │ ├── speed_rounded.xml │ │ ├── system_theme.xml │ │ ├── trash_rounded.xml │ │ ├── trash_rounded_filled.xml │ │ ├── widget_next.xml │ │ ├── widget_pause.xml │ │ ├── widget_play.xml │ │ └── widget_previous.xml │ │ ├── font │ │ └── nunito.ttf │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-ro │ │ └── strings.xml │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values-v31 │ │ └── colors.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── automotive_app_desc.xml │ │ └── music_widget_info.xml │ └── release │ └── generated │ └── baselineProfiles │ ├── baseline-prof.txt │ └── startup-prof.txt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── changelogs │ │ └── 1 │ ├── full_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── screenshot0.png │ │ │ ├── screenshot1.png │ │ │ ├── screenshot2.png │ │ │ ├── screenshot3.png │ │ │ └── screenshot4.png │ ├── short_description.txt │ └── title.txt │ ├── es-ES │ ├── changelogs │ │ └── 1 │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ └── fr-FR │ ├── changelogs │ └── 1 │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── font_licence.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: https://bit.ly/sosaucePayPal 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-ui-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug UI Bug 3 | about: Use this to report an UI bug 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Screenshots** 14 | If applicable, add screenshots to help explain your problem. 15 | 16 | **Smartphone (please complete the following information):** 17 | - Device: 18 | - OS: 19 | - Version: 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/logic-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Logic Bug 3 | about: Use this to report a logic bug 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Indicate the steps to reproduce. 15 | 16 | **Expected behavior** 17 | What behavior did you expect ? 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Smartphone (please complete the following information):** 23 | - Device: 24 | - OS: 25 | - Version: 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | misc.xml 22 | deploymentTargetDropDown.xml 23 | render.experimental.xml 24 | 25 | # Keystore files 26 | *.jks 27 | *.keystore 28 | 29 | # Google Services (e.g. APIs or Firebase) 30 | google-services.json 31 | 32 | # Android Profiling 33 | *.hprof 34 | .DS_Store 35 | .kotlin/sessions/kotlin-compiler-9804472835993706459.salive 36 | -------------------------------------------------------------------------------- /GET_STARTED.md: -------------------------------------------------------------------------------- 1 |

🚀 Getting Started

2 | 3 | 4 | ### Code Guidelines 5 | 6 | CuteApps currently don't have alot of guidelines, but it is best if you follow the few ones they have such as : 7 | 8 | - Clutter-free experience : This is self explanatory, avoid adding elements if un-needed, two example could be : 9 | - flashing music icon : The music icon in the searchbar flashes red, indicating that it is clickable / has an action related to it 10 | - the restart button, the seek to previous automatically becomes one 10 seconds in the song instead being a whole new button which brings us to the next guideline : 11 | - Make things clear : If you are adding a feature, make sure it is clear of what it does, clear text/description, accurate icon 12 | - Landscape : CuteApps MUST be fully compatible with landscape mode, if you are adding any screen, make sure it has a landscape variant, otherwise your PR will be ignored 13 | - Creativity : This isn't mandatory, but if you are designing a screen or something else, be creative ! Try things no other apps has before, be unique ! Remember, failure is just a step closer to perfection ! 14 | 15 | ### Prerequisites 16 | 17 | - Android Studio (latest version recommended) 18 | - Java Development Kit (JDK) 11 or higher 19 | - Git 20 | 21 | ### Installation 22 | 23 | 1. **Clone the repository:** 24 | ```sh 25 | git clone https://github.com/sosauce/CuteMusic.git 26 | cd CuteMusic 27 | ``` 28 | 29 | 2. **Open the project in Android Studio:** 30 | - Open Android Studio. 31 | - Select `Open an existing project`. 32 | - Navigate to the `CuteMusic` directory and select it. 33 | 34 | 3. **Build the project:** 35 | - Click on `Build` in the top menu. 36 | - Select `Make Project` and ensure there are no errors. 37 | 38 | 4. **Run the app:** 39 | - Connect an Android device or use an emulator. 40 | - Click on `Run` in the top menu. 41 | - Select your device and click `OK`. 42 | 43 | ### Contributing 44 | 45 | 1. **Fork the repository:** 46 | - Click the `Fork` button on the top right of the repository page. 47 | 48 | 2. **Create a new branch:** 49 | ```sh 50 | git checkout -b feature/YourFeatureName 51 | ``` 52 | 53 | 3. **Make your changes:** 54 | - Implement your feature or bug fix. 55 | - Ensure your code follows the project's coding standards. 56 | 4. **Commit your changes:** 57 | ```sh 58 | git add . 59 | git commit -m "Add feature: YourFeatureName" 60 | ``` 61 | 5. **Push to your fork:** 62 | ```sh 63 | git push origin feature/YourFeatureName 64 | ``` 65 | 6. **Create a Pull Request:** 66 | - Go to the original repository. 67 | - Click on `Pull Requests` and then `New Pull Request`. 68 | - Select your branch and submit the pull request. 69 | 70 | Thank you to anyone taking their time to contribute and improve the app :heart:!!! 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

CuteMusic

3 |

CuteMusic is a cute and powerful offline music player for Android!

4 | 5 | > [!CAUTION] 6 | > The Google Play Store release is a fake and stolen one. CuteMusic is NOT officialy available on the Google Play Store !!! 7 |

8 | 9 | 10 | 11 |

12 |

13 | 14 | Disponible en español 15 | 16 |

17 |

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |

I recommend installing from GitHub, as it guarantees access to the latest update.

28 |

29 | 30 | 31 | 32 |

33 | 34 | 35 | 36 | 37 |

38 | 39 |

👀 Overview

40 | 41 | - Play any song from anywhere just by sharing the audio file to the app without downloading it! 42 | - Easy search across all your music/albums/artists! 43 | - Very fast and snappy! 44 | - No unnecessary permissions needed! 45 | - Material 3/You & Monet theming (+ Amoled mode)! 46 | - Blacklist Folders! 47 | - Beautiful landscape UI! 48 | - Tag Editor! 49 | - Playlists support! 50 | - Load and persist songs from anywhere using Android's S.A.F! 51 | - Part of the CuteApps ecosystem! 52 | - Makes you a cutie! 53 | 54 | --- 55 |

💬 Contact Me

56 | 57 | - Discord server: https://discord.gg/c6aCu4yjbu 58 | - Email: sosauce_dev@protonmail.com 59 | 60 | --- 61 |

❤️ Support

62 | 63 |

You can support me by donating on my PayPal: https://bit.ly/sosaucePayPal. Thank you so much for the support ❤️

64 | 65 | --- 66 |

⚠️ Copyright

67 | 68 |

Copyright (c)2025 sosauce 69 | 70 | This program is free software: you can redistribute it and/or modify 71 | it under the terms of the GNU General Public License as published by 72 | the Free Software Foundation, either version 3 of the License, or 73 | (at your option) any later version. 74 | 75 | This program is distributed in the hope that it will be useful, 76 | but WITHOUT ANY WARRANTY; without even the implied warranty of 77 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 78 | GNU General Public License for more details. 79 | 80 | The above copyright notice, this permission notice, and its license shall be included in all copies or substantial portions of the Software. 81 | 82 | You can find a copy of the GNU General Public License v3 [here](https://www.gnu.org/licenses/)

83 | 84 | 85 | --- 86 | To get started with contributing, please check the [get started readme](https://github.com/sosauce/CuteMusic/blob/main/GET_STARTED.md) 87 | 88 | --- 89 | 90 | #### You can find the SHA-256 here : https://sosauce.github.io/projects/ 91 | -------------------------------------------------------------------------------- /README_ES.MD: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

CuteMusic

6 |

CuteMusic es una aplicación sencilla, ligera y de código abierto para reproducir música offline en Android, desarrollada en Jetpack Compose y Media3.

7 | 8 |

9 | 10 | 11 | 12 |

13 | 14 |

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

25 | 26 |

Recomendamos la instalación desde GitHub, ya que garantiza el acceso a las últimas actualizaciones.

27 | 28 |

29 | 30 | 31 | 32 |

33 | 34 |

👀 Resumen

35 | 36 | - Reproduce tus canciones desde cualquier lugar compartiendo el archivo de audio con la aplicación ¡sin necesidad de descargarlo! 37 | - ¡Búsqueda fácil en toda tu música/álbumes/artistas! 38 | - ¡Muy rápida y ágil! 39 | - ¡Sin permisos innecesarios! 40 | - ¡Tematización Material 3/You & Monet (+ modo Amoled)! 41 | - Lista negra de carpetas. 42 | - ¡Bonita interfaz de usuario panorámica! 43 | - ¡Editor de etiquetas! 44 | - ¡¡Soporta listas de reproducción! 45 | - ¡¡Te convierte en una monada! 46 | 47 |

👑 2025 Hoja de ruta !!!

48 | 49 | - Importación de canciones desde cualquier lugar mediante el [S.A.F](https://developer.android.com/guide/topics/providers/document-provider). 50 | - Android Auto (''Querido Google, Media3 necesita urgentemente una integración fácil con Auto''). 51 | - Widget(s?). 52 | 53 | --- 54 | 55 |

💬 Contactar conmigo

56 | 57 | - Servidor de Discord: https://discord.gg/c6aCu4yjbu. 58 | - Email: sosauce_dev@protonmail.com. 59 | 60 | --- 61 | 62 |

❤️ Ayuda

63 | 64 |

Puedes apoyarme donando en mi PayPal: https://bit.ly/sosaucePayPal. Muchísimas gracias por el apoyo ❤️

65 | 66 | --- 67 | 68 |

⚠️ Copyright

69 | 70 |

Copyright (c)2024 sosauce. 71 | 72 | Este programa es software libre: puede redistribuirlo y/o modificarlo bajo los términos de la Licencia Pública General GNU publicada por la Free Software Foundation, ya sea la versión 3 de la Licencia, o (a su elección) cualquier versión posterior. 73 | 74 | Este programa se distribuye con la esperanza de que sea útil, pero SIN NINGUNA GARANTÍA; ni siquiera la garantía implícita de COMERCIABILIDAD o IDONEIDAD PARA UN PROPÓSITO PARTICULAR. Consulte la Licencia Pública General GNU para más detalles. 75 | 76 | El aviso de copyright anterior, este aviso de permiso y su licencia se incluirán en todas las copias o partes sustanciales del Software. 77 | 78 | Puede encontrar una copia de la Licencia Pública General GNU v3 [aquí](https://www.gnu.org/licenses/).

79 | 80 | --- 81 | 82 | Para empezar a contribuir, consulte el [readme de introducción](https://github.com/sosauce/CuteMusic/blob/main/GET_STARTED.md). 83 | 84 | --- 85 | 86 | #### El SHA-256 se encuentra aquí: https://sosauce.github.io/projects/ 87 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) 3 | alias(libs.plugins.kotlin) 4 | alias(libs.plugins.compose.compiler) 5 | alias(libs.plugins.serialization) 6 | alias(libs.plugins.ksp) 7 | } 8 | 9 | android { 10 | namespace = "com.sosauce.cutemusic" 11 | compileSdk = 36 12 | 13 | defaultConfig { 14 | applicationId = "com.sosauce.cutemusic" 15 | minSdk = 26 16 | targetSdk = 36 17 | versionCode = 31 18 | versionName = "2.8.1" 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary = true 22 | } 23 | ndk { 24 | //noinspection ChromeOsAbiSupport 25 | abiFilters += arrayOf("arm64-v8a", "armeabi-v7a") 26 | } 27 | } 28 | 29 | applicationVariants.all { 30 | val variant = this 31 | variant.outputs 32 | .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } 33 | .forEach { output -> 34 | output.outputFileName = "CM_${variant.versionName}.apk" 35 | } 36 | } 37 | 38 | 39 | buildTypes { 40 | release { 41 | isMinifyEnabled = true 42 | isShrinkResources = true 43 | proguardFiles( 44 | getDefaultProguardFile("proguard-android-optimize.txt"), 45 | "proguard-rules.pro" 46 | ) 47 | } 48 | compileOptions { 49 | sourceCompatibility = JavaVersion.VERSION_17 50 | targetCompatibility = JavaVersion.VERSION_17 51 | } 52 | kotlinOptions { 53 | jvmTarget = "17" 54 | } 55 | buildFeatures { 56 | compose = true 57 | } 58 | packaging { 59 | resources { 60 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 61 | } 62 | } 63 | dependenciesInfo { 64 | includeInApk = false 65 | includeInBundle = false 66 | } 67 | 68 | 69 | // splits { 70 | // abi { 71 | // isEnable = true 72 | // reset() 73 | // include("armeabi-v7a", "arm64-v8a") 74 | // isUniversalApk = true 75 | // } 76 | // } 77 | 78 | 79 | } 80 | 81 | dependencies { 82 | implementation(platform(libs.androidx.compose.bom)) 83 | implementation(libs.androidx.core.ktx) 84 | implementation(libs.androidx.activity.compose) 85 | implementation(libs.androidx.material3) 86 | implementation(libs.androidx.ui) 87 | implementation(libs.androidx.material.icons.extended) 88 | implementation(libs.androidx.lifecycle.runtime.compose) 89 | implementation(libs.androidx.core.splashscreen) 90 | implementation(libs.androidx.datastore.preferences) 91 | implementation(libs.coil.compose) 92 | implementation(libs.androidx.media3.common) 93 | implementation(libs.androidx.media3.exoplayer) 94 | implementation(libs.androidx.media3.session) 95 | implementation(libs.squigglyslider) 96 | implementation(libs.androidx.compose.animation) 97 | implementation(libs.kotlinx.serialization.json) 98 | implementation(libs.koin.android) 99 | implementation(libs.koin.androidx.compose) 100 | implementation(libs.material.kolor) 101 | implementation(libs.koin.androidx.startup) 102 | implementation(libs.taglib) 103 | debugImplementation(libs.androidx.ui.tooling) 104 | implementation(libs.androidx.room.ktx) 105 | implementation(libs.androidx.emoji2.emojipicker) 106 | implementation(libs.kmpalette.core) 107 | implementation(libs.haze) 108 | implementation(libs.androidx.glance) 109 | implementation(libs.androidx.glance.appwidget) 110 | implementation(libs.androidx.navigation3.runtime) 111 | implementation(libs.androidx.navigation3.ui) 112 | implementation(libs.reorderable) 113 | ksp(libs.androidx.room.compiler) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -------------------------------------------------------------------------------- /app/release/baselineProfiles/0/CM_2.8.0.dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/release/baselineProfiles/0/CM_2.8.0.dm -------------------------------------------------------------------------------- /app/release/baselineProfiles/1/CM_2.8.0.dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/release/baselineProfiles/1/CM_2.8.0.dm -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.sosauce.cutemusic", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 30, 15 | "versionName": "2.8.0", 16 | "outputFile": "CM_2.8.0.apk" 17 | } 18 | ], 19 | "elementType": "File", 20 | "baselineProfiles": [ 21 | { 22 | "minApi": 28, 23 | "maxApi": 30, 24 | "baselineProfiles": [ 25 | "baselineProfiles/1/CM_2.8.0.dm" 26 | ] 27 | }, 28 | { 29 | "minApi": 31, 30 | "maxApi": 2147483647, 31 | "baselineProfiles": [ 32 | "baselineProfiles/0/CM_2.8.0.dm" 33 | ] 34 | } 35 | ], 36 | "minSdkVersionForDexing": 26 37 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 11 | 12 | 13 | 16 | 19 | 20 | 21 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 50 | 51 | 52 | 53 | 54 | 55 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 119 | 120 | 121 | 124 | 125 | 126 | 127 | 128 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/RoomConverters.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data 2 | 3 | import androidx.room.TypeConverter 4 | import kotlinx.serialization.json.Json 5 | 6 | class MediaItemConverter { 7 | 8 | 9 | @TypeConverter 10 | fun mediaItemToString(mediaItems: List): String { 11 | return Json.encodeToString(mediaItems) 12 | } 13 | 14 | @TypeConverter 15 | fun stringToMediaItem(string: String): List { 16 | return Json.decodeFromString>(string) 17 | } 18 | 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/actions/MediaItemActions.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data.actions 2 | 3 | import android.net.Uri 4 | import androidx.activity.result.ActivityResultLauncher 5 | import androidx.activity.result.IntentSenderRequest 6 | 7 | sealed interface MediaItemActions { 8 | 9 | data class DeleteMediaItem( 10 | val uri: List, 11 | val activityResultLauncher: ActivityResultLauncher 12 | ) : MediaItemActions 13 | 14 | data class ShareMediaItem( 15 | val uri: Uri, 16 | ) : MediaItemActions 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/actions/MetadataActions.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data.actions 2 | 3 | import android.net.Uri 4 | 5 | sealed interface MetadataActions { 6 | 7 | data class LoadSong( 8 | val path: String, 9 | val uri: Uri 10 | ) : MetadataActions 11 | 12 | data class UpdateAudioArt( 13 | val newArtUri: Uri 14 | ) : MetadataActions 15 | 16 | data object SaveChanges : MetadataActions 17 | 18 | data object RemoveArtwork : MetadataActions 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/actions/PlayerActions.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data.actions 2 | 3 | import androidx.media3.common.MediaItem 4 | 5 | sealed interface PlayerActions { 6 | data object PlayOrPause : PlayerActions 7 | data object SeekToNextMusic : PlayerActions 8 | data object SeekToPreviousMusic : PlayerActions 9 | data object RestartSong : PlayerActions 10 | data object PlayRandom : PlayerActions 11 | data object StopPlayback : PlayerActions 12 | data class SeekTo(val position: Long) : PlayerActions 13 | data class SeekToSlider(val position: Long) : PlayerActions 14 | data class RewindTo(val position: Long) : PlayerActions 15 | data class StartPlayback(val mediaId: String) : PlayerActions 16 | data class SeekToMusicIndex(val index: Int) : PlayerActions 17 | 18 | /** 19 | * @param mediaId If set to null, it means we want to play a random song 20 | */ 21 | data class StartAlbumPlayback( 22 | val albumName: String, 23 | val mediaId: String? 24 | ) : PlayerActions 25 | 26 | /** 27 | * @param mediaId If set to null, it means we want to play a random song 28 | */ 29 | data class StartArtistPlayback( 30 | val artistName: String, 31 | val mediaId: String? 32 | ) : PlayerActions 33 | 34 | data class StartPlaylistPlayback( 35 | val playlistSongsId: List, 36 | val mediaId: String? 37 | ) : PlayerActions 38 | 39 | data class UpdateCurrentPosition( 40 | val position: Long 41 | ) : PlayerActions 42 | 43 | data class SetSleepTimer( 44 | val hours: Int, 45 | val minutes: Int 46 | ) : PlayerActions 47 | 48 | data class ReArrangeQueue( 49 | val from: Int, 50 | val to: Int 51 | ) : PlayerActions 52 | 53 | data class RemoveFromQueue( 54 | val mediaId: String 55 | ) : PlayerActions 56 | 57 | data class AddToQueue( 58 | val mediaItem: MediaItem 59 | ) : PlayerActions 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/actions/PlaylistActions.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data.actions 2 | 3 | import android.net.Uri 4 | import com.sosauce.cutemusic.domain.model.Playlist 5 | 6 | sealed interface PlaylistActions { 7 | 8 | data object CreatePlaylist : PlaylistActions 9 | data class UpdateStateName(val name: String) : PlaylistActions 10 | data class UpdateStateEmoji(val emoji: String) : PlaylistActions 11 | data class DeletePlaylist(val playlist: Playlist) : PlaylistActions 12 | data class UpsertPlaylist(val playlist: Playlist) : PlaylistActions // Modify a playlist basically 13 | data class ImportM3uPlaylist(val uri: Uri) : PlaylistActions 14 | data class ExportM3uPlaylist( 15 | val uri: Uri, 16 | val tracks: List 17 | ) : PlaylistActions 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/datastore/SettingsExt.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data.datastore 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.rememberCoroutineScope 10 | import androidx.compose.ui.platform.LocalConfiguration 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.datastore.preferences.core.Preferences 13 | import androidx.datastore.preferences.core.edit 14 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.distinctUntilChanged 17 | import kotlinx.coroutines.flow.map 18 | import kotlinx.coroutines.launch 19 | import kotlinx.serialization.json.Json 20 | 21 | @Composable 22 | fun rememberPreference( 23 | key: Preferences.Key, 24 | defaultValue: T, 25 | ): MutableState { 26 | val coroutineScope = rememberCoroutineScope() 27 | val context = LocalContext.current 28 | val state by remember { 29 | context.dataStore.data 30 | .map { it[key] ?: defaultValue } 31 | }.collectAsStateWithLifecycle(defaultValue) 32 | 33 | return remember(state) { 34 | object : MutableState { 35 | override var value: T 36 | get() = state 37 | set(value) { 38 | coroutineScope.launch { 39 | context.dataStore.edit { 40 | it[key] = value 41 | } 42 | } 43 | } 44 | 45 | override fun component1() = value 46 | override fun component2(): (T) -> Unit = { value = it } 47 | } 48 | } 49 | } 50 | 51 | fun getPreference( 52 | key: Preferences.Key, 53 | defaultValue: T, 54 | context: Context 55 | ): Flow = 56 | context.dataStore.data 57 | .map { preference -> 58 | preference[key] ?: defaultValue 59 | } 60 | 61 | suspend inline fun saveCustomPreference( 62 | value: T, 63 | key: Preferences.Key, 64 | context: Context 65 | ) { 66 | val json = Json.encodeToString(value) 67 | context.dataStore.edit { prefs -> 68 | prefs[key] = json 69 | } 70 | } 71 | 72 | inline fun getCustomPreference( 73 | key: Preferences.Key, 74 | defaultValue: T, 75 | context: Context 76 | ): Flow { 77 | return context.dataStore.data.map { preferences -> 78 | val jsonString = preferences[key] 79 | 80 | jsonString?.let { string -> 81 | Json.decodeFromString(string) 82 | } ?: defaultValue 83 | 84 | }.distinctUntilChanged() 85 | } 86 | 87 | 88 | @Composable 89 | fun rememberIsLandscape(): Boolean { 90 | val config = LocalConfiguration.current 91 | 92 | return remember(config.orientation) { 93 | config.orientation == Configuration.ORIENTATION_LANDSCAPE 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/playlist/PlaylistDao.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data.playlist 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Query 6 | import androidx.room.Upsert 7 | import com.sosauce.cutemusic.domain.model.Playlist 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao 11 | interface PlaylistDao { 12 | 13 | @Upsert 14 | suspend fun upsertPlaylist(playlist: Playlist) 15 | 16 | @Delete 17 | suspend fun deletePlaylist(playlist: Playlist) 18 | 19 | @Query("SELECT * FROM playlist ORDER BY name ASC") 20 | fun getPlaylists(): Flow> 21 | // 22 | // @Query("UPDATE playlist SET name = :name, emoji = :emoji WHERE id =:id") 23 | // suspend fun updateNameAndEmoji( 24 | // id: Int, 25 | // name: String, 26 | // emoji: String 27 | // ) 28 | 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/playlist/PlaylistDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data.playlist 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.sosauce.cutemusic.data.MediaItemConverter 7 | import com.sosauce.cutemusic.domain.model.Playlist 8 | 9 | @Database( 10 | entities = [Playlist::class], 11 | version = 1 12 | ) 13 | @TypeConverters(MediaItemConverter::class) 14 | abstract class PlaylistDatabase : RoomDatabase() { 15 | abstract val dao: PlaylistDao 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/playlist/PlaylistState.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data.playlist 2 | 3 | data class PlaylistState( 4 | val emoji: String = "", 5 | val name: String = "" 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/data/states/MusicState.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.data.states 2 | 3 | import android.net.Uri 4 | import androidx.compose.runtime.Stable 5 | import com.sosauce.cutemusic.domain.model.Lyrics 6 | 7 | @Stable 8 | data class MusicState( 9 | val title: String = "", 10 | val artist: String = "", 11 | val artistId: Long = 0, 12 | val art: Uri? = null, 13 | val isPlaying: Boolean = false, 14 | val position: Long = 0L, 15 | val duration: Long = 0L, 16 | val uri: String = "", 17 | val path: String = "", 18 | val album: String = "", 19 | val albumId: Long = 0, 20 | val size: Long = 0, 21 | val speed: Float = 1.0f, 22 | val pitch: Float = 1.0f, 23 | val isPlayerReady: Boolean = false, 24 | val sleepTimerActive: Boolean = false, 25 | val mediaId: String = "", 26 | val mediaIndex: Int = 0, 27 | val loadedMedias: Map = emptyMap(), // Map of mediaId to index 28 | val lyrics: List = emptyList() 29 | ) 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.di 2 | 3 | import androidx.room.Room 4 | import com.sosauce.cutemusic.data.playlist.PlaylistDatabase 5 | import com.sosauce.cutemusic.domain.repository.MediaStoreHelper 6 | import com.sosauce.cutemusic.domain.repository.MediaStoreHelperImpl 7 | import com.sosauce.cutemusic.domain.repository.SafManager 8 | import com.sosauce.cutemusic.main.quickplay.QuickPlayViewModel 9 | import com.sosauce.cutemusic.ui.screens.metadata.MetadataViewModel 10 | import com.sosauce.cutemusic.ui.shared_components.MusicViewModel 11 | import com.sosauce.cutemusic.ui.shared_components.PlaylistViewModel 12 | import org.koin.android.ext.koin.androidApplication 13 | import org.koin.android.ext.koin.androidContext 14 | import org.koin.core.module.dsl.singleOf 15 | import org.koin.core.module.dsl.viewModelOf 16 | import org.koin.dsl.module 17 | 18 | val appModule = module { 19 | 20 | single { 21 | MediaStoreHelperImpl(androidContext()) 22 | } 23 | 24 | single { 25 | Room.databaseBuilder( 26 | context = androidApplication(), 27 | klass = PlaylistDatabase::class.java, 28 | name = "playlist.db" 29 | ).build().dao 30 | } 31 | 32 | 33 | singleOf(::SafManager) 34 | viewModelOf(::MusicViewModel) 35 | viewModelOf(::MetadataViewModel) 36 | viewModelOf(::PlaylistViewModel) 37 | viewModelOf(::QuickPlayViewModel) 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/domain/model/Album.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.domain.model 2 | 3 | data class Album( 4 | val id: Long, 5 | val name: String, 6 | val artist: String 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/domain/model/Artist.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.domain.model 2 | 3 | data class Artist( 4 | val id: Long, 5 | val name: String 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/domain/model/Folder.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.domain.model 2 | 3 | data class Folder( 4 | val name: String, 5 | val path: String 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/domain/model/Lyrics.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.domain.model 2 | 3 | import java.util.UUID 4 | 5 | data class Lyrics( 6 | val timestamp: Long = 0L, 7 | val lineLyrics: String = "", 8 | val id: String = UUID.randomUUID().toString() 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/domain/model/Playlist.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.domain.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity 7 | data class Playlist( 8 | @PrimaryKey(autoGenerate = true) 9 | val id: Int = 0, 10 | val emoji: String, 11 | val name: String, 12 | val musics: List // List of songs ID aka mediaId 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelper.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.domain.repository 2 | 3 | import android.net.Uri 4 | import androidx.activity.result.ActivityResultLauncher 5 | import androidx.activity.result.IntentSenderRequest 6 | import androidx.media3.common.MediaItem 7 | import com.sosauce.cutemusic.domain.model.Album 8 | import com.sosauce.cutemusic.domain.model.Artist 9 | import com.sosauce.cutemusic.domain.model.Folder 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | interface MediaStoreHelper { 13 | 14 | val musics: List 15 | val albums: List 16 | val artists: List 17 | val folders: List 18 | 19 | fun fetchMusics(): List 20 | fun fetchLatestMusics(): Flow> 21 | 22 | fun fetchAlbums(): List 23 | fun fetchLatestAlbums(): Flow> 24 | 25 | fun fetchArtists(): List 26 | fun fetchLatestArtists(): Flow> 27 | 28 | fun fetchFoldersWithMusics(): List 29 | fun fetchLatestFoldersWithMusics(): Flow> 30 | 31 | suspend fun deleteMusics( 32 | uris: List, 33 | intentSenderLauncher: ActivityResultLauncher 34 | ) 35 | 36 | suspend fun editMusic( 37 | uris: List, 38 | intentSenderLauncher: ActivityResultLauncher 39 | ) 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/domain/repository/SafManager.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.domain.repository 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.os.ParcelFileDescriptor 7 | import androidx.core.net.toUri 8 | import androidx.media3.common.MediaItem 9 | import androidx.media3.common.MediaMetadata 10 | import com.kyant.taglib.Metadata 11 | import com.kyant.taglib.TagLib 12 | import com.sosauce.cutemusic.data.datastore.getSafTracks 13 | import com.sosauce.cutemusic.utils.getUriFromByteArray 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.map 17 | import kotlinx.coroutines.withContext 18 | 19 | class SafManager( 20 | private val context: Context 21 | ) { 22 | 23 | 24 | fun fetchLatestSafTracks(): Flow> = getSafTracks(context) 25 | .map { tracks -> 26 | tracks.map { uri -> 27 | uriToMediaItem(uri.toUri()) 28 | } 29 | } 30 | 31 | 32 | private suspend fun uriToMediaItem(uri: Uri): MediaItem { 33 | return withContext(Dispatchers.IO) { 34 | context.contentResolver.openFileDescriptor(uri, "r")?.use { fd -> 35 | val metadata = loadAudioMetadata(fd) 36 | 37 | val title = metadata?.propertyMap?.get("TITLE")?.getOrNull(0) ?: "" 38 | val artist = metadata?.propertyMap?.get("ARTIST")?.joinToString(", ") ?: "" 39 | val album = metadata?.propertyMap?.get("ALBUM")?.getOrNull(0) 40 | val duration = metadata?.propertyMap?.get("DURATION")?.getOrNull(0) 41 | val artUri = 42 | TagLib.getFrontCover(fd.dup().detachFd())?.data?.getUriFromByteArray(context) 43 | 44 | MediaItem 45 | .Builder() 46 | .setUri(uri) 47 | .setMediaId(uri.hashCode().toString()) 48 | .setMediaMetadata( 49 | MediaMetadata 50 | .Builder() 51 | .setIsBrowsable(false) 52 | .setIsPlayable(true) 53 | .setTitle(title) 54 | .setArtist(artist) 55 | .setAlbumTitle(album) 56 | .setArtworkUri(artUri) 57 | .setDurationMs(duration?.toLong() ?: 0) 58 | .setExtras( 59 | Bundle() 60 | .apply { 61 | putString("folder", "SAF") 62 | putLong("size", fd.statSize) 63 | putString("path", "${uri.path}") 64 | putString("uri", uri.toString()) 65 | putLong("album_id", 0) 66 | putLong("artist_id", 0) 67 | putBoolean("is_saf", true) 68 | }).build() 69 | ) 70 | .build() 71 | } ?: throw IllegalArgumentException("Unable to open file descriptor for uri") 72 | } 73 | } 74 | 75 | 76 | private suspend fun loadAudioMetadata(songFd: ParcelFileDescriptor): Metadata? { 77 | val fd = songFd.dup()?.detachFd() ?: throw NullPointerException() 78 | 79 | return withContext(Dispatchers.IO) { 80 | TagLib.getMetadata(fd) 81 | } 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/main/App.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.main 2 | 3 | import android.app.Application 4 | import com.sosauce.cutemusic.di.appModule 5 | import org.koin.android.ext.koin.androidContext 6 | import org.koin.androix.startup.KoinStartup.onKoinStartup 7 | 8 | class App : Application() { 9 | init { 10 | @Suppress("OPT_IN_USAGE") 11 | onKoinStartup { 12 | androidContext(this@App) 13 | modules(appModule) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/main/AutoPlaybackService.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.main 2 | 3 | import android.content.Intent 4 | import android.media.MediaDescription 5 | import android.media.browse.MediaBrowser 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.service.media.MediaBrowserService 9 | import com.sosauce.cutemusic.domain.repository.MediaStoreHelperImpl 10 | import com.sosauce.cutemusic.utils.ROOT_ID 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.SupervisorJob 14 | import kotlinx.coroutines.flow.collectLatest 15 | import kotlinx.coroutines.launch 16 | 17 | class AutoPlaybackService : MediaBrowserService() { 18 | 19 | 20 | private val mediaStoreHelper by lazy { MediaStoreHelperImpl(this) } 21 | private val job = SupervisorJob() 22 | private val scope = CoroutineScope(Dispatchers.IO + job) 23 | 24 | 25 | override fun onGetRoot( 26 | clientPackageName: String, 27 | clientUid: Int, 28 | rootHints: Bundle? 29 | ): BrowserRoot = BrowserRoot(ROOT_ID, null) 30 | 31 | override fun onLoadChildren( 32 | parentId: String, 33 | result: Result?> 34 | ) { 35 | 36 | val mediaItems: MutableList = mutableListOf() 37 | 38 | if (ROOT_ID == parentId) { 39 | scope.launch { 40 | mediaStoreHelper.fetchLatestMusics().collectLatest { list -> 41 | list.forEach { mediaItem -> 42 | mediaItems.add( 43 | MediaBrowser.MediaItem( 44 | MediaDescription.Builder() 45 | .setMediaId(mediaItem.mediaId) 46 | .setTitle(mediaItem.mediaMetadata.title ?: "No title") 47 | .setIconUri(mediaItem.mediaMetadata.artworkUri ?: Uri.EMPTY) 48 | .build(), 49 | MediaBrowser.MediaItem.FLAG_PLAYABLE 50 | ) 51 | ) 52 | } 53 | } 54 | result.sendResult(mediaItems) 55 | } 56 | } else result.sendResult(listOf()) 57 | } 58 | 59 | override fun onDestroy() { 60 | super.onDestroy() 61 | job.cancel() 62 | } 63 | 64 | override fun onTaskRemoved(rootIntent: Intent?) { 65 | super.onTaskRemoved(rootIntent) 66 | job.cancel() 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.main 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.compose.setContent 9 | import androidx.activity.enableEdgeToEdge 10 | import androidx.compose.foundation.isSystemInDarkTheme 11 | import androidx.compose.material3.Scaffold 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.graphics.ImageBitmap 17 | import androidx.core.app.ActivityCompat 18 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 19 | import androidx.core.view.WindowCompat 20 | import com.sosauce.cutemusic.data.datastore.rememberAppTheme 21 | import com.sosauce.cutemusic.domain.model.Playlist 22 | import com.sosauce.cutemusic.ui.navigation.Nav 23 | import com.sosauce.cutemusic.ui.theme.CuteMusicTheme 24 | import com.sosauce.cutemusic.utils.CuteTheme 25 | 26 | class MainActivity : ComponentActivity() { 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | installSplashScreen() 30 | enableEdgeToEdge() 31 | val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 32 | arrayOf(Manifest.permission.READ_MEDIA_AUDIO) 33 | } else { 34 | arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) 35 | } 36 | 37 | ActivityCompat.requestPermissions( 38 | this, 39 | permission, 40 | 0 41 | ) 42 | try { 43 | val c = Playlist::class.java 44 | Log.d("FIELDS_DEBUG", "Fields in Playlist: ${c.declaredFields.joinToString { it.name }}") 45 | } catch (e: Exception) { 46 | Log.e("FIELDS_DEBUG", "Error getting fields", e) 47 | } 48 | setContent { 49 | val theme by rememberAppTheme() 50 | var artImageBitmap by remember { mutableStateOf(ImageBitmap(1, 1)) } 51 | val isSystemInDarkTheme = isSystemInDarkTheme() 52 | 53 | CuteMusicTheme(artImageBitmap = artImageBitmap) { 54 | 55 | WindowCompat 56 | .getInsetsController(window, window.decorView) 57 | .apply { 58 | 59 | val isLight = if (theme == CuteTheme.SYSTEM) !isSystemInDarkTheme else theme == CuteTheme.LIGHT 60 | 61 | isAppearanceLightStatusBars = isLight 62 | isAppearanceLightNavigationBars = isLight 63 | } 64 | 65 | Scaffold { _ -> 66 | Nav { imageBitmap -> 67 | artImageBitmap = imageBitmap 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | 75 | // override fun onDestroy() { 76 | // super.onDestroy() 77 | // sendBroadcast( 78 | // Intent( 79 | // "CM_CUR_PLAY_CHANGED" 80 | // ).apply { 81 | // putExtra("currentlyPlaying", "") 82 | // } 83 | // ) 84 | // } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.main 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.PendingIntent 5 | import android.appwidget.AppWidgetManager 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.os.Build 9 | import androidx.media3.common.AudioAttributes 10 | import androidx.media3.common.C 11 | import androidx.media3.common.MediaMetadata 12 | import androidx.media3.common.Player 13 | import androidx.media3.common.util.UnstableApi 14 | import androidx.media3.exoplayer.ExoPlayer 15 | import androidx.media3.session.DefaultMediaNotificationProvider 16 | import androidx.media3.session.MediaLibraryService 17 | import androidx.media3.session.MediaLibraryService.MediaLibrarySession 18 | import androidx.media3.session.MediaSession 19 | import com.sosauce.cutemusic.R 20 | import com.sosauce.cutemusic.ui.widgets.WidgetBroadcastReceiver 21 | import com.sosauce.cutemusic.ui.widgets.WidgetCallback 22 | import com.sosauce.cutemusic.utils.CUTE_MUSIC_ID 23 | import com.sosauce.cutemusic.utils.PACKAGE 24 | import com.sosauce.cutemusic.utils.WIDGET_NEW_DATA 25 | import com.sosauce.cutemusic.utils.WIDGET_NEW_IS_PLAYING 26 | 27 | 28 | class PlaybackService : MediaLibraryService(), MediaLibrarySession.Callback, Player.Listener, 29 | WidgetCallback { 30 | 31 | private var mediaLibrarySession: MediaLibrarySession? = null 32 | private val audioAttributes = AudioAttributes 33 | .Builder() 34 | .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) 35 | .setUsage(C.USAGE_MEDIA) 36 | .build() 37 | 38 | private val widgetReceiver = WidgetBroadcastReceiver() 39 | 40 | 41 | override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { 42 | super.onMediaMetadataChanged(mediaMetadata) 43 | val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE).apply { 44 | putExtra(WIDGET_NEW_DATA, WIDGET_NEW_DATA) 45 | putExtra("title", mediaMetadata.title.toString()) 46 | putExtra("artist", mediaMetadata.artist.toString()) 47 | putExtra("artUri", mediaMetadata.artworkUri.toString()) 48 | } 49 | 50 | sendBroadcast(intent) 51 | } 52 | 53 | override fun onIsPlayingChanged(isPlaying: Boolean) { 54 | super.onIsPlayingChanged(isPlaying) 55 | 56 | val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE).apply { 57 | putExtra(WIDGET_NEW_DATA, WIDGET_NEW_IS_PLAYING) 58 | putExtra("isPlaying", isPlaying) 59 | } 60 | 61 | sendBroadcast(intent) 62 | } 63 | 64 | override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? = 65 | mediaLibrarySession 66 | 67 | 68 | @SuppressLint("UnspecifiedRegisterReceiverFlag") 69 | @UnstableApi 70 | override fun onCreate() { 71 | super.onCreate() 72 | 73 | val player = ExoPlayer.Builder(applicationContext) 74 | .setAudioAttributes(audioAttributes, true) 75 | .setHandleAudioBecomingNoisy(true) 76 | .build() 77 | mediaLibrarySession = MediaLibrarySession 78 | .Builder(this, player, this) 79 | .setId(CUTE_MUSIC_ID) 80 | .setSessionActivity( 81 | PendingIntent.getActivity( 82 | this, 83 | 0, 84 | Intent(this, MainActivity::class.java), 85 | PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT 86 | ) 87 | ) 88 | .build() 89 | 90 | IntentFilter(PACKAGE).also { 91 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 92 | registerReceiver( 93 | widgetReceiver, 94 | it, 95 | RECEIVER_EXPORTED 96 | ) 97 | } else { 98 | registerReceiver(widgetReceiver, it) 99 | } 100 | } 101 | widgetReceiver.startCallback(this) 102 | 103 | setMediaNotificationProvider( 104 | DefaultMediaNotificationProvider.Builder(this).build().apply { 105 | setSmallIcon(R.drawable.music_note_rounded) 106 | } 107 | ) 108 | 109 | player.addListener(this) 110 | 111 | } 112 | 113 | 114 | override fun onDestroy() { 115 | mediaLibrarySession?.run { 116 | player.release() 117 | release() 118 | mediaLibrarySession = null 119 | } 120 | stopSelf() 121 | try { 122 | widgetReceiver.also { 123 | it.stopCallback() 124 | unregisterReceiver(it) 125 | } 126 | } catch(e: IllegalArgumentException) { 127 | return 128 | } 129 | super.onDestroy() 130 | } 131 | 132 | 133 | @UnstableApi 134 | override fun onTaskRemoved(rootIntent: Intent?) { 135 | super.onTaskRemoved(rootIntent) 136 | mediaLibrarySession?.run { 137 | player.release() 138 | release() 139 | mediaLibrarySession = null 140 | } 141 | widgetReceiver.also { 142 | it.stopCallback() 143 | unregisterReceiver(it) 144 | } 145 | pauseAllPlayersAndStopSelf() 146 | } 147 | 148 | 149 | companion object { 150 | private const val CURRENTLY_PLAYING_CHANGED = "CM_CUR_PLAY_CHANGED" 151 | } 152 | 153 | 154 | override fun skipToNext() { 155 | mediaLibrarySession?.player?.seekToNextMediaItem() 156 | } 157 | 158 | override fun playOrPause() { 159 | 160 | if (mediaLibrarySession?.player?.isPlaying == true) { 161 | mediaLibrarySession?.player?.pause() 162 | } else { 163 | mediaLibrarySession?.player?.play() 164 | } 165 | } 166 | 167 | override fun skipToPrevious() { 168 | mediaLibrarySession?.player?.seekToPrevious() 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayUiState.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.main.quickplay 2 | 3 | data class QuickPlayUiState( 4 | val isSongLoaded: Boolean = false, 5 | val title: String = "", 6 | val artist: String = "", 7 | val artUri: String = "", 8 | val duration: Long = 0, 9 | val currentPosition: Long = 0, 10 | val isPlaying: Boolean = false 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.main.quickplay 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.graphics.Bitmap 6 | import android.graphics.BitmapFactory 7 | import android.media.MediaMetadataRetriever 8 | import android.net.Uri 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.setValue 12 | import androidx.lifecycle.AndroidViewModel 13 | import androidx.lifecycle.viewModelScope 14 | import androidx.media3.common.AudioAttributes 15 | import androidx.media3.common.C 16 | import androidx.media3.common.MediaItem 17 | import androidx.media3.common.MediaMetadata 18 | import androidx.media3.common.Player 19 | import androidx.media3.exoplayer.ExoPlayer 20 | import com.sosauce.cutemusic.data.actions.PlayerActions 21 | import com.sosauce.cutemusic.data.states.MusicState 22 | import kotlinx.coroutines.delay 23 | import kotlinx.coroutines.flow.MutableStateFlow 24 | import kotlinx.coroutines.flow.asStateFlow 25 | import kotlinx.coroutines.flow.update 26 | import kotlinx.coroutines.launch 27 | 28 | class QuickPlayViewModel( 29 | application: Application 30 | ) : AndroidViewModel(application) { 31 | 32 | private val audioAttributes = AudioAttributes 33 | .Builder() 34 | .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) 35 | .setUsage(C.USAGE_MEDIA) 36 | .build() 37 | 38 | private val listener = object : Player.Listener { 39 | override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { 40 | super.onMediaMetadataChanged(mediaMetadata) 41 | _musicState.update { 42 | it.copy( 43 | title = mediaMetadata.title.toString(), 44 | artist = mediaMetadata.artist.toString(), 45 | art = mediaMetadata.artworkUri, 46 | ) 47 | } 48 | } 49 | 50 | override fun onIsPlayingChanged(isPlaying: Boolean) { 51 | super.onIsPlayingChanged(isPlaying) 52 | _musicState.update { 53 | it.copy( 54 | isPlaying = isPlaying 55 | ) 56 | } 57 | } 58 | 59 | override fun onEvents(player: Player, events: Player.Events) { 60 | super.onEvents(player, events) 61 | viewModelScope.launch { 62 | while (player.isPlaying) { 63 | _musicState.update { 64 | it.copy( 65 | duration = player.duration, 66 | position = player.currentPosition 67 | ) 68 | } 69 | delay(500) 70 | } 71 | } 72 | } 73 | } 74 | 75 | 76 | private val player = ExoPlayer.Builder(application) 77 | .setAudioAttributes(audioAttributes, true) 78 | .setHandleAudioBecomingNoisy(true) 79 | .build() 80 | .apply { 81 | addListener(listener) 82 | } 83 | 84 | var isSongLoaded by mutableStateOf(false) 85 | 86 | private val _musicState = MutableStateFlow(MusicState()) 87 | val musicState = _musicState.asStateFlow() 88 | 89 | 90 | init { 91 | viewModelScope.launch { 92 | while (player.mediaItemCount == 0) delay(300) 93 | 94 | isSongLoaded = true 95 | } 96 | } 97 | 98 | override fun onCleared() { 99 | super.onCleared() 100 | player.removeListener(listener) 101 | player.release() 102 | } 103 | 104 | 105 | fun loadSong(uri: Uri) { 106 | val mediaItem = MediaItem.fromUri(uri) 107 | player.addMediaItem(mediaItem) 108 | player.prepare() 109 | player.play() 110 | } 111 | 112 | fun loadAlbumArt(context: Context, uri: Uri): Bitmap? { 113 | val retriever = MediaMetadataRetriever() 114 | return try { 115 | retriever.setDataSource(context, uri) 116 | val art = retriever.embeddedPicture 117 | art?.let { BitmapFactory.decodeByteArray(it, 0, it.size) } 118 | } catch (e: Exception) { 119 | e.printStackTrace() 120 | null 121 | } finally { 122 | retriever.release() 123 | } 124 | } 125 | 126 | 127 | fun handlePlayerAction(action: PlayerActions) { 128 | when (action) { 129 | is PlayerActions.PlayOrPause -> if (player.isPlaying) player.pause() else player.play() 130 | is PlayerActions.UpdateCurrentPosition -> { 131 | _musicState.update { 132 | it.copy( 133 | position = action.position 134 | ) 135 | } 136 | } 137 | 138 | is PlayerActions.SeekToSlider -> player.seekTo(action.position) 139 | is PlayerActions.SeekTo -> player.seekTo(player.currentPosition + action.position) 140 | is PlayerActions.RewindTo -> player.seekTo(player.currentPosition - action.position) 141 | else -> Unit 142 | } 143 | } 144 | 145 | 146 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/navigation/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.navigation 2 | 3 | import androidx.navigation3.runtime.NavKey 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed class Screen(): NavKey { 8 | @Serializable 9 | data object Main : Screen() 10 | 11 | @Serializable 12 | data object NowPlaying : Screen() 13 | 14 | @Serializable 15 | data object Settings : Screen() 16 | 17 | @Serializable 18 | data object Blacklisted : Screen() 19 | 20 | @Serializable 21 | data object Albums : Screen() 22 | 23 | @Serializable 24 | data object Artists : Screen() 25 | 26 | @Serializable 27 | data object Playlists : Screen() 28 | 29 | @Serializable 30 | data object Saf : Screen() 31 | 32 | @Serializable 33 | data class AlbumsDetails( 34 | val id: Long 35 | ) : Screen() 36 | 37 | @Serializable 38 | data class ArtistsDetails( 39 | val id: Long 40 | ) : Screen() 41 | 42 | @Serializable 43 | data class PlaylistDetails( 44 | val id: Int 45 | ) : Screen() 46 | 47 | @Serializable 48 | data class MetadataEditor( 49 | val id: String 50 | ) : Screen() 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsLandscape.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSharedTransitionApi::class) 2 | 3 | package com.sosauce.cutemusic.ui.screens.album 4 | 5 | import android.net.Uri 6 | import androidx.compose.animation.ExperimentalSharedTransitionApi 7 | import androidx.compose.animation.SharedTransitionScope 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.displayCutoutPadding 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.height 15 | import androidx.compose.foundation.layout.navigationBarsPadding 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.layout.statusBarsPadding 19 | import androidx.compose.foundation.layout.width 20 | import androidx.compose.foundation.lazy.LazyColumn 21 | import androidx.compose.foundation.lazy.items 22 | import androidx.compose.foundation.shape.RoundedCornerShape 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Scaffold 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.draw.clip 29 | import androidx.compose.ui.layout.ContentScale 30 | import androidx.compose.ui.res.pluralStringResource 31 | import androidx.compose.ui.res.stringResource 32 | import androidx.compose.ui.unit.dp 33 | import androidx.media3.common.MediaItem 34 | import androidx.navigation3.ui.LocalNavAnimatedContentScope 35 | import coil3.compose.AsyncImage 36 | import com.sosauce.cutemusic.R 37 | import com.sosauce.cutemusic.data.actions.MediaItemActions 38 | import com.sosauce.cutemusic.data.actions.PlayerActions 39 | import com.sosauce.cutemusic.data.states.MusicState 40 | import com.sosauce.cutemusic.domain.model.Album 41 | import com.sosauce.cutemusic.ui.navigation.Screen 42 | import com.sosauce.cutemusic.ui.shared_components.CuteNavigationButton 43 | import com.sosauce.cutemusic.ui.shared_components.CuteText 44 | import com.sosauce.cutemusic.ui.shared_components.LocalMusicListItem 45 | import com.sosauce.cutemusic.utils.ImageUtils 46 | 47 | @Composable 48 | fun SharedTransitionScope.AlbumDetailsLandscape( 49 | musics: List, 50 | album: Album, 51 | onNavigateUp: () -> Unit, 52 | onNavigate: (Screen) -> Unit, 53 | musicState: MusicState, 54 | onHandlePlayerActions: (PlayerActions) -> Unit, 55 | onLoadMetadata: (String, Uri) -> Unit = { _, _ -> }, 56 | onHandleMediaItemAction: (MediaItemActions) -> Unit, 57 | ) { 58 | 59 | 60 | Box( 61 | modifier = Modifier 62 | .fillMaxSize() 63 | .displayCutoutPadding() 64 | ) { 65 | Row( 66 | modifier = Modifier.fillMaxSize() 67 | ) { 68 | Column { 69 | AsyncImage( 70 | model = ImageUtils.getAlbumArt(album.id), 71 | stringResource(R.string.artwork), 72 | modifier = Modifier 73 | .statusBarsPadding() 74 | .size(200.dp) 75 | .sharedElement( 76 | sharedContentState = rememberSharedContentState(key = album.id), 77 | animatedVisibilityScope = LocalNavAnimatedContentScope.current, 78 | ) 79 | .clip(RoundedCornerShape(10)), 80 | contentScale = ContentScale.Crop 81 | ) 82 | Spacer(Modifier.height(10.dp)) 83 | CuteText( 84 | text = album.name, 85 | modifier = Modifier.sharedElement( 86 | sharedContentState = rememberSharedContentState(key = album.name + album.id), 87 | animatedVisibilityScope = LocalNavAnimatedContentScope.current, 88 | ) 89 | ) 90 | CuteText( 91 | text = album.artist, 92 | color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f), 93 | modifier = Modifier.sharedElement( 94 | sharedContentState = rememberSharedContentState(key = album.artist + album.id), 95 | animatedVisibilityScope = LocalNavAnimatedContentScope.current, 96 | ) 97 | ) 98 | CuteText(pluralStringResource(R.plurals.tracks, musics.size, musics.size)) 99 | Spacer(modifier = Modifier.width(5.dp)) 100 | } 101 | Scaffold { paddingValues -> 102 | LazyColumn( 103 | contentPadding = paddingValues 104 | ) { 105 | items( 106 | items = musics, 107 | key = { it.mediaId } 108 | ) { music -> 109 | LocalMusicListItem( 110 | modifier = Modifier 111 | .padding(horizontal = 5.dp), 112 | music = music, 113 | currentMusicUri = musicState.uri, 114 | onShortClick = { 115 | onHandlePlayerActions( 116 | PlayerActions.StartPlayback( 117 | it 118 | ) 119 | ) 120 | }, 121 | isPlayerReady = musicState.isPlayerReady, 122 | onLoadMetadata = onLoadMetadata, 123 | onHandleMediaItemAction = onHandleMediaItemAction, 124 | onHandlePlayerActions = onHandlePlayerActions, 125 | onNavigate = onNavigate 126 | ) 127 | } 128 | } 129 | } 130 | } 131 | 132 | CuteNavigationButton( 133 | modifier = Modifier 134 | .navigationBarsPadding() 135 | .align(Alignment.BottomStart) 136 | ) { onNavigateUp() } 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/SortingDropdownMenu.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.sosauce.cutemusic.ui.screens.main.components 4 | 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.DropdownMenu 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.RadioButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.unit.dp 12 | import com.sosauce.cutemusic.R 13 | import com.sosauce.cutemusic.ui.shared_components.CuteDropdownMenuItem 14 | import com.sosauce.cutemusic.ui.shared_components.CuteText 15 | 16 | @Composable 17 | fun SortingDropdownMenu( 18 | expanded: Boolean, 19 | onDismissRequest: () -> Unit, 20 | isSortedByASC: Boolean, 21 | onChangeSorting: (Boolean) -> Unit, 22 | additionalActions: (@Composable () -> Unit)? = null 23 | ) { 24 | DropdownMenu( 25 | expanded = expanded, 26 | onDismissRequest = onDismissRequest, 27 | shape = RoundedCornerShape(24.dp), 28 | ) { 29 | additionalActions?.invoke() 30 | CuteDropdownMenuItem( 31 | onClick = { onChangeSorting(true) }, 32 | text = { CuteText(stringResource(R.string.ascending)) }, 33 | leadingIcon = { 34 | RadioButton( 35 | selected = isSortedByASC, 36 | onClick = null 37 | ) 38 | } 39 | ) 40 | CuteDropdownMenuItem( 41 | onClick = { onChangeSorting(false) }, 42 | text = { CuteText(stringResource(R.string.descending)) }, 43 | leadingIcon = { 44 | RadioButton( 45 | selected = !isSortedByASC, 46 | onClick = null 47 | ) 48 | } 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataState.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.screens.metadata 2 | 3 | import android.net.Uri 4 | import androidx.compose.runtime.mutableStateMapOf 5 | import androidx.compose.runtime.snapshots.SnapshotStateMap 6 | import com.kyant.taglib.AudioProperties 7 | import com.kyant.taglib.Metadata 8 | import com.kyant.taglib.Picture 9 | 10 | data class MetadataState( 11 | val mutablePropertiesMap: SnapshotStateMap = mutableStateMapOf(), 12 | val songPath: String = "", 13 | val songUri: Uri = Uri.EMPTY, 14 | val metadata: Metadata? = null, 15 | val audioProperties: AudioProperties? = null, 16 | val art: Picture? = null, 17 | val newArtUri: Uri = Uri.EMPTY 18 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Artwork.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3ExpressiveApi::class) 2 | 3 | package com.sosauce.cutemusic.ui.screens.playing.components 4 | 5 | import androidx.compose.animation.Crossfade 6 | import androidx.compose.animation.ExperimentalSharedTransitionApi 7 | import androidx.compose.animation.SharedTransitionScope 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.PaddingValues 10 | import androidx.compose.foundation.layout.aspectRatio 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.wrapContentSize 13 | import androidx.compose.foundation.pager.HorizontalPager 14 | import androidx.compose.foundation.pager.rememberPagerState 15 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.LaunchedEffect 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableIntStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.runtime.snapshotFlow 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.draw.clip 26 | import androidx.compose.ui.graphics.graphicsLayer 27 | import androidx.compose.ui.layout.ContentScale 28 | import androidx.compose.ui.res.stringResource 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.unit.lerp 31 | import androidx.media3.common.MediaItem 32 | import coil3.compose.AsyncImage 33 | import com.sosauce.cutemusic.R 34 | import com.sosauce.cutemusic.data.actions.PlayerActions 35 | import com.sosauce.cutemusic.data.datastore.rememberCarousel 36 | import com.sosauce.cutemusic.data.datastore.rememberNpArtShape 37 | import com.sosauce.cutemusic.data.datastore.rememberShouldApplyShuffle 38 | import com.sosauce.cutemusic.data.states.MusicState 39 | import com.sosauce.cutemusic.utils.ImageUtils 40 | import com.sosauce.cutemusic.utils.toShape 41 | import kotlinx.coroutines.flow.filter 42 | import kotlinx.coroutines.flow.first 43 | import kotlin.math.absoluteValue 44 | 45 | @Composable 46 | fun SharedTransitionScope.Artwork( 47 | pagerModifier: Modifier = Modifier, 48 | loadedMedias: List = emptyList(), 49 | musicState: MusicState, 50 | onHandlePlayerActions: (PlayerActions) -> Unit, 51 | ) { 52 | val useCarousel by rememberCarousel() 53 | val artShape by rememberNpArtShape() 54 | val useShuffle by rememberShouldApplyShuffle() 55 | val pagerState = 56 | rememberPagerState(initialPage = loadedMedias.indexOfFirst { it.mediaId == musicState.mediaId }) { loadedMedias.size } 57 | 58 | 59 | if (useCarousel) { 60 | var lastPage by remember { mutableIntStateOf(loadedMedias.indexOfFirst { it.mediaId == musicState.mediaId }) } 61 | 62 | LaunchedEffect(pagerState.settledPage) { 63 | if (musicState.mediaIndex == pagerState.settledPage) return@LaunchedEffect 64 | if (pagerState.settledPage != lastPage) { 65 | snapshotFlow { pagerState.isScrollInProgress } 66 | .filter { !it } 67 | .first() 68 | if (useShuffle) { 69 | onHandlePlayerActions(PlayerActions.PlayRandom) 70 | } else onHandlePlayerActions(PlayerActions.SeekToMusicIndex(pagerState.settledPage)) 71 | lastPage = pagerState.settledPage 72 | } 73 | } 74 | 75 | 76 | LaunchedEffect(musicState.mediaIndex) { 77 | pagerState.animateScrollToPage(musicState.mediaIndex) 78 | } 79 | 80 | HorizontalPager( 81 | state = pagerState, 82 | key = { loadedMedias[it].mediaId }, 83 | contentPadding = PaddingValues(horizontal = 30.dp), 84 | modifier = pagerModifier 85 | ) { page -> 86 | Box( 87 | modifier = Modifier 88 | .aspectRatio(1f), 89 | contentAlignment = Alignment.Center 90 | ) { 91 | AsyncImage( 92 | model = loadedMedias[page].mediaMetadata.artworkUri, 93 | contentDescription = stringResource(R.string.artwork), 94 | modifier = Modifier 95 | .graphicsLayer { 96 | val pageOffset = 97 | (pagerState.currentPage - page + pagerState.currentPageOffsetFraction).absoluteValue 98 | 99 | lerp( 100 | start = 75.dp, 101 | stop = 100.dp, 102 | fraction = 1f - pageOffset.coerceIn(0f, 1f) 103 | ).also { scale -> 104 | scaleY = scale / 100.dp 105 | } 106 | } 107 | // .sharedElement( 108 | // state = rememberSharedContentState(key = SharedTransitionKeys.MUSIC_ARTWORK + musicState.currentMediaId), 109 | // animatedVisibilityScope = animatedVisibilityScope 110 | // 111 | // ) 112 | .fillMaxSize(0.95f) 113 | .clip(artShape.toShape()), 114 | contentScale = ContentScale.Crop 115 | ) 116 | } 117 | } 118 | 119 | } else { 120 | Crossfade( 121 | targetState = musicState.art, 122 | modifier = Modifier 123 | .aspectRatio(1f) 124 | .wrapContentSize() 125 | ) { 126 | AsyncImage( 127 | model = ImageUtils.imageRequester(it), 128 | contentDescription = stringResource(R.string.artwork), 129 | modifier = Modifier 130 | .fillMaxSize(0.9f) 131 | // .sharedElement( 132 | // sharedContentState = rememberSharedContentState(musicState.mediaId), 133 | // animatedVisibilityScope = LocalNavAnimatedContentScope.current 134 | // ) 135 | .clip(artShape.toShape()), 136 | contentScale = ContentScale.Crop 137 | ) 138 | } 139 | } 140 | } 141 | 142 | 143 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/CuteSlider.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) 2 | 3 | package com.sosauce.cutemusic.ui.screens.playing.components 4 | 5 | import androidx.compose.animation.core.animateFloatAsState 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.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.runtime.Composable 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.unit.dp 21 | import com.sosauce.cutemusic.data.actions.PlayerActions 22 | import com.sosauce.cutemusic.data.datastore.rememberSliderStyle 23 | import com.sosauce.cutemusic.data.states.MusicState 24 | import com.sosauce.cutemusic.ui.shared_components.CuteText 25 | import com.sosauce.cutemusic.utils.formatToReadableTime 26 | 27 | @Composable 28 | fun CuteSlider( 29 | musicState: MusicState, 30 | onHandlePlayerActions: (PlayerActions) -> Unit, 31 | ) { 32 | val sliderStyle by rememberSliderStyle() 33 | var tempSliderValue by remember { mutableStateOf(null) } 34 | val value by animateFloatAsState( 35 | targetValue = tempSliderValue ?: musicState.position.toFloat() 36 | ) 37 | val sliderState = rememberCuteSliderState( 38 | value = value, 39 | onValueChange = { tempSliderValue = it }, 40 | onValueChangeFinished = { 41 | tempSliderValue?.let { 42 | onHandlePlayerActions( 43 | PlayerActions.UpdateCurrentPosition(it.toLong()) 44 | ) 45 | onHandlePlayerActions( 46 | PlayerActions.SeekToSlider(it.toLong()) 47 | ) 48 | } 49 | tempSliderValue = null 50 | }, 51 | valueRange = 0f..musicState.duration.toFloat(), 52 | enabled = true 53 | ) 54 | 55 | 56 | Column( 57 | modifier = Modifier.padding(horizontal = 15.dp) 58 | ) { 59 | Row( 60 | horizontalArrangement = Arrangement.SpaceBetween, 61 | modifier = Modifier.fillMaxWidth() 62 | ) { 63 | CuteText( 64 | text = musicState.position.formatToReadableTime(), 65 | color = MaterialTheme.colorScheme.primary 66 | ) 67 | CuteText( 68 | text = musicState.duration.formatToReadableTime(), 69 | color = MaterialTheme.colorScheme.primary 70 | ) 71 | } 72 | sliderStyle.toSlider( 73 | state = sliderState, 74 | isPlaying = musicState.isPlaying 75 | ) 76 | 77 | // Slider( 78 | // value = value, 79 | // onValueChange = { tempSliderValue = it }, 80 | // onValueChangeFinished = { 81 | // tempSliderValue?.let { 82 | // onHandlePlayerActions( 83 | // PlayerActions.UpdateCurrentPosition(it.toLong()) 84 | // ) 85 | // onHandlePlayerActions( 86 | // PlayerActions.SeekToSlider(it.toLong()) 87 | // ) 88 | // } 89 | // tempSliderValue = null 90 | // }, 91 | // track = { sliderState -> 92 | // if (useClassicSlider) { 93 | // SliderDefaults.Track( 94 | // sliderState = sliderState, 95 | // drawStopIndicator = null, 96 | // thumbTrackGapSize = 0.dp, 97 | // modifier = Modifier.height(4.dp) 98 | // ) 99 | // } else { 100 | // val amplitude by animateDpAsState( 101 | // targetValue = if (musicState.isPlaying && !isDragging) 5.dp else 0.dp, 102 | // animationSpec = MotionScheme.expressive().slowSpatialSpec() 103 | // ) 104 | // SquigglySlider.Track( 105 | // interactionSource = rememberInteractionSource(), 106 | // colors = SliderDefaults.colors(), 107 | // enabled = true, 108 | // sliderState = sliderState, 109 | // squigglesSpec = SquigglySlider.SquigglesSpec( 110 | // amplitude = amplitude, 111 | // wavelength = 45.dp 112 | // ) 113 | // ) 114 | // } 115 | // }, 116 | // thumb = { 117 | // val thumbWidth by animateDpAsState( 118 | // targetValue = when (useClassicSlider) { 119 | // true -> if (isDragging) 28.dp else 20.dp 120 | // false -> if (isDragging) 12.dp else 4.dp 121 | // } 122 | // ) 123 | // SliderDefaults.Thumb( 124 | // interactionSource = rememberInteractionSource(), 125 | // thumbSize = DpSize( 126 | // width = thumbWidth, 127 | // height = if (useClassicSlider) 20.dp else 22.dp 128 | // ), 129 | // ) 130 | // }, 131 | // valueRange = 0f..musicState.duration.toFloat(), 132 | // interactionSource = interactionSource 133 | // ) 134 | } 135 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/CuteSquigglySlider.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.sosauce.cutemusic.ui.screens.playing.components 4 | 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.Slider 8 | import androidx.compose.material3.SliderColors 9 | import androidx.compose.material3.SliderDefaults 10 | import androidx.compose.material3.SliderState 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.DpSize 15 | import androidx.compose.ui.unit.coerceAtLeast 16 | import androidx.compose.ui.unit.dp 17 | import me.saket.squiggles.SquigglySlider 18 | import me.saket.squiggles.SquigglySlider.SquigglesAnimator 19 | import me.saket.squiggles.SquigglySlider.SquigglesSpec 20 | 21 | /** 22 | * Squiggly slider but you can pass a thumb 23 | */ 24 | @Composable 25 | fun CuteSquigglySlider( 26 | value: Float, 27 | onValueChange: (Float) -> Unit, 28 | modifier: Modifier = Modifier, 29 | enabled: Boolean = true, 30 | valueRange: ClosedFloatingPointRange = 0f..1f, 31 | onValueChangeFinished: (() -> Unit)? = null, 32 | colors: SliderColors = SliderDefaults.colors(), 33 | squigglesSpec: SquigglesSpec = SquigglesSpec(), 34 | squigglesAnimator: SquigglesAnimator = SquigglySlider.rememberSquigglesAnimator(), 35 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 36 | thumb: @Composable (SliderState) -> Unit = { 37 | SquigglySlider.Thumb( 38 | interactionSource = interactionSource, 39 | colors = colors, 40 | enabled = enabled, 41 | thumbSize = DpSize( 42 | width = squigglesSpec.strokeWidth.coerceAtLeast(4.dp), 43 | height = (squigglesSpec.strokeWidth * 4).coerceAtLeast(16.dp), 44 | ) 45 | ) 46 | }, 47 | ) { 48 | Slider( 49 | value = value, 50 | onValueChange = onValueChange, 51 | modifier = modifier, 52 | enabled = enabled, 53 | onValueChangeFinished = onValueChangeFinished, 54 | colors = colors, 55 | interactionSource = interactionSource, 56 | thumb = thumb, 57 | track = { sliderState -> 58 | SquigglySlider.Track( 59 | interactionSource = interactionSource, 60 | colors = colors, 61 | enabled = enabled, 62 | sliderState = sliderState, 63 | squigglesSpec = squigglesSpec, 64 | squigglesAnimator = squigglesAnimator, 65 | ) 66 | }, 67 | valueRange = valueRange 68 | ) 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/CuteTimePicker.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.sosauce.cutemusic.ui.screens.playing.components 4 | 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.material3.TimePicker 10 | import androidx.compose.material3.TimePickerDialog 11 | import androidx.compose.material3.rememberTimePickerState 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.unit.sp 18 | import com.sosauce.cutemusic.R 19 | import com.sosauce.cutemusic.ui.shared_components.CuteText 20 | 21 | @Composable 22 | fun CuteTimePicker( 23 | initialMillis: Long = 0, 24 | onDismissRequest: () -> Unit, 25 | onSetTimer: (hours: Int, minutes: Int) -> Unit 26 | ) { 27 | 28 | 29 | val initialSeconds = remember { initialMillis / 1000 } 30 | val initialMinutes = remember { (initialSeconds / 60) % 60 } 31 | val initialHours = remember { (initialMinutes / 60) % 24 } 32 | 33 | // is24Hour is set to true to not show the AM/PM selector, as this time picker is used to start a countdown anyways 34 | val timePickerState = rememberTimePickerState( 35 | is24Hour = true, 36 | initialMinute = initialMinutes.toInt(), 37 | initialHour = initialHours.toInt(), 38 | ) 39 | 40 | TimePickerDialog( 41 | title = { 42 | CuteText( 43 | text = stringResource(R.string.set_sleep_timer), 44 | fontSize = 24.sp 45 | ) 46 | }, 47 | confirmButton = { 48 | TextButton( 49 | onClick = { onSetTimer(timePickerState.hour, timePickerState.minute) } 50 | ) { 51 | CuteText(stringResource(R.string.okay)) 52 | } 53 | }, 54 | dismissButton = { 55 | TextButton( 56 | onClick = onDismissRequest 57 | ) { 58 | CuteText(stringResource(R.string.cancel)) 59 | } 60 | }, 61 | onDismissRequest = onDismissRequest 62 | ) { 63 | Spacer(Modifier.height(10.dp)) 64 | TimePicker(timePickerState) 65 | } 66 | 67 | // AlertDialog( 68 | // text = { TimePicker(timePickerState) }, 69 | // title = { 70 | // CuteText(stringResource(R.string.set_sleep_timer)) 71 | // }, 72 | // confirmButton = { 73 | // TextButton( 74 | // onClick = { onSetTimer(timePickerState.hour, timePickerState.minute) } 75 | // ) { 76 | // CuteText(stringResource(R.string.okay)) 77 | // } 78 | // }, 79 | // dismissButton = { 80 | // TextButton( 81 | // onClick = onDismissRequest 82 | // ) { 83 | // CuteText(stringResource(R.string.cancel)) 84 | // } 85 | // }, 86 | // onDismissRequest = onDismissRequest 87 | // ) 88 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/QueueSheet.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.sosauce.cutemusic.ui.screens.playing.components 4 | 5 | import androidx.compose.animation.core.animateFloatAsState 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.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.ModalBottomSheet 11 | import androidx.compose.material3.rememberModalBottomSheetState 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.draw.scale 19 | import androidx.media3.common.MediaItem 20 | import com.sosauce.cutemusic.data.actions.PlayerActions 21 | import com.sosauce.cutemusic.data.states.MusicState 22 | import com.sosauce.cutemusic.ui.shared_components.QueueMusicListItem 23 | import com.sosauce.cutemusic.utils.thenIf 24 | import sh.calvin.reorderable.ReorderableItem 25 | import sh.calvin.reorderable.rememberReorderableLazyListState 26 | 27 | @Composable 28 | fun QueueSheet( 29 | onDismissRequest: () -> Unit, 30 | loadedMedias: List, 31 | onHandlePlayerAction: (PlayerActions) -> Unit, 32 | musicState: MusicState 33 | ) { 34 | val lazyListState = rememberLazyListState() 35 | val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to -> 36 | onHandlePlayerAction( 37 | PlayerActions.ReArrangeQueue(from.index, to.index) 38 | ) 39 | } 40 | ModalBottomSheet( 41 | onDismissRequest = onDismissRequest, 42 | sheetState = rememberModalBottomSheetState(true), 43 | ) { 44 | LazyColumn(state = lazyListState) { 45 | items( 46 | items = loadedMedias, 47 | key = { it.mediaId } 48 | ) { music -> 49 | ReorderableItem(reorderableLazyListState, key = music.mediaId) { isDragging -> 50 | val scale by animateFloatAsState( 51 | targetValue = if (isDragging) 1.05f else 1f 52 | ) 53 | QueueMusicListItem( 54 | modifier = Modifier.scale(scale), 55 | music = music, 56 | currentMusicUri = musicState.uri, 57 | onHandlePlayerActions = onHandlePlayerAction 58 | ) 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/RateAdjustmentDialog.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.screens.playing.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 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.text.KeyboardActions 10 | import androidx.compose.foundation.text.KeyboardOptions 11 | import androidx.compose.material3.OutlinedTextField 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.focus.focusRequester 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.text.TextRange 22 | import androidx.compose.ui.text.TextStyle 23 | import androidx.compose.ui.text.input.KeyboardType 24 | import androidx.compose.ui.text.input.TextFieldValue 25 | import androidx.compose.ui.text.style.TextAlign 26 | import androidx.compose.ui.unit.dp 27 | import com.sosauce.cutemusic.R 28 | import com.sosauce.cutemusic.ui.shared_components.CuteText 29 | import com.sosauce.cutemusic.utils.rememberFocusRequester 30 | 31 | @Composable 32 | fun RateAdjustmentDialog( 33 | rate: Float, 34 | onSetNewRate: (Float) -> Unit, 35 | ) { 36 | 37 | val focusRequest = rememberFocusRequester() 38 | LaunchedEffect(Unit) { focusRequest.requestFocus() } 39 | 40 | var newRate by remember { mutableStateOf("%.2f".format(rate)) } 41 | var textFieldValue by remember { 42 | mutableStateOf( 43 | TextFieldValue( 44 | text = newRate, 45 | selection = TextRange(newRate.length) 46 | ) 47 | ) 48 | } 49 | 50 | 51 | Column { 52 | CuteText(stringResource(id = R.string.new_rate)) 53 | Spacer(Modifier.height(10.dp)) 54 | Row( 55 | modifier = Modifier.fillMaxWidth(), 56 | horizontalArrangement = Arrangement.Center 57 | ) { 58 | OutlinedTextField( 59 | value = textFieldValue, 60 | onValueChange = { 61 | textFieldValue = it 62 | newRate = it.text 63 | }, 64 | singleLine = true, 65 | keyboardActions = KeyboardActions( 66 | onDone = { 67 | if (newRate.toFloat() > 2.0f) { 68 | onSetNewRate(2.0f) 69 | } else if (newRate.toFloat() < 0.5f) { 70 | onSetNewRate(0.5f) 71 | } else { 72 | onSetNewRate(newRate.toFloat()) 73 | } 74 | } 75 | ), 76 | keyboardOptions = KeyboardOptions( 77 | keyboardType = KeyboardType.Number 78 | ), 79 | modifier = Modifier 80 | .fillMaxWidth(0.5f) 81 | .focusRequester(focusRequest), 82 | textStyle = TextStyle( 83 | textAlign = TextAlign.Center 84 | ) 85 | ) 86 | } 87 | } 88 | } 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/playlists/CreatePlaylistDialog.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.sosauce.cutemusic.ui.screens.playlists 4 | 5 | import androidx.compose.animation.AnimatedContent 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.rememberScrollState 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.foundation.verticalScroll 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.rounded.Close 17 | import androidx.compose.material3.AlertDialog 18 | import androidx.compose.material3.ExperimentalMaterial3Api 19 | import androidx.compose.material3.Icon 20 | import androidx.compose.material3.IconButton 21 | import androidx.compose.material3.ModalBottomSheet 22 | import androidx.compose.material3.OutlinedTextField 23 | import androidx.compose.material3.TextButton 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.draw.clip 32 | import androidx.compose.ui.res.painterResource 33 | import androidx.compose.ui.res.stringResource 34 | import androidx.compose.ui.unit.dp 35 | import androidx.compose.ui.unit.sp 36 | import androidx.compose.ui.viewinterop.AndroidView 37 | import androidx.emoji2.emojipicker.EmojiPickerView 38 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 39 | import com.sosauce.cutemusic.R 40 | import com.sosauce.cutemusic.data.actions.PlaylistActions 41 | import com.sosauce.cutemusic.ui.shared_components.CuteText 42 | import com.sosauce.cutemusic.ui.shared_components.PlaylistViewModel 43 | import org.koin.androidx.compose.koinViewModel 44 | 45 | @Composable 46 | fun CreatePlaylistDialog( 47 | onDismissRequest: () -> Unit 48 | ) { 49 | val playlistViewModel = koinViewModel() 50 | val playlists by playlistViewModel.allPlaylists.collectAsStateWithLifecycle() 51 | val playlistState by playlistViewModel.state.collectAsStateWithLifecycle() 52 | var showEmojiPicker by remember { mutableStateOf(false) } 53 | 54 | if (showEmojiPicker) { 55 | ModalBottomSheet( 56 | onDismissRequest = { showEmojiPicker = false }, 57 | dragHandle = null 58 | ) { 59 | AndroidView( 60 | modifier = Modifier 61 | .fillMaxWidth() 62 | .verticalScroll(rememberScrollState()), 63 | factory = { ctx -> 64 | EmojiPickerView(ctx).apply { 65 | setOnEmojiPickedListener(onEmojiPickedListener = { 66 | playlistViewModel.handlePlaylistActions(PlaylistActions.UpdateStateEmoji(it.emoji)) 67 | }) 68 | } 69 | } 70 | ) 71 | } 72 | } 73 | 74 | 75 | AlertDialog( 76 | onDismissRequest = onDismissRequest, 77 | title = { CuteText(stringResource(R.string.create_playlist)) }, 78 | confirmButton = { 79 | TextButton( 80 | onClick = { 81 | playlistViewModel.handlePlaylistActions(PlaylistActions.CreatePlaylist) 82 | onDismissRequest() 83 | } 84 | ) { 85 | CuteText(stringResource(R.string.create)) 86 | } 87 | }, 88 | dismissButton = { 89 | TextButton( 90 | onClick = onDismissRequest 91 | ) { 92 | CuteText(stringResource(R.string.cancel)) 93 | } 94 | }, 95 | text = { 96 | Column { 97 | IconButton( 98 | onClick = { playlistViewModel.handlePlaylistActions(PlaylistActions.UpdateStateEmoji("")) }, 99 | modifier = Modifier.align(Alignment.End) 100 | ) { 101 | Icon( 102 | imageVector = Icons.Rounded.Close, 103 | contentDescription = stringResource(R.string.remove_emoji), 104 | ) 105 | } 106 | Box( 107 | modifier = Modifier 108 | .align(Alignment.CenterHorizontally) 109 | .size(100.dp) 110 | .padding(bottom = 10.dp) 111 | .clip(RoundedCornerShape(10)) 112 | .clickable { 113 | showEmojiPicker = true 114 | }, 115 | contentAlignment = Alignment.Center 116 | ) { 117 | if (playlistState.emoji.isNotBlank()) { 118 | AnimatedContent(playlistState.emoji) { 119 | CuteText( 120 | text = it, 121 | fontSize = 40.sp 122 | ) 123 | } 124 | } else { 125 | Icon( 126 | painter = painterResource(R.drawable.add_emoji_rounded), 127 | contentDescription = stringResource(R.string.emoji), 128 | modifier = Modifier.size(40.dp) 129 | ) 130 | } 131 | } 132 | OutlinedTextField( 133 | value = playlistState.name, 134 | onValueChange = { playlistViewModel.handlePlaylistActions(PlaylistActions.UpdateStateName(it)) }, 135 | placeholder = { 136 | CuteText("${stringResource(R.string.playlist)} ${playlists.size + 1}") 137 | } 138 | ) 139 | } 140 | } 141 | ) 142 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/playlists/PlaylistPicker.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.sosauce.cutemusic.ui.screens.playlists 4 | 5 | import android.widget.Toast 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.layout.wrapContentWidth 11 | import androidx.compose.foundation.lazy.LazyColumn 12 | import androidx.compose.foundation.lazy.items 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.rounded.Add 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.ModalBottomSheet 18 | import androidx.compose.material3.OutlinedButton 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.setValue 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.platform.LocalContext 27 | import androidx.compose.ui.res.stringResource 28 | import androidx.compose.ui.unit.dp 29 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 30 | import com.sosauce.cutemusic.R 31 | import com.sosauce.cutemusic.data.actions.PlaylistActions 32 | import com.sosauce.cutemusic.domain.model.Playlist 33 | import com.sosauce.cutemusic.ui.shared_components.CuteText 34 | import com.sosauce.cutemusic.ui.shared_components.PlaylistViewModel 35 | import com.sosauce.cutemusic.utils.ICON_TEXT_SPACING 36 | import com.sosauce.cutemusic.utils.copyMutate 37 | import org.koin.androidx.compose.koinViewModel 38 | 39 | @Composable 40 | fun PlaylistPicker( 41 | mediaId: List, 42 | onDismissRequest: () -> Unit, 43 | onAddingFinished: () -> Unit = {} 44 | ) { 45 | val context = LocalContext.current 46 | val playlistViewModel = koinViewModel() 47 | val playlists by playlistViewModel.allPlaylists.collectAsStateWithLifecycle() 48 | var showPlaylistCreatorDialog by remember { mutableStateOf(false) } 49 | 50 | 51 | 52 | if (showPlaylistCreatorDialog) { 53 | CreatePlaylistDialog { showPlaylistCreatorDialog = false } 54 | } 55 | 56 | ModalBottomSheet( 57 | onDismissRequest = onDismissRequest 58 | ) { 59 | LazyColumn { 60 | item { 61 | OutlinedButton( 62 | onClick = { showPlaylistCreatorDialog = true }, 63 | modifier = Modifier 64 | .fillMaxWidth() 65 | .wrapContentWidth() 66 | ) { 67 | Row( 68 | verticalAlignment = Alignment.CenterVertically 69 | ) { 70 | Icon( 71 | imageVector = Icons.Rounded.Add, 72 | contentDescription = null 73 | ) 74 | Spacer(Modifier.width(ICON_TEXT_SPACING.dp)) 75 | CuteText(stringResource(R.string.create_playlist)) 76 | } 77 | } 78 | } 79 | 80 | items( 81 | items = playlists, 82 | key = { it.id } 83 | ) { playlist -> 84 | PlaylistItem( 85 | playlist = playlist, 86 | allowEditAction = false, 87 | onClickPlaylist = { 88 | 89 | val newPlaylist = Playlist( 90 | id = playlist.id, 91 | name = playlist.name, 92 | emoji = playlist.emoji, 93 | musics = playlist.musics.copyMutate { 94 | mediaId.forEach { id -> 95 | if (!contains(id)) { 96 | add(id) 97 | } else { 98 | Toast.makeText( 99 | context, 100 | context.getString(R.string.alrdy_in_playlist), 101 | Toast.LENGTH_SHORT 102 | ).show() 103 | } 104 | } 105 | } 106 | ) 107 | playlistViewModel.handlePlaylistActions( 108 | PlaylistActions.UpsertPlaylist(newPlaylist) 109 | ) 110 | onAddingFinished() 111 | }, 112 | onHandlePlaylistActions = playlistViewModel::handlePlaylistActions 113 | ) 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/AboutCard.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.screens.settings.compenents 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 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.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.Card 13 | import androidx.compose.material3.CardDefaults 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.MaterialTheme 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.platform.LocalContext 22 | import androidx.compose.ui.platform.LocalUriHandler 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.res.stringResource 25 | import androidx.compose.ui.unit.dp 26 | import com.sosauce.cutemusic.R 27 | import com.sosauce.cutemusic.ui.shared_components.CuteText 28 | 29 | @Composable 30 | fun AboutCard() { 31 | 32 | val context = LocalContext.current 33 | val version = context.packageManager.getPackageInfo(context.packageName, 0).versionName 34 | val uriHandler = LocalUriHandler.current 35 | 36 | Card( 37 | colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .padding(horizontal = 16.dp, vertical = 2.dp), 41 | shape = RoundedCornerShape(24.dp) 42 | ) { 43 | Row( 44 | verticalAlignment = Alignment.CenterVertically 45 | ) { 46 | Box( 47 | modifier = Modifier 48 | .size(100.dp) 49 | .padding(15.dp) 50 | .clip(RoundedCornerShape(15)) 51 | .background(Color(0xFFFAB3AA)), 52 | contentAlignment = Alignment.Center 53 | ) { 54 | Icon( 55 | painter = painterResource(R.drawable.music_note_rounded), 56 | contentDescription = stringResource(id = R.string.app_icon), 57 | modifier = Modifier.size(60.dp) 58 | ) 59 | } 60 | Column { 61 | CuteText( 62 | text = stringResource(id = R.string.cm_by_sosauce), 63 | 64 | ) 65 | CuteText( 66 | text = "${stringResource(id = R.string.version)} $version", 67 | color = MaterialTheme.colorScheme.onSurfaceVariant 68 | ) 69 | } 70 | } 71 | Row( 72 | modifier = Modifier 73 | .padding(8.dp) 74 | ) { 75 | Button( 76 | onClick = { uriHandler.openUri("https://github.com/sosauce/CuteMusic/releases") }, 77 | shape = RoundedCornerShape( 78 | topStart = 24.dp, 79 | bottomStart = 24.dp, 80 | topEnd = 24.dp, 81 | bottomEnd = 24.dp 82 | ), 83 | modifier = Modifier.fillMaxWidth() 84 | ) { 85 | CuteText(text = stringResource(id = R.string.update)) 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/FolderItem.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.screens.settings.compenents 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.basicMarquee 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.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material3.Card 13 | import androidx.compose.material3.CardDefaults 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.ColorFilter 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.unit.Dp 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | import com.sosauce.cutemusic.R 24 | import com.sosauce.cutemusic.ui.shared_components.CuteText 25 | import java.io.File 26 | 27 | @Composable 28 | fun FolderItem( 29 | modifier: Modifier = Modifier, 30 | folder: String, 31 | topDp: Dp, 32 | bottomDp: Dp, 33 | actionButton: @Composable () -> Unit 34 | ) { 35 | Card( 36 | modifier = modifier 37 | .padding(horizontal = 16.dp, vertical = 2.dp), 38 | colors = CardDefaults.cardColors( 39 | containerColor = MaterialTheme.colorScheme.surfaceContainer 40 | ), 41 | shape = RoundedCornerShape( 42 | topStart = topDp, 43 | topEnd = topDp, 44 | bottomStart = bottomDp, 45 | bottomEnd = bottomDp 46 | ), 47 | ) { 48 | Row( 49 | modifier = Modifier 50 | .fillMaxWidth() 51 | .padding(15.dp), 52 | verticalAlignment = Alignment.CenterVertically, 53 | horizontalArrangement = Arrangement.SpaceBetween 54 | ) { 55 | Image( 56 | painter = painterResource(R.drawable.folder_rounded), 57 | contentDescription = null, 58 | modifier = Modifier.size(33.dp), 59 | colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground) 60 | ) 61 | Column( 62 | modifier = Modifier 63 | .weight(1f) 64 | .padding(start = 10.dp), 65 | horizontalAlignment = Alignment.Start 66 | ) { 67 | CuteText( 68 | text = File(folder).name, 69 | fontSize = 18.sp 70 | ) 71 | CuteText( 72 | text = folder, 73 | fontSize = 13.sp, 74 | color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), 75 | modifier = Modifier.basicMarquee() 76 | ) 77 | } 78 | actionButton() 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsCategoryCard.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.screens.settings.compenents 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.Card 11 | import androidx.compose.material3.CardDefaults 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.painter.Painter 18 | import androidx.compose.ui.unit.Dp 19 | import androidx.compose.ui.unit.dp 20 | import com.sosauce.cutemusic.ui.shared_components.CuteText 21 | 22 | @Composable 23 | fun SettingsCategoryCard( 24 | icon: Painter, 25 | name: String, 26 | description: String, 27 | topDp: Dp, 28 | bottomDp: Dp, 29 | onNavigate: () -> Unit 30 | ) { 31 | Card( 32 | onClick = onNavigate, 33 | colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), 34 | modifier = Modifier 35 | .fillMaxWidth() 36 | .padding(horizontal = 16.dp, vertical = 2.dp), 37 | shape = RoundedCornerShape( 38 | topStart = topDp, 39 | topEnd = topDp, 40 | bottomStart = bottomDp, 41 | bottomEnd = bottomDp 42 | ) 43 | ) { 44 | Row( 45 | modifier = Modifier 46 | .padding(16.dp), 47 | verticalAlignment = Alignment.CenterVertically 48 | ) { 49 | Icon( 50 | painter = icon, 51 | contentDescription = null 52 | ) 53 | Spacer(Modifier.width(15.dp)) 54 | Column { 55 | CuteText(name) 56 | CuteText( 57 | text = description, 58 | color = MaterialTheme.colorScheme.onSurfaceVariant, 59 | style = MaterialTheme.typography.bodyMedium 60 | ) 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsScreens.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.screens.settings.compenents 2 | 3 | import androidx.navigation3.runtime.NavKey 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed class SettingsScreens(): NavKey { 8 | 9 | @Serializable 10 | data object Settings: SettingsScreens() 11 | 12 | @Serializable 13 | data object LookAndFeel: SettingsScreens() 14 | 15 | @Serializable 16 | data object NowPlaying: SettingsScreens() 17 | 18 | @Serializable 19 | data object Library: SettingsScreens() 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/shared_components/AnimatedIconButton.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.shared_components 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.foundation.interaction.collectIsPressedAsState 5 | import androidx.compose.foundation.layout.offset 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.IconButton 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.rememberCoroutineScope 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.graphicsLayer 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | import androidx.compose.ui.unit.IntOffset 15 | import com.sosauce.cutemusic.utils.rememberAnimatable 16 | import com.sosauce.cutemusic.utils.rememberInteractionSource 17 | import kotlinx.coroutines.launch 18 | 19 | @Composable 20 | fun AnimatedIconButton( 21 | modifier: Modifier = Modifier, 22 | buttonModifier: Modifier = Modifier, 23 | onClick: () -> Unit, 24 | animationDirection: Float, 25 | icon: ImageVector, 26 | contentDescription: String 27 | ) { 28 | val scope = rememberCoroutineScope() 29 | val animatable = rememberAnimatable() 30 | val interactionSource = rememberInteractionSource() 31 | val isPressed by interactionSource.collectIsPressedAsState() 32 | val scale by animateFloatAsState( 33 | targetValue = if (isPressed) 0.7f else 1f 34 | ) 35 | 36 | 37 | IconButton( 38 | onClick = { 39 | onClick() 40 | scope.launch { 41 | animatable.animateTo(animationDirection) 42 | animatable.animateTo(0f) 43 | } 44 | }, 45 | modifier = buttonModifier, 46 | interactionSource = interactionSource 47 | ) { 48 | Icon( 49 | imageVector = icon, 50 | contentDescription = contentDescription, 51 | modifier = modifier 52 | .offset { 53 | IntOffset( 54 | x = animatable.value.toInt(), 55 | y = 0 56 | ) 57 | } 58 | .graphicsLayer { 59 | scaleX = scale 60 | scaleY = scale 61 | } 62 | ) 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/shared_components/CuteDropdownMenuItem.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) 2 | 3 | package com.sosauce.cutemusic.ui.shared_components 4 | 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.automirrored.rounded.PlaylistAdd 9 | import androidx.compose.material.icons.rounded.PlaylistRemove 10 | import androidx.compose.material3.DropdownMenuItem 11 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import androidx.media3.common.MediaItem 23 | import com.sosauce.cutemusic.R 24 | import com.sosauce.cutemusic.ui.screens.playlists.PlaylistPicker 25 | 26 | /** 27 | * A dropdown menu item with some padding and clipped corners, 28 | * also adds a visible parameter, if needed. 29 | */ 30 | @Composable 31 | fun CuteDropdownMenuItem( 32 | text: @Composable () -> Unit, 33 | onClick: () -> Unit, 34 | modifier: Modifier = Modifier, 35 | leadingIcon: @Composable (() -> Unit)? = null, 36 | trailingIcon: @Composable (() -> Unit)? = null, 37 | visible: Boolean = true 38 | ) { 39 | 40 | if (visible) { 41 | DropdownMenuItem( 42 | text = text, 43 | onClick = onClick, 44 | modifier = modifier 45 | .padding(horizontal = 2.dp) 46 | .clip(RoundedCornerShape(12.dp)), 47 | leadingIcon = leadingIcon, 48 | trailingIcon = trailingIcon, 49 | ) 50 | } 51 | } 52 | 53 | @Composable 54 | fun AddToPlaylistDropdownItem( 55 | music: MediaItem 56 | ) { 57 | 58 | var showPlaylistDialog by remember { mutableStateOf(false) } 59 | if (showPlaylistDialog) { 60 | PlaylistPicker( 61 | mediaId = listOf(music.mediaId), 62 | onDismissRequest = { showPlaylistDialog = false } 63 | ) 64 | } 65 | 66 | 67 | CuteDropdownMenuItem( 68 | onClick = { showPlaylistDialog = true }, 69 | text = { 70 | CuteText(stringResource(R.string.add_to_playlist)) 71 | }, 72 | leadingIcon = { 73 | Icon( 74 | imageVector = Icons.AutoMirrored.Rounded.PlaylistAdd, 75 | contentDescription = null 76 | ) 77 | } 78 | ) 79 | } 80 | 81 | @Composable 82 | fun RemoveFromPlaylistDropdownItem( 83 | onRemoveFromPlaylist: () -> Unit 84 | ) { 85 | CuteDropdownMenuItem( 86 | onClick = onRemoveFromPlaylist, 87 | text = { 88 | CuteText(stringResource(R.string.remove_from_playlist)) 89 | }, 90 | leadingIcon = { 91 | Icon( 92 | imageVector = Icons.Rounded.PlaylistRemove, 93 | contentDescription = null 94 | ) 95 | } 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/shared_components/CuteNavigationButton.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.shared_components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack 8 | import androidx.compose.material.icons.rounded.Shuffle 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.SmallFloatingActionButton 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.vector.ImageVector 16 | import androidx.compose.ui.unit.dp 17 | 18 | @Composable 19 | fun CuteNavigationButton( 20 | modifier: Modifier = Modifier, 21 | playlistName: (@Composable () -> Unit)? = null, 22 | onNavigateUp: () -> Unit 23 | ) { 24 | SmallFloatingActionButton( 25 | onClick = onNavigateUp, 26 | modifier = modifier, 27 | shape = RoundedCornerShape(14.dp), 28 | containerColor = MaterialTheme.colorScheme.surfaceContainer 29 | ) { 30 | Row( 31 | verticalAlignment = Alignment.CenterVertically, 32 | modifier = Modifier.padding(if (playlistName != null) 5.dp else 0.dp) 33 | ) { 34 | Icon( 35 | imageVector = Icons.AutoMirrored.Rounded.ArrowBack, 36 | contentDescription = null 37 | ) 38 | playlistName?.invoke() 39 | } 40 | } 41 | } 42 | 43 | @Composable 44 | fun CuteActionButton( 45 | modifier: Modifier = Modifier, 46 | imageVector: ImageVector = Icons.Rounded.Shuffle, 47 | action: () -> Unit 48 | ) { 49 | SmallFloatingActionButton( 50 | onClick = action, 51 | modifier = modifier, 52 | shape = RoundedCornerShape(14.dp) 53 | ) { 54 | Icon( 55 | imageVector = imageVector, 56 | contentDescription = null 57 | ) 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/shared_components/CuteText.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.shared_components 2 | 3 | import androidx.compose.material3.LocalTextStyle 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.text.TextLayoutResult 10 | import androidx.compose.ui.text.TextStyle 11 | import androidx.compose.ui.text.style.TextAlign 12 | import androidx.compose.ui.text.style.TextOverflow 13 | import androidx.compose.ui.unit.TextUnit 14 | import com.sosauce.cutemusic.data.datastore.rememberUseSystemFont 15 | import com.sosauce.cutemusic.ui.theme.GlobalFont 16 | 17 | @Composable 18 | fun CuteText( 19 | text: String, 20 | modifier: Modifier = Modifier, 21 | color: Color = Color.Unspecified, 22 | fontSize: TextUnit = TextUnit.Unspecified, 23 | textAlign: TextAlign? = null, 24 | maxLines: Int = Int.MAX_VALUE, 25 | style: TextStyle = LocalTextStyle.current, 26 | onTextLayout: ((TextLayoutResult) -> Unit)? = null, 27 | overflow: TextOverflow = TextOverflow.Clip, 28 | ) { 29 | val useSystemFont by rememberUseSystemFont() 30 | val fontFamily = if (useSystemFont) { 31 | null 32 | } else { 33 | GlobalFont 34 | } 35 | 36 | Text( 37 | text = text, 38 | modifier = modifier, 39 | color = color, 40 | fontSize = fontSize, 41 | textAlign = textAlign, 42 | maxLines = maxLines, 43 | fontFamily = fontFamily, 44 | style = style, 45 | onTextLayout = onTextLayout, 46 | overflow = overflow 47 | ) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/shared_components/LazyRowWithScrollButton.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.shared_components 2 | 3 | import androidx.compose.animation.slideInHorizontally 4 | import androidx.compose.animation.slideOutHorizontally 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.lazy.LazyRow 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.foundation.lazy.rememberLazyListState 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.IconButton 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.rememberCoroutineScope 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import kotlinx.coroutines.launch 18 | 19 | @Composable 20 | fun LazyRowWithScrollButton( 21 | items: List, 22 | content: @Composable (T) -> Unit 23 | ) { 24 | val state = rememberLazyListState() 25 | val scope = rememberCoroutineScope() 26 | 27 | Box { 28 | LazyRow( 29 | state = state 30 | ) { 31 | items( 32 | items = items, 33 | key = { it.hashCode() } 34 | ) { type -> 35 | content(type) 36 | } 37 | } 38 | androidx.compose.animation.AnimatedVisibility( 39 | visible = state.canScrollForward, 40 | modifier = Modifier.align(Alignment.CenterEnd), 41 | enter = slideInHorizontally { it }, 42 | exit = slideOutHorizontally { it } 43 | ) { 44 | IconButton( 45 | onClick = { 46 | scope.launch { 47 | state.animateScrollToItem(items.lastIndex) 48 | } 49 | } 50 | ) { 51 | Icon( 52 | imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight, 53 | contentDescription = null 54 | ) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/shared_components/ScreenSelection.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.shared_components 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.automirrored.rounded.QueueMusic 11 | import androidx.compose.material3.DropdownMenu 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.Immutable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.graphics.painter.Painter 20 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 21 | import androidx.compose.ui.res.painterResource 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.unit.dp 24 | import com.sosauce.cutemusic.R 25 | import com.sosauce.cutemusic.ui.navigation.Screen 26 | import com.sosauce.cutemusic.utils.CurrentScreen 27 | 28 | @Composable 29 | fun ScreenSelection( 30 | expanded: Boolean, 31 | onDismissRequest: () -> Unit, 32 | onNavigate: (Screen) -> Unit 33 | ) { 34 | val items = listOf( 35 | NavigationItem( 36 | title = R.string.music, 37 | navigateTo = Screen.Main, 38 | icon = painterResource(R.drawable.music_note_rounded) 39 | ), 40 | NavigationItem( 41 | title = R.string.albums, 42 | navigateTo = Screen.Albums, 43 | icon = painterResource(androidx.media3.session.R.drawable.media3_icon_album) 44 | ), 45 | NavigationItem( 46 | title = R.string.artists, 47 | navigateTo = Screen.Artists, 48 | icon = painterResource(R.drawable.artist_rounded) 49 | ), 50 | NavigationItem( 51 | title = R.string.playlists, 52 | navigateTo = Screen.Playlists, 53 | icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.QueueMusic) 54 | ) 55 | 56 | ) 57 | 58 | DropdownMenu( 59 | expanded = expanded, 60 | onDismissRequest = onDismissRequest, 61 | shape = RoundedCornerShape(24.dp), 62 | ) { 63 | Column( 64 | verticalArrangement = Arrangement.Center 65 | ) { 66 | items.forEach { navigationItem -> 67 | 68 | val bgColor by animateColorAsState( 69 | targetValue = if (navigationItem.navigateTo == CurrentScreen.screen) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent 70 | ) 71 | 72 | CuteDropdownMenuItem( 73 | onClick = { onNavigate(navigationItem.navigateTo) }, 74 | text = { CuteText(stringResource(navigationItem.title)) }, 75 | leadingIcon = { 76 | Icon( 77 | painter = navigationItem.icon, 78 | contentDescription = stringResource(navigationItem.title) 79 | ) 80 | }, 81 | modifier = Modifier 82 | .padding(2.dp) 83 | .background( 84 | color = bgColor, 85 | shape = RoundedCornerShape(12.dp) 86 | ) 87 | ) 88 | } 89 | } 90 | } 91 | } 92 | 93 | @Immutable 94 | data class NavigationItem( 95 | val title: Int, 96 | val navigateTo: Screen, 97 | val icon: Painter, 98 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/shared_components/SelectedBar.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.shared_components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 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.fillMaxWidth 9 | import androidx.compose.foundation.layout.navigationBarsPadding 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.automirrored.rounded.PlaylistAdd 14 | import androidx.compose.material.icons.rounded.Close 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.IconButton 17 | import androidx.compose.material3.MaterialTheme 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.res.painterResource 27 | import androidx.compose.ui.unit.dp 28 | import com.sosauce.cutemusic.R 29 | import com.sosauce.cutemusic.ui.screens.playlists.PlaylistPicker 30 | import com.sosauce.cutemusic.utils.rememberSearchbarMaxFloatValue 31 | import com.sosauce.cutemusic.utils.rememberSearchbarRightPadding 32 | 33 | @Composable 34 | fun SelectedBar( 35 | modifier: Modifier = Modifier, 36 | selectedElements: List, 37 | onClearSelected: () -> Unit 38 | ) { 39 | 40 | var showPlaylistDialog by remember { mutableStateOf(false) } 41 | 42 | if (showPlaylistDialog) { 43 | PlaylistPicker( 44 | mediaId = selectedElements, 45 | onDismissRequest = { showPlaylistDialog = false }, 46 | onAddingFinished = onClearSelected 47 | ) 48 | } 49 | 50 | Column( 51 | modifier = modifier 52 | .navigationBarsPadding() 53 | .fillMaxWidth(rememberSearchbarMaxFloatValue()) 54 | .padding(end = rememberSearchbarRightPadding()) 55 | .clip(RoundedCornerShape(24.dp)) 56 | .background(MaterialTheme.colorScheme.surface) 57 | .border( 58 | width = 1.dp, 59 | color = MaterialTheme.colorScheme.surfaceContainer, 60 | shape = RoundedCornerShape(24.dp) 61 | ) 62 | ) { 63 | Row( 64 | verticalAlignment = Alignment.CenterVertically 65 | ) { 66 | IconButton( 67 | onClick = onClearSelected 68 | ) { 69 | Icon( 70 | imageVector = Icons.Rounded.Close, 71 | contentDescription = null 72 | ) 73 | } 74 | CuteText(selectedElements.size.toString()) 75 | } 76 | 77 | Row( 78 | verticalAlignment = Alignment.CenterVertically, 79 | horizontalArrangement = Arrangement.SpaceEvenly, 80 | modifier = Modifier.fillMaxWidth() 81 | ) { 82 | IconButton( 83 | onClick = { showPlaylistDialog = true } 84 | ) { 85 | Icon( 86 | imageVector = Icons.AutoMirrored.Rounded.PlaylistAdd, 87 | contentDescription = null 88 | ) 89 | } 90 | 91 | IconButton( 92 | onClick = {} 93 | ) { 94 | Icon( 95 | painter = painterResource(R.drawable.trash_rounded_filled), 96 | contentDescription = null, 97 | tint = MaterialTheme.colorScheme.error 98 | ) 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/shared_components/ThreadDivider.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.shared_components 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.foundation.layout.width 6 | import androidx.compose.material3.DividerDefaults 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.Path 12 | import androidx.compose.ui.graphics.StrokeCap 13 | import androidx.compose.ui.graphics.drawscope.Stroke 14 | import androidx.compose.ui.unit.Dp 15 | import androidx.compose.ui.unit.dp 16 | 17 | // Basically HorizontalDivider + VerticalDivider with a curved edge 18 | @Composable 19 | fun ThreadDivider( 20 | modifier: Modifier = Modifier, 21 | thickness: Dp = DividerDefaults.Thickness, 22 | color: Color = MaterialTheme.colorScheme.onBackground, 23 | curveSize: Dp = 10.dp 24 | ) { 25 | Canvas( 26 | modifier = modifier 27 | .width(20.dp) 28 | .height(50.dp) 29 | ) { 30 | val strokeWidth = thickness.toPx() 31 | val midX = strokeWidth / 2 32 | val midY = size.height / 2 33 | val curvePx = curveSize.toPx() 34 | 35 | val path = Path().apply { 36 | moveTo(midX, 0f) 37 | lineTo(midX, midY - curvePx) 38 | cubicTo( 39 | midX, midY, 40 | midX + curvePx, midY, 41 | midX + curvePx, midY 42 | ) 43 | lineTo(size.width, midY) 44 | } 45 | 46 | drawPath( 47 | path = path, 48 | color = color, 49 | style = Stroke(width = strokeWidth, cap = StrokeCap.Round) 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val md_theme_light_primary = Color(0xFFB0137F) 6 | val md_theme_light_onPrimary = Color(0xFFFFFFFF) 7 | val md_theme_light_primaryContainer = Color(0xFFFFD8E9) 8 | val md_theme_light_onPrimaryContainer = Color(0xFF3C0029) 9 | val md_theme_light_secondary = Color(0xFF725763) 10 | val md_theme_light_onSecondary = Color(0xFFFFFFFF) 11 | val md_theme_light_secondaryContainer = Color(0xFFFDD9E8) 12 | val md_theme_light_onSecondaryContainer = Color(0xFF291520) 13 | val md_theme_light_tertiary = Color(0xFF7F543B) 14 | val md_theme_light_onTertiary = Color(0xFFFFFFFF) 15 | val md_theme_light_tertiaryContainer = Color(0xFFFFDBC9) 16 | val md_theme_light_onTertiaryContainer = Color(0xFF311302) 17 | val md_theme_light_error = Color(0xFFBA1A1A) 18 | val md_theme_light_errorContainer = Color(0xFFFFDAD6) 19 | val md_theme_light_onError = Color(0xFFFFFFFF) 20 | val md_theme_light_onErrorContainer = Color(0xFF410002) 21 | val md_theme_light_background = Color(0xFFFFFBFF) 22 | val md_theme_light_onBackground = Color(0xFF1F1A1C) 23 | val md_theme_light_surface = Color(0xFFFFFBFF) 24 | val md_theme_light_onSurface = Color(0xFF1F1A1C) 25 | val md_theme_light_surfaceVariant = Color(0xFFF0DEE4) 26 | val md_theme_light_onSurfaceVariant = Color(0xFF504349) 27 | val md_theme_light_outline = Color(0xFF827379) 28 | val md_theme_light_inverseOnSurface = Color(0xFFF9EEF1) 29 | val md_theme_light_inverseSurface = Color(0xFF352F31) 30 | val md_theme_light_inversePrimary = Color(0xFFFFAFD7) 31 | val md_theme_light_surfaceTint = Color(0xFFB0137F) 32 | val md_theme_light_outlineVariant = Color(0xFFD4C2C8) 33 | val md_theme_light_scrim = Color(0xFF000000) 34 | 35 | val md_theme_dark_primary = Color(0xFFFFAFD7) 36 | val md_theme_dark_onPrimary = Color(0xFF610044) 37 | val md_theme_dark_primaryContainer = Color(0xFF890062) 38 | val md_theme_dark_onPrimaryContainer = Color(0xFFFFD8E9) 39 | val md_theme_dark_secondary = Color(0xFFDFBDCC) 40 | val md_theme_dark_onSecondary = Color(0xFF402A35) 41 | val md_theme_dark_secondaryContainer = Color(0xFF58404C) 42 | val md_theme_dark_onSecondaryContainer = Color(0xFFFDD9E8) 43 | val md_theme_dark_tertiary = Color(0xFFF3BA9B) 44 | val md_theme_dark_onTertiary = Color(0xFF4A2812) 45 | val md_theme_dark_tertiaryContainer = Color(0xFF643D26) 46 | val md_theme_dark_onTertiaryContainer = Color(0xFFFFDBC9) 47 | val md_theme_dark_error = Color(0xFFFFB4AB) 48 | val md_theme_dark_errorContainer = Color(0xFF93000A) 49 | val md_theme_dark_onError = Color(0xFF690005) 50 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 51 | val md_theme_dark_background = Color(0xFF1F1A1C) 52 | val md_theme_dark_onBackground = Color(0xFFEBE0E2) 53 | val md_theme_dark_surface = Color(0xFF1F1A1C) 54 | val md_theme_dark_onSurface = Color(0xFFEBE0E2) 55 | val md_theme_dark_surfaceVariant = Color(0xFF504349) 56 | val md_theme_dark_onSurfaceVariant = Color(0xFFD4C2C8) 57 | val md_theme_dark_outline = Color(0xFF9C8D93) 58 | val md_theme_dark_inverseOnSurface = Color(0xFF1F1A1C) 59 | val md_theme_dark_inverseSurface = Color(0xFFEBE0E2) 60 | val md_theme_dark_inversePrimary = Color(0xFFB0137F) 61 | val md_theme_dark_surfaceTint = Color(0xFFFFAFD7) 62 | val md_theme_dark_outlineVariant = Color(0xFF504349) 63 | val md_theme_dark_scrim = Color(0xFF000000) -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/widgets/WidgetActions.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.widgets 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.sosauce.cutemusic.utils.PACKAGE 7 | import com.sosauce.cutemusic.utils.WIDGET_ACTION_BROADCAST 8 | import kotlin.random.Random 9 | 10 | const val WIDGET_ACTION_SKIP_PREVIOUS = "WIDGET_ACTION_SKIP_PREVIOUS" 11 | const val WIDGET_ACTION_PLAYORPAUSE = "WIDGET_ACTION_SKIP_PLAYORPAUSE" 12 | const val WIDGET_ACTION_SKIP_NEXT = "WIDGET_ACTION_SKIP_NEXT" 13 | 14 | fun createWidgetPendingIntent( 15 | context: Context, 16 | widgetActions: String 17 | ): PendingIntent { 18 | val pendingIntent = PendingIntent.getBroadcast( 19 | context, 20 | Random.nextInt(), 21 | Intent(PACKAGE).putExtra( 22 | WIDGET_ACTION_BROADCAST, 23 | widgetActions 24 | ), 25 | PendingIntent.FLAG_IMMUTABLE 26 | 27 | ) 28 | 29 | return pendingIntent 30 | } 31 | 32 | 33 | interface WidgetCallback { 34 | fun skipToNext() 35 | fun playOrPause() 36 | fun skipToPrevious() 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/ui/widgets/WidgetBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.ui.widgets 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.sosauce.cutemusic.utils.WIDGET_ACTION_BROADCAST 7 | 8 | class WidgetBroadcastReceiver : BroadcastReceiver() { 9 | 10 | private var callback: WidgetCallback? = null 11 | 12 | override fun onReceive(context: Context?, intent: Intent?) { 13 | println("broadcast widget action: ${intent?.action}") 14 | val action = intent?.extras?.getString(WIDGET_ACTION_BROADCAST) ?: return 15 | 16 | when (action) { 17 | WIDGET_ACTION_PLAYORPAUSE -> callback?.playOrPause() 18 | WIDGET_ACTION_SKIP_NEXT -> callback?.skipToNext() 19 | WIDGET_ACTION_SKIP_PREVIOUS -> callback?.skipToPrevious() 20 | } 21 | } 22 | 23 | fun startCallback(callback: WidgetCallback) { 24 | this.callback = callback 25 | } 26 | 27 | fun stopCallback() { 28 | this.callback = null 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.utils 2 | 3 | const val CUTE_MUSIC_ID = "CUTE_MUSIC_ID" 4 | const val PACKAGE = "com.sosauce.cutemusic" 5 | const val ROOT_ID = "cute_music_root" 6 | const val ICON_TEXT_SPACING = 5 7 | const val NAVIGATION_PREFIX = "com.sosauce.cutemusic.ui.navigation.Screen." 8 | const val GOOGLE_SEARCH = "https://www.google.com/search?q=" 9 | const val WIDGET_UPDATE = "WIDGET_UPDATE" 10 | const val WIDGET_NEW_DATA = "WIDGET_NEW_DATA" 11 | const val WIDGET_NEW_IS_PLAYING = "WIDGET_NEW_IS_PLAYING" 12 | const val WIDGET_ACTION_BROADCAST = "WIDGET_NEW_DATA" 13 | 14 | 15 | 16 | object SharedTransitionKeys { 17 | const val CURRENTLY_PLAYING = "CURRENTLY_PLAYING" 18 | const val ARTIST = "ARTIST" 19 | const val PLAY_PAUSE_BUTTON = "PLAY_PAUSE_BUTTON" 20 | const val FAB = "FAB" 21 | const val SKIP_NEXT_BUTTON = "SKIP_NEXT_BUTTON" 22 | const val SKIP_PREVIOUS_BUTTON = "SKIP_PREVIOUS_BUTTON" 23 | const val MUSIC_ARTWORK = "MUSIC_ARTWORK" 24 | } 25 | 26 | object CuteTheme { 27 | const val SYSTEM = "SYSTEM" 28 | const val DARK = "DARK" 29 | const val LIGHT = "LIGHT" 30 | const val AMOLED = "AMOLED" 31 | } 32 | 33 | object SliderStyle { 34 | const val WAVY = "WAVY" 35 | const val CLASSIC = "CLASSIC" 36 | const val MATERIAL3 = "MATERIAL3" 37 | } 38 | 39 | object AnimationDirection { 40 | const val LEFT = -25f 41 | const val RIGHT = 25f 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sosauce.cutemusic.utils 2 | 3 | import android.content.ContentUris 4 | import android.content.Context 5 | import android.net.Uri 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.graphics.ImageBitmap 8 | import androidx.compose.ui.graphics.asImageBitmap 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.core.net.toUri 11 | import coil3.ImageLoader 12 | import coil3.request.ImageRequest 13 | import coil3.request.SuccessResult 14 | import coil3.request.allowHardware 15 | import coil3.request.crossfade 16 | import coil3.request.transformations 17 | import coil3.toBitmap 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.withContext 20 | import java.io.FileNotFoundException 21 | import coil3.request.ImageRequest as ImageRequest3 22 | 23 | object ImageUtils { 24 | 25 | @Composable 26 | fun imageRequester(img: Any?): ImageRequest3 { 27 | val context = LocalContext.current 28 | val request = ImageRequest3.Builder(context) 29 | .data(img) 30 | .crossfade(true) 31 | .transformations() 32 | .diskCacheKey(img.toString()) 33 | .memoryCacheKey(img.toString()) 34 | .build() 35 | .apply { 36 | } 37 | 38 | return request 39 | } 40 | 41 | fun getAlbumArt(albumId: Long): Any? { 42 | val sArtworkUri = "content://media/external/audio/albumart".toUri() 43 | return try { 44 | ContentUris.withAppendedId(sArtworkUri, albumId) 45 | } catch (e: FileNotFoundException) { 46 | e.printStackTrace() 47 | null 48 | } 49 | } 50 | 51 | // Kinda ugly fix to always load the new art for MaterialArt, but I guess it's better than loading the viewmodel in the app's theme 52 | suspend fun loadNewArt( 53 | context: Context, 54 | art: Uri?, 55 | onImageLoadSuccess: (ImageBitmap) -> Unit 56 | ) = withContext(Dispatchers.IO) { 57 | val imageLoader = ImageLoader.Builder(context).build() 58 | val request = ImageRequest.Builder(context) 59 | .data(art) 60 | .allowHardware(false) 61 | .build() 62 | val result = imageLoader.execute(request) 63 | 64 | if (result is SuccessResult) { 65 | onImageLoadSuccess(result.image.toBitmap().asImageBitmap()) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/add_emoji_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/add_photo_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/artist_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bedtime_outlined.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/carousel.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/classic_slider.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dark_mode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/edit_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/export.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/folder_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/grid_view.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/image.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/info_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/library.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/lyrics_rounded.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/music_note_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/queue_music_rounded.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/reset.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/resource_import.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/saf.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/speed_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/system_theme.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/trash_rounded.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/trash_rounded_filled.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_next.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_pause.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_play.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_previous.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/font/nunito.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/font/nunito.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Thème 4 | Paramètres 5 | Mode Sombre 6 | Mode Amoled 7 | Version 8 | Soutenir 9 | Mettre à Jour 10 | A Propos 11 | Taille 12 | Type 13 | Bitrate 14 | Musique précédent 15 | Button jouer/pause 16 | Prochaine musique 17 | Pas d\'artistes trouvés! 18 | Art 19 | Veuillez redémarrer l\'app pour prendre effet ! 20 | Utiliser la police système 21 | Artistes 22 | Musiques 23 | CuteMusic par sosauce 24 | Icone de l\'app 25 | Rechercher 26 | Fermer 27 | Plus 28 | Salut! 29 | Il semble que CuteMusic n\'a pas access à vos musiques! Vous pouvez lui autoriser en dessous! 30 | Autoriser les permissions 31 | Pas de musiques trouvées ! 32 | Il y a eu un problème en essayant de supprimer la musique. 33 | Musique supprimée avec succès 34 | OK 35 | Divers 36 | Pas d\'albums trouvés ! 37 | Suivre le système 38 | Ascendant 39 | Descendant 40 | Régler la vitesse de lecture 41 | Albums 42 | Oui 43 | Dossiers Blacklistés 44 | Supprimer le dossier 45 | Êtes-vous sûr de vouloir supprimer ce dossier ? 46 | Dossier déjà blacklisté! 47 | Titre 48 | Année 49 | Genre 50 | Numéro de piste 51 | Numéro de disque 52 | Éditer 53 | Supprimer 54 | Rien en cours! 55 | UI 56 | Material Art 57 | Succès 58 | Erreur en essayant de sauvegarder les modifications. 59 | Éditeur 60 | Détails 61 | Utiliser le slider classique 62 | Annuler 63 | Fixer la vitesse et le tempo 64 | Fixer la vitesse 65 | Fixer le tempo 66 | Vitesse 67 | Tempo 68 | Taux 69 | Assembler la vitesse et le tempo 70 | Entrer un nouveau taux entre 0.5 et 2 71 | Partager 72 | Montrer le button fermer sur la CuteSearchbar 73 | Duration 74 | Dossiers 75 | Gérer les onglets visibles 76 | Fixer un temps de veille 77 | Aller à: 78 | Pas de paroles trouvées ! 79 | Égaliseur 80 | Cette musiques proviens du S.A.F 81 | Ouvrir le S.A.F pour ajouter des musiques 82 | Blacklister 83 | Pas blacklister 84 | S.A.F manageur 85 | Vous n\'avez pas de playlist! Créez en une pour rendre votre expérience encore meilleure! 86 | La musique est déjà dans cette playlist! 87 | Supprimer la playlist 88 | Modifier 89 | Modifier la playlist 90 | Créer une playlist 91 | Créer 92 | Playlist 93 | Playlistes 94 | Ajouter à une playlist 95 | Le thème de l\'app sera basé sur la cover de la musique en cours. 96 | Regrouper les musiques par leurs dossiers 97 | Nom du fichier 98 | Utiliser la vue caroussel 99 | Rechercher ici 100 | Rechercher des paroles 101 | La musique est entrain de charger… 102 | Nombre de grilles 103 | -------------------------------------------------------------------------------- /app/src/main/res/values-ro/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CuteMusic 3 | Tema 4 | Setări 5 | Mod de întunecare 6 | În mod Amoled 7 | Versiunea 8 | Sprijină 9 | Caută actualizări 10 | Despre 11 | Mărime 12 | Tip 13 | Bitrate 14 | Înapoi Melodia 15 | Butonul Redare/Pauză 16 | Înainte Melodia 17 | Nu s-au găsit artiști ! 18 | Artă 19 | Artiști 20 | Albume 21 | CuteMusic de sosauce 22 | Iconiță aplicație 23 | Căutare 24 | Închide 25 | Mai Mult 26 | Bună! 27 | Se pare că CuteMusic nu are permisiunea de a-ți accesa melodiile! O poți acorda mai jos! 28 | Solicită permisiunea 29 | Nu s-a găsit muzică ! 30 | A fost o eroare la ștergerea melodia. 31 | Melodia ștearsă cu succes. 32 | OK 33 | Altele 34 | Nu s-au găsit albume ! 35 | Urmărește sistemul 36 | Ascendent 37 | Descendent 38 | Setarea vitezei de redare 39 | Da 40 | Foldere Excluse 41 | Șterge Folder 42 | Ești sigur că vrei să ștergi acest folder ? 43 | Folder deja este exclusată ! 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/values-v31/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_neutral2_900 4 | @android:color/system_accent1_100 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #201A1A 4 | #f0d2ce 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FAB3AA 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/automotive_app_desc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/xml/music_widget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) apply false 3 | alias(libs.plugins.kotlin) apply false 4 | alias(libs.plugins.compose.compiler) apply false 5 | alias(libs.plugins.ksp) apply false 6 | } 7 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/1: -------------------------------------------------------------------------------- 1 | Placeholder -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | - Play any song from anywhere just by sharing the audio file to the app without downloading it! 2 | - Easy search across all your music/albums/artists! 3 | - Very fast and snappy! 4 | - No unnecessary permissions needed! 5 | - Material 3/You & Monet theming (+ Amoled mode)! 6 | - Blacklist Folders! 7 | - Beautiful landscape UI! 8 | - Tag Editor! 9 | - Playlist support ! 10 | - Material art: Color scheme dynamically changes depending on currently playing song's artwork ! 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot0.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | CuteMusic is a simple, lightweight and offline music player app for Android. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | CuteMusic -------------------------------------------------------------------------------- /fastlane/metadata/android/es-ES/changelogs/1: -------------------------------------------------------------------------------- 1 | Placeholder -------------------------------------------------------------------------------- /fastlane/metadata/android/es-ES/full_description.txt: -------------------------------------------------------------------------------- 1 | - ¡Reproduce cualquier canción desde cualquier lugar simplemente compartiendo el archivo de audio con la app sin necesidad de descargarlo! 2 | - ¡Busca fácil y rápidamente entre toda tu música, álbumes y artistas! 3 | - ¡Muy rápido y ágil! 4 | - ¡Sin permisos innecesarios! 5 | - ¡Temas Material 3/You & Monet (y modo AMOLED)! 6 | - ¡Lista negra de carpetas! 7 | ¡Interfaz hermosa en modo horizontal! 8 | - ¡Editor de etiquetas! 9 | - ¡Soporte para listas de reproducción! 10 | - ¡Arte dinámico: el esquema de colores cambia según la carátula de la canción que estés reproduciendo! 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/es-ES/short_description.txt: -------------------------------------------------------------------------------- 1 | CuteMusic es una app de reproductor de música para Android que es simple, ligera y funciona sin conexión. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/es-ES/title.txt: -------------------------------------------------------------------------------- 1 | CuteMusic -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/changelogs/1: -------------------------------------------------------------------------------- 1 | Placeholder -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/full_description.txt: -------------------------------------------------------------------------------- 1 | - Jouez n'importe quelle musique, just en partageant le fichier à l'application, sans le télécharger ! 2 | - Recherche simple à travers touts vos musiques/albums/artistes ! 3 | - Très rapide et fluide ! 4 | - Pas de permissions inutiles ! 5 | - Support pour Material 3/ Monet (+ Mode amoled) ! 6 | - Ajouter des dossiers à la liste noir ! 7 | - Un beau mode paysage ! 8 | - Éditeur de métas-données ! 9 | - Support pour les playlistes ! 10 | - Material art : La palette de couleurs change dynamiquement en fonction de l'illustration de la chanson en cours de lecture ! 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/short_description.txt: -------------------------------------------------------------------------------- 1 | CuteMusic est un lecteur de musique hors ligne simple et minialiste pour Android. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/title.txt: -------------------------------------------------------------------------------- 1 | CuteMusic -------------------------------------------------------------------------------- /font_licence.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## For more details on how to configure your build environment visit 2 | # http://www.gradle.org/docs/current/userguide/build_environment.html 3 | # 4 | # Specifies the JVM arguments used for the daemon process. 5 | # The setting is particularly useful for tweaking memory settings. 6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m 7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 8 | # 9 | # When configured, Gradle will run in incubating parallel mode. 10 | # This option should only be used with decoupled projects. For more details, visit 11 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 12 | # org.gradle.parallel=true 13 | #Mon Mar 24 23:03:29 CET 2025 14 | android.nonTransitiveRClass=true 15 | android.useAndroidX=true 16 | kotlin.code.style=official 17 | org.gradle.configuration-cache=true 18 | org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M" -Dfile.encoding\=UTF-8 19 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.10.1" 3 | emoji2Emojipicker = "1.5.0" 4 | glance = "1.1.1" 5 | haze = "1.6.0" 6 | koinAndroid = "4.0.2" 7 | koinAndroidxCompose = "4.0.2" 8 | koinAndroidxStartup = "4.0.0" 9 | kotlin = "2.1.20" 10 | activityCompose = "1.10.1" 11 | coilCompose = "3.0.4" 12 | composeBom = "2025.05.01" 13 | composeAnimation = "1.8.2" 14 | coreKtx = "1.16.0" 15 | coreSplashscreen = "1.0.1" 16 | datastorePreferences = "1.1.7" 17 | kotlinxSerializationJson = "1.8.0" 18 | materialKolor = "3.0.0-beta01" 19 | media3Common = "1.7.1" 20 | media3Exoplayer = "1.7.1" 21 | media3Session = "1.7.1" 22 | reorderable = "2.4.3" # https://github.com/Calvin-LL/Reorderable 23 | squigglyslider = "1.0.0" 24 | serialization = "2.0.20" 25 | taglib = "1.0.0-alpha25" 26 | roomCompiler = "2.7.1" 27 | roomKtx = "2.7.1" 28 | ksp = "2.1.20-1.0.31" 29 | kmpalette = "3.1.0" 30 | material3 = "1.4.0-alpha15" 31 | nav3Core = "1.0.0-alpha02" 32 | 33 | 34 | 35 | 36 | [libraries] 37 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } 38 | androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "composeAnimation" } 39 | androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } 40 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } 41 | androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } 42 | androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } 43 | androidx-emoji2-emojipicker = { module = "androidx.emoji2:emoji2-emojipicker", version.ref = "emoji2Emojipicker" } 44 | androidx-glance = { module = "androidx.glance:glance", version.ref = "glance" } 45 | androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" } 46 | androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose" } 47 | androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } 48 | androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } 49 | androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3Common" } 50 | androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" } 51 | androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Session" } 52 | androidx-ui = { module = "androidx.compose.ui:ui" } 53 | androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } 54 | coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } 55 | haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } 56 | koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" } 57 | koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinAndroidxCompose" } 58 | koin-androidx-startup = { module = "io.insert-koin:koin-androidx-startup", version.ref = "koinAndroidxStartup" } 59 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } 60 | material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" } 61 | reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } 62 | squigglyslider = { module = "me.saket.squigglyslider:squigglyslider", version.ref = "squigglyslider" } 63 | taglib = { module = "com.github.Kyant0:taglib", version.ref = "taglib" } 64 | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" } 65 | androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" } 66 | kmpalette-core = { module = "com.kmpalette:kmpalette-core", version.ref = "kmpalette" } 67 | androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } 68 | androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } 69 | 70 | 71 | 72 | [plugins] 73 | androidApplication = { id = "com.android.application", version.ref = "agp" } 74 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 75 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 76 | serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization"} 77 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sosauce/CuteMusic/649ebcc7adbd51c04018418f162bb5cd82c9276e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 13 19:30:48 CET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-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 | 9 | 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | repositories { 13 | google() 14 | mavenCentral() 15 | maven("https://jitpack.io") 16 | } 17 | } 18 | 19 | 20 | rootProject.name = "CuteMusic" 21 | include(":app") 22 | --------------------------------------------------------------------------------