├── .github └── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── .gitignore ├── .idea ├── .gitignore ├── DtonatorPreferences.xml ├── appInsightsSettings.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── discord.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── API_Spowlo_APKs.json ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── schemas │ └── com.bobbyesp.spowlo.database.AppDatabase │ │ └── 1.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── bobbyesp │ │ └── spowlo │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── bobbyesp │ │ │ └── spowlo │ │ │ ├── App.kt │ │ │ ├── CrashHandlerActivity.kt │ │ │ ├── Downloader.kt │ │ │ ├── DownloaderKeepUpService.kt │ │ │ ├── MainActivity.kt │ │ │ ├── NotificationActionReceiver.kt │ │ │ ├── database │ │ │ ├── AppDatabase.kt │ │ │ ├── Backup.kt │ │ │ ├── CommandShortcut.kt │ │ │ ├── CommandTemplate.kt │ │ │ ├── CookieProfile.kt │ │ │ ├── DownloadedSongInfo.kt │ │ │ └── SongsInfoDao.kt │ │ │ ├── di │ │ │ └── NetworkModules.kt │ │ │ ├── features │ │ │ ├── mod_downloader │ │ │ │ ├── README.md │ │ │ │ ├── data │ │ │ │ │ └── remote │ │ │ │ │ │ ├── ModsDownloaderAPI.kt │ │ │ │ │ │ └── ModsDownloaderAPIImpl.kt │ │ │ │ ├── domain │ │ │ │ │ └── model │ │ │ │ │ │ └── APIResponseDto.kt │ │ │ │ └── ui │ │ │ │ │ └── components │ │ │ │ │ ├── ArchTagComponent.kt │ │ │ │ │ ├── PackageItem.kt │ │ │ │ │ └── PackagesListCardComponent.kt │ │ │ └── spotify_api │ │ │ │ ├── data │ │ │ │ ├── paging │ │ │ │ │ └── SpotifyAppPagingSources.kt │ │ │ │ └── remote │ │ │ │ │ └── SpotifyApiRequests.kt │ │ │ │ └── model │ │ │ │ └── SpotifyData.kt │ │ │ ├── ui │ │ │ ├── common │ │ │ │ ├── AnimatedComposable.kt │ │ │ │ ├── AsyncImageImpl.kt │ │ │ │ ├── CompositionLocals.kt │ │ │ │ ├── Ext.kt │ │ │ │ └── Route.kt │ │ │ ├── components │ │ │ │ ├── BottomDrawer.kt │ │ │ │ ├── Buttons.kt │ │ │ │ ├── Chips.kt │ │ │ │ ├── CommonComponents.kt │ │ │ │ ├── ConsoleOutputComponent.kt │ │ │ │ ├── DialogItems.kt │ │ │ │ ├── Dialogs.kt │ │ │ │ ├── Divider.kt │ │ │ │ ├── IconButtons.kt │ │ │ │ ├── ImageComponents.kt │ │ │ │ ├── PreferenceItems.kt │ │ │ │ ├── SettingItem.kt │ │ │ │ ├── ShimmerEffect.kt │ │ │ │ ├── TextField.kt │ │ │ │ ├── TopAppBar.kt │ │ │ │ ├── WarningCard.kt │ │ │ │ ├── about │ │ │ │ │ ├── ContributorComponent.kt │ │ │ │ │ └── HeadDeveloperComponent.kt │ │ │ │ ├── cards │ │ │ │ │ └── ExpandableElevatedCard.kt │ │ │ │ ├── download_tasks │ │ │ │ │ └── DownloadingTaskItem.kt │ │ │ │ ├── history │ │ │ │ │ └── HistoryMediaComponents.kt │ │ │ │ ├── others │ │ │ │ │ └── shimmer │ │ │ │ │ │ └── cards │ │ │ │ │ │ └── HorizontalSongCardShimmer.kt │ │ │ │ ├── settings │ │ │ │ │ └── SettingsComponents.kt │ │ │ │ ├── songs │ │ │ │ │ ├── FeatureIcons.kt │ │ │ │ │ ├── MiniMetadataInfoComponent.kt │ │ │ │ │ ├── SongCard.kt │ │ │ │ │ ├── SongMetadataCard.kt │ │ │ │ │ ├── metadata_viewer │ │ │ │ │ │ ├── ExtraInfoCard.kt │ │ │ │ │ │ └── TrackComponent.kt │ │ │ │ │ └── search_feat │ │ │ │ │ │ └── SearchingSongComponent.kt │ │ │ │ └── text │ │ │ │ │ ├── AnimatedTextCount.kt │ │ │ │ │ └── SharedText.kt │ │ │ ├── dialogs │ │ │ │ ├── DownloaderSettingsDialog.kt │ │ │ │ ├── NotificationPermissionDialog.kt │ │ │ │ ├── UpdateDialog.kt │ │ │ │ ├── UpdaterBottomDrawer.kt │ │ │ │ └── bottomsheets │ │ │ │ │ ├── DownloaderBottomSheet.kt │ │ │ │ │ └── PagerUtils.kt │ │ │ ├── ext │ │ │ │ └── PagingExt.kt │ │ │ ├── icons │ │ │ │ ├── artist.kt │ │ │ │ └── yt_music.kt │ │ │ ├── pages │ │ │ │ ├── InitialEntry.kt │ │ │ │ ├── MarkdownViewerPage.kt │ │ │ │ ├── common_pages │ │ │ │ │ ├── ErrorPage.kt │ │ │ │ │ ├── LoadingPage.kt │ │ │ │ │ └── NotImplementedPage.kt │ │ │ │ ├── download_tasks │ │ │ │ │ ├── DownloadTasksPage.kt │ │ │ │ │ └── FullscreenConsoleOutput.kt │ │ │ │ ├── downloader │ │ │ │ │ ├── DownloaderPage.kt │ │ │ │ │ └── DownloaderViewModel.kt │ │ │ │ ├── history │ │ │ │ │ ├── DownloadHistoryBottomDrawer.kt │ │ │ │ │ ├── DownloadsHistoryPage.kt │ │ │ │ │ └── DownloadsHistoryViewModel.kt │ │ │ │ ├── metadata_viewer │ │ │ │ │ ├── binders │ │ │ │ │ │ └── SpotifyInfoBinder.kt │ │ │ │ │ ├── pages │ │ │ │ │ │ ├── AlbumPage.kt │ │ │ │ │ │ ├── ArtistPage.kt │ │ │ │ │ │ ├── PlaylistViewPage.kt │ │ │ │ │ │ └── TrackPage.kt │ │ │ │ │ └── playlists │ │ │ │ │ │ ├── PlaylistPageViewModel.kt │ │ │ │ │ │ └── SpotifyItemPage.kt │ │ │ │ ├── mod_downloader │ │ │ │ │ ├── ModsDownloaderPage.kt │ │ │ │ │ └── ModsDownloaderViewModel.kt │ │ │ │ ├── playlist │ │ │ │ │ └── PlaylistMetadataPage.kt │ │ │ │ ├── searcher │ │ │ │ │ ├── SearcherPage.kt │ │ │ │ │ └── SearcherPageViewModel.kt │ │ │ │ └── settings │ │ │ │ │ ├── SettingsPage.kt │ │ │ │ │ ├── about │ │ │ │ │ └── AboutPage.kt │ │ │ │ │ ├── appearance │ │ │ │ │ ├── AppThemePreferencesPage.kt │ │ │ │ │ ├── AppearancePage.kt │ │ │ │ │ └── LanguagePage.kt │ │ │ │ │ ├── cookies │ │ │ │ │ ├── CookiesSettingsPage.kt │ │ │ │ │ ├── CookiesSettingsViewModel.kt │ │ │ │ │ └── WebViewPage.kt │ │ │ │ │ ├── directories │ │ │ │ │ └── DownloadsDirectoriesPage.kt │ │ │ │ │ ├── documentation │ │ │ │ │ └── DocumentationPage.kt │ │ │ │ │ ├── downloader │ │ │ │ │ ├── DownloaderSettingsPage.kt │ │ │ │ │ └── FormatSettingsDialogs.kt │ │ │ │ │ ├── general │ │ │ │ │ └── GeneralSettingsPage.kt │ │ │ │ │ ├── spotify │ │ │ │ │ ├── SpotifySettingsDialogs.kt │ │ │ │ │ └── SpotifySettingsPage.kt │ │ │ │ │ └── updater │ │ │ │ │ └── UpdaterPage.kt │ │ │ └── theme │ │ │ │ ├── ColorScheme.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── utils │ │ │ ├── ChromeCustomTabsUtil.kt │ │ │ ├── DatabaseUtil.kt │ │ │ ├── DownloaderUtil.kt │ │ │ ├── FilesUtil.kt │ │ │ ├── LanguageSettings.kt │ │ │ ├── ListUtil.kt │ │ │ ├── NotificationsUtil.kt │ │ │ ├── PreferencesUtil.kt │ │ │ ├── TextUtils.kt │ │ │ └── UpdateUtil.kt │ └── res │ │ ├── drawable │ │ ├── github_mark.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ ├── icons8_youtube.xml │ │ ├── outline_arrow_back_24.xml │ │ ├── outline_cancel_24.xml │ │ ├── outline_content_copy_24.xml │ │ ├── sample.webp │ │ ├── sample1.webp │ │ ├── sample2.webp │ │ ├── sample3.webp │ │ ├── spotify_logo.xml │ │ ├── telegram_icon.xml │ │ ├── wolf_avatar.png │ │ └── youtube_music_icons8.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── raw │ │ ├── cli_commands.md │ │ └── index.md │ │ ├── values-es │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── locales_config.xml │ │ └── provider_paths.xml │ └── test │ └── java │ └── com │ └── bobbyesp │ └── spowlo │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── color ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ └── java │ ├── com │ └── kyant │ │ └── monet │ │ ├── ColorSpec.kt │ │ ├── Monet.kt │ │ ├── PaletteStyle.kt │ │ └── TonalPalettes.kt │ └── io │ └── material │ ├── hct │ ├── Cam16.kt │ ├── Hct.kt │ ├── HctSolver.kt │ └── ViewingConditions.kt │ └── utils │ ├── ColorUtils.kt │ ├── MathUtils.kt │ └── StringUtils.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve 3 | labels: [bug] 4 | body: 5 | 6 | 7 | - type: checkboxes 8 | id: checklist 9 | attributes: 10 | label: Checklist 11 | description: | 12 | Carefully read and work through this check list in order to prevent the most common mistakes and misuse of Spowlo/spotDL (note that most of the bugs are caused by the spotDL library and we need to report them in their repo): 13 | options: 14 | - label: I've verified that I'm running the latest [**stable version**](https://github.com/BobbyESP/Spowlo/releases/latest/) of Spowlo or any later [**preview versions**](https://github.com/BobbyESP/Spowlo/releases). 15 | required: true 16 | - label: I've checked that the YouTube Music is available in my country. 17 | required: true 18 | - label: I understand that the issue will be (ignored/closed) if I intentionally remove or skip any mandatory field. 19 | required: true 20 | 21 | - type: textarea 22 | attributes: 23 | label: Describe the bug 24 | description: 25 | placeholder: | 26 | A clear and concise description of what the bug is. 27 | validations: 28 | required: false 29 | 30 | - type: textarea 31 | attributes: 32 | label: To Reproduce 33 | placeholder: | 34 | Steps to reproduce the behavior: 35 | 1.Go to '...' 36 | 2.Click on '....' 37 | 3.Scroll down to '....' 38 | 4.See error 39 | validations: 40 | required: false 41 | 42 | - type: textarea 43 | attributes: 44 | label: Error reports 45 | placeholder: | 46 | Click on the displayed error below the text box area to copy it. 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | attributes: 52 | label: Screenshots & Screen Records 53 | placeholder: | 54 | Screenshots & Screen Records can amp up bug reports. 55 | validations: 56 | required: false 57 | 58 | - type: textarea 59 | attributes: 60 | label: Device info 61 | description: | 62 | Please provide some information of the device you are using. 63 | You can get the device info by doing these steps: 64 | 1) Open Spowlo 65 | 2) Go to settings 66 | 3) Click on About 67 | 4) Scroll all the way down 68 | 5) Click "Version" 69 | 6) Paste the info below. 70 | placeholder: | 71 | App version: x.x.x (XxXxX) 72 | Device information: Android XX (API XX) 73 | Supported ABIs: [arm64-v8a, armeabi-v7a, armeabi] 74 | spotDL version: X.X.X 75 | validations: 76 | required: false 77 | 78 | - type: textarea 79 | attributes: 80 | label: Additional context 81 | description: 82 | placeholder: | 83 | Add any other context about the problem here. 84 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # disable blank issue creation 2 | blank_issues_enabled: false 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature for the general download mode 3 | title: "[Feature Request]" 4 | labels: [enhancement] 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | description: | 11 | Even if you're not sure about the answer, feel free to leave it blank and provide us with more information about this request. 12 | options: 13 | - label: This feature I'm requesting is already implemented in spotDL. 14 | required: false 15 | - label: This feature is intended to be a UI/UX update. 16 | required: false 17 | - label: This feature is not going to conflict with many of the existing options. 18 | required: false 19 | - type: textarea 20 | id: description_1 21 | attributes: 22 | label: Is your feature request related to a problem? Please describe it and link to the GitHub issue. 23 | description: 24 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 25 | validations: 26 | required: false 27 | - type: textarea 28 | id: description_2 29 | attributes: 30 | label: Describe the solution you'd like 31 | description: 32 | placeholder: A clear and concise description of what you want to include in the app. 33 | validations: 34 | required: false 35 | - type: textarea 36 | id: description_3 37 | attributes: 38 | label: Song/playlist link 39 | description: 40 | placeholder: Please provide us with a link to the video for which this feature might be beneficial. 41 | validations: 42 | required: false 43 | - type: textarea 44 | id: description_4 45 | attributes: 46 | label: Additional context 47 | description: 48 | placeholder: Add any other context or screenshots about the feature request here. 49 | validations: 50 | required: false 51 | render: shell 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | keystore.properties 17 | /.idea/deploymentTargetSelector.xml 18 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/DtonatorPreferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/appInsightsSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 71 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | spotify 3 |
4 |

Spowlo

5 |
6 | 7 | A Spotify songs downloader powered by [spotDL](https://github.com/spotDL/spotify-downloader/) made with Jetpack Compose and Material You 8 | 9 | [![Telegram Channel](https://img.shields.io/badge/Telegram-Spowlo-green?style=flat&logo=telegram)](https://t.me/spowlo_chatroom) 10 | ![GitHub all releases](https://img.shields.io/github/downloads/BobbyESP/Spowlo/total?label=Downloads&logo=github) 11 | ![GitHub Repo stars](https://img.shields.io/github/stars/BobbyESP/Spowlo?color=informational&label=Stars) 12 | 13 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/BobbyESP/Spowlo?logo=github&logoColor=%23fff&style=for-the-badge) 14 | ![GitHub top language](https://img.shields.io/github/languages/top/BobbyESP/Spowlo?style=for-the-badge) 15 |
16 | 17 | ## ⚠️📖 Important notice 18 | Hello everyone, 19 | 20 | I want to share a quick update regarding the future of Spowlo. Due to ongoing challenges, including instability with SpotDL and increased personal commitments, I’m currently taking a break from maintaining the app. The changes from Spotify have also made it harder to keep things running smoothly. 21 | 22 | While this isn't a final goodbye, I will reflect and assess the best way forward. 23 | 24 | If you have any questions or concerns, feel free to reach out in our [Telegram channel](https://t.me/spowlo_chatroom). 25 | 26 | Thank you for your understanding and support! 27 | 28 | ## 📸 Screenshots 29 | 30 |
31 |
32 | 33 | 34 | 35 |
36 |
37 | 38 | ## ⚠️ Warning 39 | Spowlo uses YT Music and YouTube to download the songs. This is because Spotify DRM bypassing can lead to an account ban and legal issues. If YT Music isn't available in your country, don't worry, you can still use YouTube as audio provider or use a VPN. We are working on making a regional bypass so don't matter your region. Thank you for understanding. 40 | 41 | ## 🔮 Features 42 | 43 | - Download songs from Spotify thanks to the [spotDL](https://github.com/spotDL/spotify-downloader/) library. 44 | 45 | - Downloading without links, just a search query 46 | 47 | - Download full playlists with just one click. 48 | 49 | - Embed synced lyrics into the downloaded songs. 50 | 51 | - Easy to use and user-friendly. 52 | 53 | - [Material Design 3](https://m3.material.io/) style UI, with dynamic color theme. 54 | 55 | - MAD: UI and logic written purely on Kotlin. It's used just an activity and composable destinations and deep links thanks to the navigation library. 56 | 57 | ## ⬇️Download 58 | 59 | For most devices, it is recommended to install the **ARM64-v8a** version of the apks 60 | 61 | - Download the latest stable version from [GitHub releases](https://github.com/BobbyESP/Spowlo/releases/latest) 62 | 63 | ## Translation 64 | 65 | We are using Hosted Weblate for the translations of the app. if you want to contribute follow [this link](https://hosted.weblate.org/engage/spowlo/) 🖇️ 66 | 67 | 68 | ## 📖Credits 69 | Thanks to [xnetcat](https://github.com/xnetcat) for it's help with some spotDL related things! 70 | 71 | Thanks to [Seal](https://github.com/JunkFood02/Seal) and [JunkFood02](https://github.com/JunkFood02) for some of the code of the app and UI ideas. (Without you, this app would not have existed). I learnt a lot about architectures, corroutines, Jetpack Compose... 72 | 73 | [Philipp Lackner](https://www.youtube.com/c/PhilippLackner). Infinite thanks to you, Philipp. You made me learn infinite things with just a few videos. This guy explains literally everything about what is he coding, make apps just to make the community learn, and give us some amazing utilities. Without he, probably I wouldn't started coding for Android. 74 | 75 | [Material color utilities](https://github.com/material-foundation/material-color-utilities) for having Material You coloring support in any device. 76 | 77 | Katoka, for the app name. (Thank you! Without your moral support I couldn't have done the app hahaha) 78 | 79 | [MoureDev by Brais Moure](https://www.youtube.com/c/MouredevApps) 80 | 81 | [Programación Android by AristiDevs](https://www.youtube.com/c/AristiDevs) 82 | 83 | And also thank you all for the internal tests of the app! 84 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /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.kts. 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 | #Add kotlinx.serialization to the list of libraries to be processed by Proguard 24 | # Keep `Companion` object fields of serializable classes. 25 | # This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. 26 | -if @kotlinx.serialization.Serializable class ** 27 | -keepclassmembers class <1> { 28 | static <1>$Companion Companion; 29 | } 30 | 31 | # Keep `serializer()` on companion objects (both default and named) of serializable classes. 32 | -if @kotlinx.serialization.Serializable class ** { 33 | static **$* *; 34 | } 35 | -keepclassmembers class <2>$<3> { 36 | kotlinx.serialization.KSerializer serializer(...); 37 | } 38 | 39 | # Keep `INSTANCE.serializer()` of serializable objects. 40 | -if @kotlinx.serialization.Serializable class ** { 41 | public static ** INSTANCE; 42 | } 43 | -keepclassmembers class <1> { 44 | public static <1> INSTANCE; 45 | kotlinx.serialization.KSerializer serializer(...); 46 | } 47 | 48 | #keep database entities 49 | -keep class com.bobbyesp.spowlo.database.** { *; } 50 | 51 | # @Serializable and @Polymorphic are used at runtime for polymorphic serialization. 52 | -keepattributes RuntimeVisibleAnnotations,AnnotationDefault 53 | 54 | # ServiceLoader support 55 | -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} 56 | -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} 57 | 58 | # Most of volatile fields are updated with AFU and should not be mangled 59 | -keepclassmembers class kotlinx.coroutines.** { 60 | volatile ; 61 | } 62 | 63 | # Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater 64 | -keepclassmembers class kotlin.coroutines.SafeContinuation { 65 | volatile ; 66 | } 67 | 68 | # These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when 69 | # kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used. 70 | -dontwarn java.lang.instrument.ClassFileTransformer 71 | -dontwarn sun.misc.SignalHandler 72 | -dontwarn java.lang.instrument.Instrumentation 73 | -dontwarn sun.misc.Signal 74 | 75 | # Only used in `kotlinx.coroutines.internal.ExceptionsConstructor`. 76 | # The case when it is not available is hidden in a `try`-`catch`, as well as a check for Android. 77 | -dontwarn java.lang.ClassValue 78 | 79 | # An annotation used for build tooling, won't be directly accessed. 80 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 81 | 82 | -dontwarn javax.annotation.** 83 | 84 | # A resource is loaded with a relative path so the package of this class must be preserved. 85 | -adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz 86 | 87 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 88 | -dontwarn org.codehaus.mojo.animal_sniffer.* 89 | 90 | # OkHttp platform used only on JVM and when Conscrypt and other security providers are available. 91 | -dontwarn okhttp3.internal.platform.** 92 | -dontwarn org.conscrypt.** 93 | -dontwarn org.bouncycastle.** 94 | -dontwarn org.openjsse.** 95 | 96 | -dontwarn org.slf4j.impl.StaticLoggerBinder 97 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bobbyesp/spowlo/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.bobbyesp.spowlo", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/DownloaderKeepUpService.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo 2 | 3 | import android.app.PendingIntent 4 | import android.app.Service 5 | import android.content.Intent 6 | import android.os.Binder 7 | import android.os.Build 8 | import android.os.IBinder 9 | import android.util.Log 10 | import com.bobbyesp.spowlo.utils.NOTIFICATION 11 | import com.bobbyesp.spowlo.utils.NotificationsUtil 12 | import com.bobbyesp.spowlo.utils.NotificationsUtil.SERVICE_NOTIFICATION_ID 13 | import com.bobbyesp.spowlo.utils.PreferencesUtil 14 | 15 | private val TAG = DownloaderKeepUpService::class.java.simpleName 16 | 17 | class DownloaderKeepUpService : Service() { 18 | override fun onBind(intent: Intent): IBinder { 19 | Log.d(TAG, "onBind: ") 20 | val pendingIntent: PendingIntent = 21 | Intent(this, MainActivity::class.java).let { notificationIntent -> 22 | PendingIntent.getActivity( 23 | this, 0, notificationIntent, 24 | PendingIntent.FLAG_IMMUTABLE 25 | ) 26 | } 27 | if (PreferencesUtil.getValue(NOTIFICATION)) { 28 | val notification = NotificationsUtil.makeServiceNotification(pendingIntent) 29 | startForeground(SERVICE_NOTIFICATION_ID, notification) 30 | } 31 | return DownloadServiceBinder() 32 | } 33 | 34 | 35 | override fun onUnbind(intent: Intent?): Boolean { 36 | Log.d(TAG, "onUnbind: ") 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 38 | stopForeground(STOP_FOREGROUND_REMOVE) 39 | } else { 40 | stopForeground(true) 41 | } 42 | stopSelf() 43 | return super.onUnbind(intent) 44 | } 45 | 46 | inner class DownloadServiceBinder : Binder() { 47 | fun getService(): DownloaderKeepUpService = this@DownloaderKeepUpService 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.ClipData 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.util.Log 8 | import com.bobbyesp.library.SpotDL 9 | import com.bobbyesp.spowlo.App.Companion.context 10 | import com.bobbyesp.spowlo.utils.NotificationsUtil 11 | import com.bobbyesp.spowlo.utils.ToastUtil 12 | 13 | class NotificationActionReceiver : BroadcastReceiver() { 14 | companion object { 15 | private const val TAG = "CancelReceiver" 16 | private const val PACKAGE_NAME_PREFIX = "com.bobbyesp.spowlo." 17 | 18 | const val ACTION_CANCEL_TASK = 0 19 | const val ACTION_ERROR_REPORT = 1 20 | 21 | const val ACTION_KEY = PACKAGE_NAME_PREFIX + "action" 22 | const val TASK_ID_KEY = PACKAGE_NAME_PREFIX + "taskId" 23 | 24 | const val NOTIFICATION_ID_KEY = PACKAGE_NAME_PREFIX + "notificationId" 25 | const val ERROR_REPORT_KEY = PACKAGE_NAME_PREFIX + "error_report" 26 | } 27 | 28 | override fun onReceive(context: Context?, intent: Intent?) { 29 | if (intent == null) return 30 | val notificationId = intent.getIntExtra(NOTIFICATION_ID_KEY, 0) 31 | val action = intent.getIntExtra(ACTION_KEY, ACTION_CANCEL_TASK) 32 | Log.d(TAG, "onReceive: $action") 33 | when (action) { 34 | ACTION_CANCEL_TASK -> { 35 | val taskId = intent.getStringExtra(TASK_ID_KEY) 36 | cancelTask(taskId, notificationId) 37 | } 38 | 39 | ACTION_ERROR_REPORT -> { 40 | val errorReport = intent.getStringExtra(ERROR_REPORT_KEY) 41 | if (!errorReport.isNullOrEmpty()) 42 | copyErrorReport(errorReport, notificationId) 43 | } 44 | } 45 | } 46 | 47 | private fun cancelTask(taskId: String?, notificationId: Int) { 48 | if (taskId.isNullOrEmpty()) return 49 | NotificationsUtil.cancelNotification(notificationId) 50 | val result = SpotDL.getInstance().destroyProcessById(taskId) 51 | NotificationsUtil.cancelNotification(notificationId) 52 | if (result) { 53 | Log.d(TAG, "Task (id:$taskId) was killed.") 54 | Downloader.onProcessCanceled(taskId) 55 | 56 | } 57 | } 58 | 59 | private fun copyErrorReport(error: String, notificationId: Int) { 60 | App.clipboard.setPrimaryClip( 61 | ClipData.newPlainText(null, error) 62 | ) 63 | context.let { ToastUtil.makeToastSuspend(it.getString(R.string.error_copied)) } 64 | NotificationsUtil.cancelNotification(notificationId) 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/database/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.database 2 | 3 | import androidx.room.AutoMigration 4 | import androidx.room.Database 5 | import androidx.room.RoomDatabase 6 | 7 | @Database( 8 | entities = [CommandShortcut::class, CommandTemplate::class, CookieProfile::class, DownloadedSongInfo::class], 9 | version = 2, 10 | exportSchema = true, 11 | autoMigrations = [AutoMigration(from = 1, to = 2)] 12 | //INFO: If changed some entities, add autoMigrations 13 | ) 14 | abstract class AppDatabase : RoomDatabase() { 15 | abstract fun songsInfoDao(): SongsInfoDao 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/database/Backup.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.database 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Backup(val templates: List, val shortcuts: List) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/database/CommandShortcut.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.database 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import kotlinx.serialization.Serializable 6 | 7 | @Entity 8 | @Serializable 9 | data class CommandShortcut( 10 | @PrimaryKey(autoGenerate = true) val id: Long = 0, 11 | val option: String 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/database/CommandTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.database 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import kotlinx.serialization.Serializable 6 | 7 | @Entity 8 | @Serializable 9 | data class CommandTemplate( 10 | @PrimaryKey(autoGenerate = true) val id: Int, 11 | val name: String, 12 | val template: String 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/database/CookieProfile.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.database 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import kotlinx.serialization.Serializable 6 | 7 | @Entity 8 | @Serializable 9 | data class CookieProfile( 10 | @PrimaryKey(autoGenerate = true) val id: Int, 11 | val url: String, 12 | val content: String 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/database/DownloadedSongInfo.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.database 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "downloaded_songs_info") 8 | data class DownloadedSongInfo( 9 | @PrimaryKey(autoGenerate = true) val id: Int, 10 | val songName: String, 11 | val songAuthor: String, 12 | val songUrl: String, 13 | val thumbnailUrl: String, 14 | val songPath: String, 15 | @ColumnInfo(defaultValue = "0.0") val songDuration: Double = 0.0, 16 | @ColumnInfo(defaultValue = "Unknown") val extractor: String = "Unknown" 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/database/SongsInfoDao.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.database 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import androidx.room.Transaction 8 | import androidx.room.Update 9 | import com.bobbyesp.spowlo.utils.FilesUtil 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | @Dao 13 | interface SongsInfoDao { 14 | @Insert 15 | suspend fun insertAll(vararg info: DownloadedSongInfo) 16 | 17 | @Query("SELECT * FROM downloaded_songs_info") 18 | fun getAllMedia(): Flow> 19 | 20 | @Query("DELETE FROM downloaded_songs_info") 21 | fun deleteAllMediaFromDb() 22 | 23 | @Query("SELECT * from downloaded_songs_info WHERE id=:id") 24 | suspend fun getInfoById(id: Int): DownloadedSongInfo 25 | 26 | @Query("DELETE FROM downloaded_songs_info WHERE id = :id") 27 | suspend fun deleteInfoById(id: Int) 28 | 29 | @Query("DELETE FROM downloaded_songs_info WHERE songPath = :path") 30 | suspend fun deleteInfoByPath(path: String) 31 | 32 | @Query("SELECT * FROM downloaded_songs_info WHERE songPath = :path") 33 | suspend fun getInfoByPath(path: String): DownloadedSongInfo? 34 | 35 | @Transaction 36 | suspend fun deleteInfoByPathAndInsert( 37 | songInfo: DownloadedSongInfo, 38 | path: String = songInfo.songPath 39 | ) { 40 | deleteInfoByPath(path) 41 | insertAll(songInfo) 42 | } 43 | 44 | @Transaction 45 | suspend fun insertInfoDistinctByPath( 46 | songInfo: DownloadedSongInfo, 47 | path: String = songInfo.songPath 48 | ) { 49 | if (getInfoByPath(path) == null) 50 | insertAll(songInfo) 51 | } 52 | 53 | @Delete 54 | suspend fun deleteInfo(vararg info: DownloadedSongInfo) 55 | 56 | @Transaction 57 | suspend fun deleteInfoListByIdList(idList: List, deleteFile: Boolean = false) { 58 | idList.forEach { id -> 59 | val info = getInfoById(id) 60 | if (deleteFile) FilesUtil.deleteFile(info.songPath) 61 | deleteInfo(info) 62 | } 63 | } 64 | 65 | @Query("SELECT * FROM CommandTemplate") 66 | fun getTemplateFlow(): Flow> 67 | 68 | @Query("SELECT * FROM CommandTemplate") 69 | suspend fun getTemplateList(): List 70 | 71 | @Query("select * from CookieProfile") 72 | fun getCookieProfileFlow(): Flow> 73 | 74 | @Insert 75 | suspend fun insertTemplate(template: CommandTemplate): Long 76 | 77 | @Insert 78 | @Transaction 79 | suspend fun importTemplates(templateList: List) 80 | 81 | @Update 82 | suspend fun updateTemplate(template: CommandTemplate) 83 | 84 | @Delete 85 | suspend fun deleteTemplate(template: CommandTemplate) 86 | 87 | @Query("SELECT * FROM CommandTemplate where id = :id") 88 | suspend fun getTemplateById(id: Int): CommandTemplate 89 | 90 | @Query("select * from CookieProfile where id=:id") 91 | suspend fun getCookieById(id: Int): CookieProfile? 92 | 93 | @Update 94 | suspend fun updateCookieProfile(cookieProfile: CookieProfile) 95 | 96 | @Delete 97 | suspend fun deleteCookieProfile(cookieProfile: CookieProfile) 98 | 99 | @Insert 100 | suspend fun insertCookieProfile(cookieProfile: CookieProfile) 101 | 102 | @Query("delete from CommandTemplate where id=:id") 103 | suspend fun deleteTemplateById(id: Int) 104 | 105 | @Query("select * from CommandShortcut") 106 | fun getCommandShortcuts(): Flow> 107 | 108 | @Query("select * from CommandShortcut") 109 | suspend fun getShortcutList(): List 110 | 111 | @Delete 112 | suspend fun deleteShortcut(commandShortcut: CommandShortcut) 113 | 114 | @Insert 115 | suspend fun insertShortcut(commandShortcut: CommandShortcut): Long 116 | 117 | @Transaction 118 | @Insert 119 | suspend fun insertAllShortcuts(shortcuts: List) 120 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/di/NetworkModules.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.di 2 | 3 | import com.bobbyesp.spowlo.features.mod_downloader.data.remote.ModsDownloaderAPIService 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | object NetworkModules { 13 | @Provides 14 | @Singleton 15 | fun provideModsDownloaderAPI(): ModsDownloaderAPIService { 16 | return ModsDownloaderAPIService.create() 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/README.md: -------------------------------------------------------------------------------- 1 | # MODS DOWNLOADER FOR SPOWLO 2 | 3 | ThIs is the mod downloader for Spowlo. It is a simple feature that allows you to download Spotify 4 | mods using the xManager API. 5 | 6 | It is actually unavailable since the xManager team told me to remove it from the app. I will try to 7 | create mods by myself and add them to the app. 8 | So sorry for the inconveniences, but they are actually a non-profit team and making this feature 9 | will make decrease their income money. -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPI.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.features.mod_downloader.data.remote 2 | 3 | import android.content.Context 4 | import com.bobbyesp.spowlo.App 5 | import com.bobbyesp.spowlo.features.mod_downloader.domain.model.APIResponseDto 6 | import com.bobbyesp.spowlo.utils.UpdateUtil 7 | import io.ktor.client.HttpClient 8 | import io.ktor.client.engine.android.Android 9 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 10 | import io.ktor.client.plugins.logging.LogLevel 11 | import io.ktor.client.plugins.logging.Logging 12 | import io.ktor.http.ContentType 13 | import io.ktor.serialization.kotlinx.json.json 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.serialization.json.Json 16 | 17 | interface ModsDownloaderAPIService { 18 | suspend fun getAPIResponse(): Result 19 | suspend fun downloadPackage( 20 | context: Context = App.context 21 | ): Flow 22 | 23 | companion object { 24 | fun create(): ModsDownloaderAPIService = ModsDownloaderAPIImpl( 25 | client = HttpClient(Android) { 26 | engine { 27 | sslManager = { 28 | it.setHostnameVerifier { _, _ -> true } //Caution, this is kind of dangerous/unsecure 29 | } 30 | } 31 | install(Logging) { 32 | level = LogLevel.ALL 33 | } 34 | install(ContentNegotiation) { 35 | json( 36 | contentType = ContentType.Application.Json, 37 | json = Json { 38 | ignoreUnknownKeys = true 39 | encodeDefaults = true 40 | } 41 | ) 42 | } 43 | } 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPIImpl.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.features.mod_downloader.data.remote 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.bobbyesp.spowlo.features.mod_downloader.domain.model.APIResponseDto 6 | import com.bobbyesp.spowlo.utils.UpdateUtil 7 | import io.ktor.client.HttpClient 8 | import io.ktor.client.call.body 9 | import io.ktor.client.request.get 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.withContext 13 | import kotlinx.serialization.json.Json 14 | 15 | class ModsDownloaderAPIImpl( 16 | private val client: HttpClient 17 | ) : ModsDownloaderAPIService { 18 | 19 | private val json = Json { 20 | ignoreUnknownKeys = true 21 | encodeDefaults = true 22 | } 23 | 24 | override suspend fun getAPIResponse(): Result { 25 | return try { 26 | val apiResponse: String = client.get("https://jsonkeeper.com/b/VGY7").body() 27 | json.decodeFromString(apiResponse).let { 28 | Result.success(it) 29 | } 30 | } catch (e: Exception) { 31 | Log.e("ModsDownloaderAPIImpl", "getAPIResponse: ", e) 32 | Result.failure(e) 33 | } 34 | } 35 | 36 | override suspend fun downloadPackage( 37 | context: Context, 38 | ): Flow { 39 | withContext(Dispatchers.IO) { 40 | TODO("Not yet implemented") 41 | } 42 | // return emptyFlow() 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/domain/model/APIResponseDto.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.features.mod_downloader.domain.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class APIResponseDto( 7 | val Latest_Versions: LatestVersionResponseDto = LatestVersionResponseDto(), 8 | val apps: AppsResponseDto = AppsResponseDto(), 9 | ) 10 | 11 | @Serializable 12 | data class ApkResponseDto( 13 | val version: String = "Unknown", 14 | val link: String = "", 15 | ) 16 | 17 | @Serializable 18 | data class AppsResponseDto( 19 | val Regular: List = emptyList(), 20 | val Regular_Cloned: List = emptyList(), 21 | val AMOLED: List = emptyList(), 22 | val AMOLED_Cloned: List = emptyList(), 23 | val Lite: List = emptyList(), 24 | ) 25 | 26 | @Serializable 27 | data class LatestVersionResponseDto( 28 | val Regular: String = "Unknown", 29 | val Regular_Cloned: String = "Unknown", 30 | val AMOLED: String = "Unknown", 31 | val AMOLED_Cloned: String = "Unknown", 32 | val Lite: String = "Unknown", 33 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/ui/components/ArchTagComponent.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.features.mod_downloader.ui.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.CornerBasedShape 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.Memory 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Surface 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.unit.dp 21 | 22 | @OptIn(ExperimentalMaterial3Api::class) 23 | @Composable 24 | fun ArchTag( 25 | modifier: Modifier = Modifier, 26 | arch: ArchType, 27 | shape: CornerBasedShape = MaterialTheme.shapes.extraSmall, 28 | ) { 29 | Surface( 30 | modifier = modifier, 31 | shape = shape, 32 | color = MaterialTheme.colorScheme.secondaryContainer 33 | ) { 34 | 35 | Row( 36 | modifier = modifier 37 | .padding(4.dp), 38 | verticalAlignment = Alignment.CenterVertically, 39 | horizontalArrangement = Arrangement.Center, 40 | ) 41 | { 42 | Icon( 43 | modifier = Modifier.size(16.dp), 44 | imageVector = Icons.Filled.Memory, 45 | contentDescription = null, 46 | ) 47 | Spacer(modifier = Modifier.width(4.dp)) 48 | Text( 49 | text = arch.type, 50 | style = MaterialTheme.typography.bodySmall, 51 | color = MaterialTheme.colorScheme.onSurface 52 | ) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/ui/components/PackageItem.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.features.mod_downloader.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.IntrinsicSize 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.foundation.layout.wrapContentSize 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.Download 16 | import androidx.compose.material.icons.outlined.ContentCopy 17 | import androidx.compose.material3.FilledTonalIconButton 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.Surface 21 | import androidx.compose.material3.Text 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.unit.dp 26 | 27 | enum class ArchType( 28 | val type: String, 29 | val description: String? = null 30 | ) { 31 | Arm64("ARM64-v8a", "64-bit ARM"), 32 | Arm("ARMEABI-v7a", "32-bit ARM"), 33 | Merged("Merged", "Merged"), 34 | } 35 | 36 | @Composable 37 | fun PackageItemComponent( 38 | modifier: Modifier = Modifier, 39 | type: ArchType = ArchType.Arm64, 40 | onClick: () -> Unit = {}, 41 | version: String = "8.7.78.373", 42 | onCopyClick: () -> Unit = {}, 43 | ) { 44 | Surface( 45 | modifier = modifier 46 | .clickable(onClick = onClick) 47 | .padding(6.dp) 48 | ) { 49 | Row( 50 | modifier = modifier 51 | .fillMaxWidth(), 52 | verticalAlignment = Alignment.CenterVertically 53 | ) { 54 | ArchTag( 55 | modifier = Modifier 56 | .height(IntrinsicSize.Max) 57 | .width(IntrinsicSize.Max), 58 | arch = type, 59 | ) 60 | Text( 61 | modifier = Modifier 62 | .padding(start = 8.dp) 63 | .height(IntrinsicSize.Max) 64 | .width(IntrinsicSize.Min), 65 | text = version, 66 | style = MaterialTheme.typography.bodySmall, 67 | ) 68 | Box( 69 | modifier = Modifier 70 | .fillMaxWidth() 71 | .wrapContentSize(Alignment.CenterEnd) 72 | ) { 73 | Row( 74 | modifier = Modifier, 75 | horizontalArrangement = Arrangement.End, 76 | verticalAlignment = Alignment.CenterVertically 77 | ) 78 | { 79 | FilledTonalIconButton( 80 | modifier = Modifier 81 | .padding(end = 12.dp) 82 | .align(Alignment.Bottom) 83 | .size(24.dp), 84 | onClick = onClick 85 | ) { 86 | Icon( 87 | Icons.Filled.Download, 88 | contentDescription = null, 89 | tint = MaterialTheme.colorScheme.onPrimaryContainer, 90 | modifier = Modifier 91 | .size(18.dp) 92 | ) 93 | } 94 | FilledTonalIconButton( 95 | modifier = Modifier 96 | .padding() 97 | .align(Alignment.Bottom) 98 | .size(24.dp), 99 | onClick = onCopyClick 100 | ) { 101 | Icon( 102 | Icons.Outlined.ContentCopy, 103 | contentDescription = null, 104 | tint = MaterialTheme.colorScheme.onPrimaryContainer, 105 | modifier = Modifier.size(18.dp) 106 | ) 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/model/SpotifyData.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.features.spotify_api.model 2 | 3 | enum class SpotifyDataType { 4 | TRACK, 5 | ALBUM, 6 | PLAYLIST, 7 | ARTIST 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/common/AsyncImageImpl.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.common 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.ColorFilter 8 | import androidx.compose.ui.graphics.DefaultAlpha 9 | import androidx.compose.ui.graphics.FilterQuality 10 | import androidx.compose.ui.graphics.drawscope.DrawScope 11 | import androidx.compose.ui.layout.ContentScale 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.platform.LocalInspectionMode 14 | import androidx.compose.ui.res.painterResource 15 | import coil.compose.AsyncImagePainter 16 | import coil.imageLoader 17 | import coil.request.ImageRequest 18 | import com.bobbyesp.spowlo.R 19 | 20 | @Composable 21 | fun AsyncImageImpl( 22 | model: Any, 23 | contentDescription: String?, 24 | modifier: Modifier = Modifier, 25 | transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = AsyncImagePainter.DefaultTransform, 26 | onState: ((AsyncImagePainter.State) -> Unit)? = null, 27 | alignment: Alignment = Alignment.Center, 28 | contentScale: ContentScale = ContentScale.Fit, 29 | alpha: Float = DefaultAlpha, 30 | colorFilter: ColorFilter? = null, 31 | filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, 32 | isPreview: Boolean = LocalInspectionMode.current 33 | ) { 34 | if (isPreview) Image( 35 | painter = painterResource(R.drawable.ic_launcher_foreground), 36 | contentDescription = contentDescription, 37 | modifier = modifier, 38 | alignment = alignment, 39 | contentScale = contentScale, 40 | alpha = alpha, 41 | colorFilter = colorFilter, 42 | ) 43 | else coil.compose.AsyncImage( 44 | model = ImageRequest.Builder(LocalContext.current).data(model).crossfade(true).build(), 45 | contentDescription = contentDescription, 46 | imageLoader = LocalContext.current.imageLoader, 47 | modifier = modifier, 48 | transform = transform, 49 | onState = onState, 50 | alignment = alignment, 51 | contentScale = contentScale, 52 | alpha = alpha, 53 | colorFilter = colorFilter, 54 | filterQuality = filterQuality 55 | ) 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/common/CompositionLocals.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.common 2 | 3 | import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass 4 | import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.CompositionLocalProvider 7 | import androidx.compose.runtime.collectAsState 8 | import androidx.compose.runtime.compositionLocalOf 9 | import androidx.compose.runtime.staticCompositionLocalOf 10 | import androidx.compose.ui.graphics.Color 11 | import com.bobbyesp.spowlo.ui.theme.DEFAULT_SEED_COLOR 12 | import com.bobbyesp.spowlo.utils.DarkThemePreference 13 | import com.bobbyesp.spowlo.utils.PreferencesUtil 14 | import com.bobbyesp.spowlo.utils.palettesMap 15 | import com.kyant.monet.LocalTonalPalettes 16 | import com.kyant.monet.PaletteStyle 17 | import com.kyant.monet.TonalPalettes.Companion.toTonalPalettes 18 | 19 | val LocalDarkTheme = compositionLocalOf { DarkThemePreference() } 20 | val LocalSeedColor = compositionLocalOf { DEFAULT_SEED_COLOR } 21 | val LocalWindowWidthState = staticCompositionLocalOf { WindowWidthSizeClass.Compact } 22 | val LocalWindowHeightState = staticCompositionLocalOf { WindowHeightSizeClass.Compact } 23 | val LocalDynamicColorSwitch = compositionLocalOf { false } 24 | val LocalPaletteStyleIndex = compositionLocalOf { 0 } 25 | 26 | @Composable 27 | fun SettingsProvider( 28 | windowWidthSizeClass: WindowWidthSizeClass, 29 | localWindowHeightSizeClass: WindowHeightSizeClass, 30 | content: @Composable () -> Unit 31 | ) { 32 | val appSettingsState = PreferencesUtil.AppSettingsStateFlow.collectAsState().value 33 | CompositionLocalProvider( 34 | LocalDarkTheme provides appSettingsState.darkTheme, 35 | LocalSeedColor provides appSettingsState.seedColor, 36 | LocalPaletteStyleIndex provides appSettingsState.paletteStyleIndex, 37 | LocalTonalPalettes provides Color(appSettingsState.seedColor).toTonalPalettes( 38 | palettesMap.getOrElse(appSettingsState.paletteStyleIndex) { PaletteStyle.TonalSpot } 39 | ), 40 | LocalWindowWidthState provides windowWidthSizeClass, 41 | LocalWindowHeightState provides localWindowHeightSizeClass, 42 | LocalDynamicColorSwitch provides appSettingsState.isDynamicColorEnabled, 43 | content = content 44 | ) 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/common/Ext.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.mutableIntStateOf 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | import com.bobbyesp.spowlo.utils.PreferencesUtil.getBoolean 8 | import com.bobbyesp.spowlo.utils.PreferencesUtil.getInt 9 | import com.bobbyesp.spowlo.utils.PreferencesUtil.getString 10 | 11 | inline val String.booleanState 12 | @Composable get() = 13 | remember { mutableStateOf(this.getBoolean()) } 14 | 15 | inline val String.stringState 16 | @Composable get() = 17 | remember { mutableStateOf(this.getString()) } 18 | 19 | inline val String.intState 20 | @Composable get() = remember { 21 | mutableIntStateOf(this.getInt()) 22 | } 23 | 24 | fun String.containsEllipsis(): Boolean { 25 | return this.contains("…") 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.common 2 | 3 | object Route { 4 | 5 | const val DOWNLOADER_SETTINGS = "downloader_settings" 6 | const val DOWNLOADER_SHEET = "downloader_sheet" 7 | const val NavGraph = "nav_graph" 8 | const val SearcherNavi = "searcher_navi" 9 | const val DownloaderNavi = "downloader_navi" 10 | 11 | 12 | const val HOME = "home" 13 | const val DOWNLOADER = "downloader" 14 | const val DOWNLOADS_HISTORY = "download_history" 15 | const val PLAYLIST = "playlist" 16 | const val SETTINGS = "settings" 17 | const val FORMAT_SELECTION = "format" 18 | const val PLAYLIST_METADATA_PAGE = "playlist_metadata_page" 19 | const val MODS_DOWNLOADER = "mods_downloader" 20 | const val SEARCHER = "searcher" 21 | const val MEDIA_PLAYER = "media_player" 22 | const val SPOTIFY_SETUP = "spotify_setup" 23 | const val UPDATER_PAGE = "updater_page" 24 | const val MARKDOWN_VIEWER = "markdown_viewer" 25 | const val DOCUMENTATION = "documentation" 26 | const val MORE_OPTIONS_HOME = "more_options_home" 27 | const val SONG_INFO_HISTORY = "song_info_history" 28 | const val DOWNLOAD_TASKS = "download_tasks" 29 | const val DownloadTasksNavi = "download_tasks_navi" 30 | const val PLAYLIST_PAGE = "playlist_page" 31 | 32 | const val APPEARANCE = "appearance" 33 | const val APP_THEME = "app_theme" 34 | const val GENERAL_DOWNLOAD_PREFERENCES = "general_download_preferences" 35 | const val SPOTIFY_PREFERENCES = "spotify_preferences" 36 | const val ABOUT = "about" 37 | const val DOWNLOAD_DIRECTORY = "download_directory" 38 | const val CREDITS = "credits" 39 | const val LANGUAGES = "languages" 40 | const val DOWNLOAD_QUEUE = "queue" 41 | const val FULLSCREEN_LOG = "fullscreen_log" 42 | 43 | const val NETWORK_PREFERENCES = "network_preferences" 44 | const val COOKIE_PROFILE = "cookie_profile" 45 | const val COOKIE_GENERATOR_WEBVIEW = "cookie_webview" 46 | 47 | //DIALOGS 48 | const val AUDIO_QUALITY_DIALOG = "audio_quality_dialog" 49 | const val AUDIO_FORMAT_DIALOG = "audio_format_dialog" 50 | } 51 | 52 | infix fun String.arg(arg: String) = "$this/{$arg}" 53 | infix fun String.id(id: Int) = "$this/$id" -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/CommonComponents.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.WindowInsets 5 | import androidx.compose.foundation.layout.asPaddingValues 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.navigationBars 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun NavigationBarSpacer(modifier: Modifier = Modifier) { 14 | Spacer( 15 | modifier = modifier.height( 16 | with(WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) { 17 | if (this.value > 30f) this else 0f.dp 18 | } 19 | ) 20 | ) 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/ConsoleOutputComponent.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.text.font.FontFamily 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import com.bobbyesp.spowlo.R 23 | import com.bobbyesp.spowlo.ui.components.text.AutoResizableText 24 | 25 | @Composable 26 | fun ConsoleOutputComponent( 27 | modifier: Modifier = Modifier, 28 | consoleOutput: String = "Unknown console output", 29 | onClick: () -> Unit = {}, 30 | ) { 31 | Column( 32 | modifier = modifier 33 | .fillMaxWidth() 34 | ) { 35 | Text( 36 | text = stringResource(id = R.string.console_output), 37 | style = MaterialTheme.typography.headlineMedium.copy( 38 | fontSize = 18.sp, 39 | fontFamily = FontFamily.Monospace 40 | ), 41 | fontWeight = FontWeight.Bold, 42 | modifier = Modifier.padding(bottom = 4.dp) 43 | ) 44 | Box( 45 | modifier = Modifier 46 | .fillMaxWidth() 47 | .padding(top = 12.dp) 48 | .clip(MaterialTheme.shapes.small) 49 | .background(Color.Black.copy(alpha = 0.8f)) 50 | .clickable { onClick() }, 51 | ) { 52 | AutoResizableText( 53 | text = consoleOutput, 54 | modifier = Modifier.padding(8.dp), 55 | textStyle = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), 56 | // color is going to be like this: if the system color is dark, then the text color is white, otherwise it's black 57 | color = Color.White, 58 | ) 59 | } 60 | } 61 | } 62 | 63 | @Preview 64 | @Composable 65 | fun ConsoleOutputComponentPreview() { 66 | ConsoleOutputComponent(modifier = Modifier.size(300.dp)) 67 | } 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/DialogItems.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.foundation.interaction.MutableInteractionSource 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.selection.selectable 10 | import androidx.compose.foundation.shape.CircleShape 11 | import androidx.compose.material3.Checkbox 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.RadioButton 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.draw.clip 21 | import androidx.compose.ui.graphics.vector.ImageVector 22 | import androidx.compose.ui.semantics.clearAndSetSemantics 23 | import androidx.compose.ui.unit.dp 24 | 25 | @Composable 26 | fun SingleChoiceItem( 27 | modifier: Modifier = Modifier, text: String, selected: Boolean, onClick: () -> Unit 28 | ) { 29 | Row( 30 | modifier = modifier 31 | .padding(vertical = 2.dp) 32 | .clip(CircleShape) 33 | .selectable( 34 | selected = selected, 35 | enabled = true, 36 | onClick = onClick, 37 | ) 38 | .fillMaxWidth() 39 | // .padding(vertical = 12.dp) 40 | , verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start 41 | ) { 42 | RadioButton( 43 | modifier = Modifier.clearAndSetSemantics { }, selected = selected, onClick = onClick 44 | ) 45 | Text( 46 | // modifier = Modifier.padding(start = 18.dp), 47 | text = text, style = MaterialTheme.typography.bodyLarge 48 | ) 49 | } 50 | } 51 | 52 | @Composable 53 | fun SingleChoiceItemWithIcon( 54 | modifier: Modifier = Modifier, 55 | text: String, 56 | selected: Boolean, 57 | onClick: () -> Unit, 58 | icon: ImageVector 59 | ) { 60 | Row( 61 | modifier = modifier 62 | .padding(vertical = 2.dp) 63 | .clip(CircleShape) 64 | .selectable( 65 | selected = selected, 66 | enabled = true, 67 | onClick = onClick, 68 | ) 69 | .fillMaxWidth() 70 | // .padding(vertical = 12.dp) 71 | , verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start 72 | ) { 73 | RadioButton( 74 | modifier = Modifier.clearAndSetSemantics { }, selected = selected, onClick = onClick 75 | ) 76 | Icon( 77 | imageVector = icon, null, modifier = Modifier 78 | .padding(horizontal = 8.dp) 79 | .size(32.dp) 80 | ) 81 | Text( 82 | // modifier = Modifier.padding(start = 18.dp), 83 | text = text, style = MaterialTheme.typography.bodyLarge 84 | ) 85 | } 86 | } 87 | 88 | @Composable 89 | fun MultiChoiceItem( 90 | modifier: Modifier = Modifier, 91 | text: String, 92 | checked: Boolean, 93 | onClick: () -> Unit, 94 | ) { 95 | Row( 96 | modifier = modifier 97 | .fillMaxWidth() 98 | .padding(vertical = 12.dp) 99 | .selectable( 100 | selected = checked, 101 | enabled = true, 102 | onClick = onClick, 103 | indication = null, 104 | interactionSource = remember { MutableInteractionSource() } 105 | ), verticalAlignment = Alignment.CenterVertically 106 | ) { 107 | Checkbox( 108 | // modifier = Modifier.clearAndSetSemantics { }, 109 | checked = checked, onCheckedChange = null 110 | ) 111 | Text( 112 | modifier = Modifier.padding(start = 12.dp), 113 | text = text, style = MaterialTheme.typography.bodyMedium 114 | ) 115 | } 116 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/Divider.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Divider 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 | 12 | 13 | @Composable 14 | fun HorizontalDivider( 15 | modifier: Modifier = Modifier, 16 | color: Color = MaterialTheme.colorScheme.outlineVariant 17 | ) { 18 | Divider( 19 | modifier = modifier 20 | .fillMaxWidth() 21 | .size(DividerDefaults.Thickness), 22 | color = color 23 | ) 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/IconButtons.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.foundation.layout.size 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.outlined.Add 6 | import androidx.compose.material.icons.outlined.Cancel 7 | import androidx.compose.material.icons.outlined.ContentPaste 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.IconButton 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.platform.LocalClipboardManager 14 | import androidx.compose.ui.res.painterResource 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import com.bobbyesp.spowlo.R 18 | 19 | @Composable 20 | fun PasteFromClipBoardButton(onPaste: (String) -> Unit = {}) { 21 | val clipboardManager = LocalClipboardManager.current 22 | PasteButton(onClick = { 23 | clipboardManager.getText()?.let { onPaste(it.toString()) } 24 | }) 25 | } 26 | 27 | @Composable 28 | fun PasteButton(onClick: () -> Unit = {}) { 29 | IconButton(onClick = onClick) { 30 | Icon( 31 | Icons.Outlined.ContentPaste, 32 | stringResource(R.string.paste) 33 | ) 34 | } 35 | } 36 | 37 | @Composable 38 | fun AddButton(onClick: () -> Unit, enabled: Boolean = true) { 39 | IconButton( 40 | onClick = onClick, enabled = enabled 41 | ) { 42 | Icon( 43 | imageVector = Icons.Outlined.Add, 44 | contentDescription = stringResource( 45 | R.string.add 46 | ) 47 | ) 48 | } 49 | } 50 | 51 | @Composable 52 | fun ClearButton(onClick: () -> Unit) { 53 | IconButton(onClick = onClick) { 54 | Icon( 55 | modifier = Modifier.size(24.dp), 56 | imageVector = Icons.Outlined.Cancel, 57 | contentDescription = stringResource(id = R.string.clear), 58 | tint = MaterialTheme.colorScheme.onSurfaceVariant 59 | ) 60 | } 61 | } 62 | 63 | @Composable 64 | fun BackButton(onClick: () -> Unit) { 65 | IconButton(modifier = Modifier, onClick = onClick) { 66 | Icon( 67 | painter = painterResource(R.drawable.outline_arrow_back_24), 68 | contentDescription = stringResource(R.string.back), 69 | ) 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/ImageComponents.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.foundation.layout.aspectRatio 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.draw.clip 8 | import androidx.compose.ui.layout.ContentScale 9 | import com.bobbyesp.spowlo.R 10 | import com.bobbyesp.spowlo.ui.common.AsyncImageImpl 11 | 12 | @Composable 13 | fun MediaImage(modifier: Modifier = Modifier, imageModel: String, isAudio: Boolean = false) { 14 | AsyncImageImpl( 15 | modifier = modifier 16 | .aspectRatio( 17 | if (!isAudio) 16f / 9f else 1f, matchHeightConstraintsFirst = true 18 | ) 19 | .clip(MaterialTheme.shapes.extraSmall), 20 | model = imageModel.ifEmpty { R.drawable.sample1 }, 21 | contentDescription = null, 22 | contentScale = ContentScale.Crop, 23 | ) 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/SettingItem.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.vector.ImageVector 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.unit.dp 19 | 20 | 21 | @Composable 22 | fun SettingTitle(text: String, fontWeight: FontWeight = FontWeight.Normal) { 23 | Text( 24 | modifier = Modifier 25 | .padding(top = 32.dp) 26 | .padding(horizontal = 20.dp, vertical = 16.dp), 27 | text = text, 28 | style = MaterialTheme.typography.displaySmall, 29 | fontWeight = fontWeight, 30 | ) 31 | } 32 | 33 | @Composable 34 | fun SettingItem(title: String, description: String, icon: ImageVector?, onClick: () -> Unit) { 35 | Surface( 36 | modifier = Modifier.clickable { onClick() } 37 | ) { 38 | Row( 39 | modifier = Modifier 40 | .fillMaxWidth() 41 | .padding(8.dp, 20.dp), 42 | verticalAlignment = Alignment.CenterVertically, 43 | ) { 44 | icon?.let { 45 | Icon( 46 | imageVector = icon, 47 | contentDescription = null, 48 | modifier = Modifier 49 | .padding(start = 8.dp, end = 16.dp) 50 | .size(24.dp), 51 | tint = MaterialTheme.colorScheme.secondary 52 | ) 53 | } 54 | Column( 55 | modifier = Modifier 56 | .weight(1f) 57 | .padding(start = if (icon == null) 12.dp else 0.dp) 58 | ) { 59 | Text( 60 | text = title, 61 | maxLines = 1, 62 | style = MaterialTheme.typography.titleLarge, 63 | color = MaterialTheme.colorScheme.onSurface 64 | ) 65 | Text( 66 | text = description, 67 | color = MaterialTheme.colorScheme.onSurfaceVariant, 68 | maxLines = 1, 69 | style = MaterialTheme.typography.bodyMedium, 70 | ) 71 | } 72 | } 73 | } 74 | } 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/ShimmerEffect.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.animation.core.animateFloat 4 | import androidx.compose.animation.core.infiniteRepeatable 5 | import androidx.compose.animation.core.rememberInfiniteTransition 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.background 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.composed 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.graphics.Brush 17 | import androidx.compose.ui.layout.onGloballyPositioned 18 | import androidx.compose.ui.unit.IntSize 19 | 20 | fun Modifier.shimmerEffect(): Modifier = composed { 21 | var size by remember { 22 | mutableStateOf(IntSize.Zero) 23 | } 24 | val transition = rememberInfiniteTransition(label = "Shimmer Effect Infinite Transition") 25 | val startOffsetX by transition.animateFloat( 26 | initialValue = -2 * size.width.toFloat(), 27 | targetValue = 2 * size.width.toFloat(), 28 | animationSpec = infiniteRepeatable( 29 | animation = tween(1000) 30 | ), label = "Shimmer Effect Animation" 31 | ) 32 | 33 | background( 34 | brush = Brush.linearGradient( 35 | colors = listOf( 36 | MaterialTheme.colorScheme.primaryContainer, 37 | MaterialTheme.colorScheme.surface, 38 | MaterialTheme.colorScheme.primaryContainer, 39 | ), 40 | start = Offset(startOffsetX, 0f), 41 | end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat()) 42 | ) 43 | ) 44 | .onGloballyPositioned { 45 | size = it.size 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/TextField.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.rounded.Close 9 | import androidx.compose.material.icons.rounded.Search 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.OutlinedTextField 14 | import androidx.compose.material3.OutlinedTextFieldDefaults 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.surfaceColorAtElevation 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.DisposableEffect 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.ExperimentalComposeUiApi 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.focus.FocusRequester 23 | import androidx.compose.ui.focus.focusRequester 24 | import androidx.compose.ui.platform.LocalFocusManager 25 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.text.input.ImeAction 28 | import androidx.compose.ui.unit.dp 29 | import com.bobbyesp.spowlo.R 30 | 31 | @OptIn(ExperimentalComposeUiApi::class) 32 | @Composable 33 | fun QueryTextBox( 34 | modifier: Modifier = Modifier, 35 | query: String, 36 | onValueChange: (String) -> Unit, 37 | onSearchCallback: () -> Unit 38 | ) { 39 | val focusRequester = remember { FocusRequester() } 40 | val focusManager = LocalFocusManager.current 41 | val softwareKeyboardController = LocalSoftwareKeyboardController.current 42 | 43 | DisposableEffect(Unit) { 44 | focusRequester.requestFocus() 45 | onDispose { 46 | // Clean up if needed 47 | } 48 | } 49 | 50 | val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( 51 | 8.dp 52 | ) 53 | OutlinedTextField( 54 | value = query, 55 | onValueChange = onValueChange, 56 | placeholder = { 57 | if (query.isEmpty()) { 58 | Text(text = stringResource(id = R.string.searcher_page_query_text_box_label)) 59 | } 60 | }, 61 | modifier = modifier 62 | .fillMaxWidth() 63 | .focusRequester(focusRequester), 64 | keyboardOptions = KeyboardOptions( 65 | imeAction = ImeAction.Search 66 | ), 67 | keyboardActions = KeyboardActions( 68 | onSearch = { 69 | focusManager.clearFocus() 70 | softwareKeyboardController?.hide() 71 | onSearchCallback() 72 | } 73 | ), 74 | leadingIcon = { 75 | Icon(imageVector = Icons.Rounded.Search, contentDescription = "Search icon") 76 | }, 77 | trailingIcon = { 78 | if (query.isNotEmpty()) { 79 | IconButton(onClick = { onValueChange("") }) { 80 | Icon(imageVector = Icons.Rounded.Close, contentDescription = "Clear icon") 81 | } 82 | } 83 | }, 84 | singleLine = true, 85 | colors = OutlinedTextFieldDefaults.colors( 86 | focusedContainerColor = containerColor, 87 | unfocusedContainerColor = containerColor, 88 | disabledContainerColor = containerColor, 89 | unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant, 90 | ), 91 | ) 92 | } 93 | 94 | @Composable 95 | fun AdjacentLabel(modifier: Modifier = Modifier, text: String) { 96 | Text( 97 | text = text, 98 | modifier = modifier 99 | .padding(bottom = 12.dp, start = 4.dp), 100 | style = MaterialTheme.typography.bodySmall, 101 | color = MaterialTheme.colorScheme.onSurfaceVariant, 102 | ) 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/TopAppBar.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import android.graphics.Path 4 | import android.view.animation.PathInterpolator 5 | import androidx.compose.foundation.layout.RowScope 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TopAppBar 10 | import androidx.compose.material3.TopAppBarDefaults 11 | import androidx.compose.material3.TopAppBarScrollBehavior 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | 15 | @OptIn(ExperimentalMaterial3Api::class) 16 | @Composable 17 | fun LargeTopAppBar( 18 | title: @Composable () -> Unit, 19 | modifier: Modifier = Modifier, 20 | navigationIcon: @Composable () -> Unit = {}, 21 | actions: @Composable RowScope.() -> Unit = {}, 22 | scrollBehavior: TopAppBarScrollBehavior? = null, 23 | ) { 24 | androidx.compose.material3.LargeTopAppBar( 25 | modifier = modifier, 26 | title = title, 27 | navigationIcon = navigationIcon, 28 | actions = actions, 29 | scrollBehavior = scrollBehavior, 30 | ) 31 | 32 | } 33 | 34 | private val path = Path().apply { 35 | moveTo(0f, 0f) 36 | lineTo(0.7f, 0.1f) 37 | cubicTo(0.7f, 0.1f, .95F, .5F, 1F, 1F) 38 | moveTo(1f, 1f) 39 | } 40 | 41 | val fraction: (Float) -> Float = { PathInterpolator(path).getInterpolation(it) } 42 | 43 | @OptIn(ExperimentalMaterial3Api::class) 44 | @Composable 45 | fun SmallTopAppBar( 46 | modifier: Modifier = Modifier, 47 | titleText: String = "", 48 | scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), 49 | title: @Composable () -> Unit = { 50 | Text( 51 | text = titleText, 52 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = fraction(scrollBehavior.state.overlappedFraction)), 53 | maxLines = 1 54 | ) 55 | }, 56 | navigationIcon: @Composable () -> Unit = {}, 57 | actions: @Composable RowScope.() -> Unit = {}, 58 | ) { 59 | TopAppBar( 60 | modifier = modifier, 61 | title = title, 62 | navigationIcon = navigationIcon, 63 | actions = actions, 64 | scrollBehavior = scrollBehavior 65 | ) 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/WarningCard.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.WarningAmber 9 | import androidx.compose.material3.CardDefaults 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.OutlinedCard 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.text.style.TextAlign 19 | import androidx.compose.ui.unit.dp 20 | 21 | @Composable 22 | fun WarningCard( 23 | modifier: Modifier = Modifier, 24 | title: String, 25 | warningText: String, 26 | ) { 27 | OutlinedCard( 28 | modifier = modifier, 29 | shape = MaterialTheme.shapes.small, 30 | colors = CardDefaults.outlinedCardColors( 31 | containerColor = MaterialTheme.colorScheme.primaryContainer, 32 | ) 33 | ) { 34 | Row( 35 | modifier = Modifier 36 | .fillMaxWidth(), 37 | verticalAlignment = Alignment.CenterVertically, 38 | ) { 39 | Icon( 40 | modifier = Modifier.weight(0.175f), 41 | imageVector = Icons.Default.WarningAmber, 42 | tint = MaterialTheme.colorScheme.onPrimaryContainer, 43 | contentDescription = "Warning icon" 44 | ) 45 | Column( 46 | modifier = Modifier 47 | .fillMaxWidth() 48 | .padding(6.dp) 49 | .weight(1f), 50 | ) { 51 | Text( 52 | text = title, 53 | style = MaterialTheme.typography.bodyLarge, 54 | fontWeight = FontWeight.Bold 55 | ) 56 | Text( 57 | text = warningText, 58 | style = MaterialTheme.typography.bodySmall, 59 | color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f), 60 | fontWeight = FontWeight.Normal, 61 | textAlign = TextAlign.Justify 62 | ) 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/about/ContributorComponent.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components.about 2 | 3 | import androidx.compose.foundation.layout.Arrangement 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.CircleShape 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.IconButton 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.alpha 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.graphics.vector.ImageVector 21 | import androidx.compose.ui.layout.ContentScale 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.text.style.TextOverflow 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import com.bobbyesp.spowlo.R 28 | import com.bobbyesp.spowlo.ui.common.AsyncImageImpl 29 | import com.bobbyesp.spowlo.utils.ChromeCustomTabsUtil 30 | 31 | @Composable 32 | fun ContributorComponent( 33 | name: String, 34 | description: String, 35 | socialUrl: String? = null, 36 | avatarUrl: String? = null, 37 | socialNetworkImage: ImageVector? = null 38 | ) { 39 | Row( 40 | modifier = Modifier.fillMaxWidth(), 41 | verticalAlignment = Alignment.CenterVertically, 42 | horizontalArrangement = Arrangement.Start 43 | ) { 44 | if (avatarUrl != null) { 45 | Box(modifier = Modifier.padding(horizontal = 16.dp)) { 46 | AsyncImageImpl( 47 | model = avatarUrl, 48 | contentDescription = stringResource(id = R.string.contributor_avatar), 49 | modifier = Modifier 50 | .size(42.dp) 51 | .clip(CircleShape) 52 | .padding(), 53 | contentScale = ContentScale.Crop, 54 | ) 55 | } 56 | } 57 | Column( 58 | modifier = Modifier 59 | .weight(1f) 60 | .fillMaxWidth() 61 | .padding(if (avatarUrl == null) 16.dp else 0.dp, 0.dp) 62 | ) { 63 | Text( 64 | text = name, 65 | modifier = Modifier.padding(top = 8.dp, bottom = 6.dp), 66 | fontWeight = FontWeight.Bold, 67 | ) 68 | Text( 69 | text = description, 70 | modifier = Modifier 71 | .padding(bottom = 6.dp, end = 16.dp) 72 | .alpha(0.7f), 73 | fontSize = 12.sp, 74 | maxLines = 2, 75 | overflow = TextOverflow.Ellipsis, 76 | ) 77 | } 78 | if (socialUrl != null && socialNetworkImage != null) { 79 | Box( 80 | modifier = Modifier 81 | .size(48.dp) 82 | .padding(end = 16.dp), 83 | contentAlignment = Alignment.Center 84 | ) { 85 | IconButton( 86 | onClick = { 87 | ChromeCustomTabsUtil.openUrl(socialUrl) 88 | }) { 89 | Icon( 90 | imageVector = socialNetworkImage, 91 | contentDescription = stringResource(id = R.string.social_logo), 92 | tint = MaterialTheme.colorScheme.primary, 93 | ) 94 | } 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/about/HeadDeveloperComponent.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components.about 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.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.alpha 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.vector.ImageVector 17 | import androidx.compose.ui.layout.ContentScale 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.res.vectorResource 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.unit.dp 22 | import com.bobbyesp.spowlo.R 23 | import com.bobbyesp.spowlo.ui.common.AsyncImageImpl 24 | import com.bobbyesp.spowlo.ui.components.TextButtonWithIcon 25 | import com.bobbyesp.spowlo.ui.theme.Shapes 26 | import com.bobbyesp.spowlo.utils.ChromeCustomTabsUtil 27 | 28 | @Composable 29 | fun HeadDeveloperComponent( 30 | modifier: Modifier = Modifier, 31 | name: String, 32 | description: String, 33 | logoUrl: String, 34 | githubUrl: String, 35 | ) { 36 | Column( 37 | modifier = modifier, 38 | horizontalAlignment = Alignment.CenterHorizontally, 39 | verticalArrangement = Arrangement.Center 40 | ) { 41 | AsyncImageImpl( 42 | model = logoUrl, 43 | contentDescription = stringResource(id = R.string.github_logo), 44 | contentScale = ContentScale.Crop, 45 | modifier = Modifier 46 | .clip(CircleShape) 47 | .size(128.dp) 48 | ) 49 | Text( 50 | text = name, 51 | modifier = Modifier.padding(top = 8.dp, bottom = 6.dp), 52 | fontWeight = FontWeight.Bold, 53 | fontSize = MaterialTheme.typography.headlineMedium.fontSize 54 | ) 55 | Text( 56 | text = description, 57 | modifier = Modifier 58 | .padding(bottom = 6.dp) 59 | .alpha(0.7f), 60 | ) 61 | Row(modifier = Modifier) { 62 | GithubLinkButton( 63 | modifier = Modifier, 64 | githubUrl = githubUrl 65 | ) 66 | } 67 | } 68 | } 69 | 70 | @Composable 71 | fun GithubLinkButton(modifier: Modifier, githubUrl: String) { 72 | TextButtonWithIcon( 73 | modifier = modifier.clip(Shapes.extraSmall), 74 | onClick = { ChromeCustomTabsUtil.openUrl(githubUrl) }, 75 | icon = ImageVector.vectorResource(id = R.drawable.github_mark), 76 | text = stringResource( 77 | id = R.string.github 78 | ) 79 | ) 80 | 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/cards/ExpandableElevatedCard.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components.cards 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.foundation.isSystemInDarkTheme 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.material.icons.Icons 12 | import androidx.compose.material.icons.outlined.ExpandLess 13 | import androidx.compose.material3.ElevatedCard 14 | import androidx.compose.material3.ExperimentalMaterial3Api 15 | import androidx.compose.material3.FilledTonalIconButton 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.saveable.rememberSaveable 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.draw.rotate 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.graphics.vector.ImageVector 29 | import androidx.compose.ui.text.font.FontWeight 30 | import androidx.compose.ui.unit.dp 31 | 32 | @OptIn(ExperimentalMaterial3Api::class) 33 | @Composable 34 | fun ExpandableElevatedCard( 35 | modifier: Modifier = Modifier, 36 | isExpanded: Boolean = false, 37 | title: String, 38 | subtitle: String, 39 | icon: ImageVector, 40 | content: @Composable () -> Unit, 41 | ) { 42 | var expanded by rememberSaveable { mutableStateOf(isExpanded) } 43 | 44 | val animatedDegree = 45 | animateFloatAsState(targetValue = if (expanded) 0f else -180f, label = "Button Rotation") 46 | 47 | ElevatedCard( 48 | modifier = modifier, 49 | onClick = { expanded = !expanded }, 50 | shape = MaterialTheme.shapes.small 51 | ) { 52 | Row( 53 | modifier = Modifier 54 | .fillMaxWidth() 55 | .padding(horizontal = 8.dp), 56 | verticalAlignment = Alignment.CenterVertically, 57 | ) { 58 | Icon( 59 | modifier = Modifier.weight(0.1f), 60 | imageVector = icon, 61 | contentDescription = "Device infor" 62 | ) 63 | Column( 64 | modifier = Modifier 65 | .fillMaxWidth() 66 | .padding(6.dp) 67 | .weight(1f), 68 | ) { 69 | Text( 70 | text = title, 71 | style = MaterialTheme.typography.bodyMedium, 72 | fontWeight = FontWeight.Bold 73 | ) 74 | Text( 75 | text = subtitle, 76 | style = MaterialTheme.typography.bodySmall, 77 | color = if (isSystemInDarkTheme()) Color.White else Color.Black, 78 | fontWeight = FontWeight.Normal 79 | ) 80 | } 81 | FilledTonalIconButton( 82 | modifier = Modifier 83 | .padding() 84 | .size(24.dp), 85 | onClick = { expanded = !expanded }) { 86 | Icon( 87 | Icons.Outlined.ExpandLess, 88 | null, 89 | tint = MaterialTheme.colorScheme.onPrimaryContainer, 90 | modifier = Modifier.rotate(animatedDegree.value) 91 | ) 92 | } 93 | } 94 | AnimatedVisibility(visible = expanded) { 95 | content() 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/others/shimmer/cards/HorizontalSongCardShimmer.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components.others.shimmer.cards 2 | 3 | import androidx.compose.foundation.layout.Box 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.aspectRatio 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.unit.dp 18 | import com.bobbyesp.spowlo.ui.components.shimmerEffect 19 | 20 | @Composable 21 | fun HorizontalSongCardShimmer( 22 | modifier: Modifier = Modifier, 23 | showSongImage: Boolean = true 24 | ) { 25 | Row( 26 | modifier = modifier, 27 | verticalAlignment = Alignment.CenterVertically 28 | ) { 29 | if (showSongImage) { 30 | Box( 31 | Modifier 32 | .width(48.dp) 33 | .height(48.dp) 34 | .aspectRatio(1f) 35 | .clip(MaterialTheme.shapes.extraSmall) 36 | .shimmerEffect() 37 | ) 38 | } 39 | Column( 40 | modifier = Modifier 41 | .fillMaxWidth() 42 | .padding(vertical = 6.dp) 43 | .padding(start = 8.dp), 44 | ) { 45 | Box( 46 | modifier = Modifier 47 | .fillMaxWidth(0.8f) 48 | .height(18.dp) 49 | .clip(MaterialTheme.shapes.small) 50 | .shimmerEffect() 51 | ) 52 | Spacer(modifier = Modifier.height(6.dp)) 53 | Box( 54 | modifier = Modifier 55 | .fillMaxWidth(0.6f) 56 | .height(12.dp) 57 | .clip(MaterialTheme.shapes.extraSmall) 58 | .shimmerEffect() 59 | ) 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/FeatureIcons.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components.songs 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.text.font.FontWeight 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import com.bobbyesp.spowlo.R 18 | 19 | @ExperimentalMaterial3Api 20 | @Composable 21 | fun ExplicitIcon(visible: Boolean, modifier: Modifier = Modifier) { 22 | if (visible) { 23 | Surface( 24 | modifier = modifier, 25 | color = MaterialTheme.colorScheme.secondaryContainer, 26 | shape = MaterialTheme.shapes.extraSmall 27 | ) { 28 | 29 | Row( 30 | modifier = Modifier, 31 | verticalAlignment = Alignment.CenterVertically, 32 | horizontalArrangement = Arrangement.Center, 33 | ) 34 | { 35 | Text( 36 | text = "E", 37 | style = MaterialTheme.typography.bodySmall, 38 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), 39 | fontWeight = FontWeight.Bold, 40 | modifier = Modifier.padding(6.dp, 4.dp, 6.dp, 4.dp) 41 | ) 42 | } 43 | } 44 | } 45 | } 46 | 47 | @ExperimentalMaterial3Api 48 | @Composable 49 | fun LyricsIcon(visible: Boolean, modifier: Modifier = Modifier) { 50 | if (visible) { 51 | Surface( 52 | modifier = Modifier, 53 | color = MaterialTheme.colorScheme.secondaryContainer, 54 | shape = MaterialTheme.shapes.extraSmall 55 | ) { 56 | 57 | Row( 58 | modifier = Modifier, 59 | verticalAlignment = Alignment.CenterVertically, 60 | horizontalArrangement = Arrangement.Center, 61 | ) 62 | { 63 | Text( 64 | text = stringResource(id = R.string.lyrics).uppercase(), 65 | style = MaterialTheme.typography.bodySmall, 66 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), 67 | fontWeight = FontWeight.Bold, 68 | modifier = Modifier.padding(6.dp, 4.dp, 6.dp, 4.dp) 69 | ) 70 | } 71 | } 72 | } 73 | } 74 | 75 | @Composable 76 | fun CustomTag(text: String) { 77 | Surface( 78 | modifier = Modifier, 79 | color = MaterialTheme.colorScheme.secondaryContainer, 80 | shape = MaterialTheme.shapes.extraSmall 81 | ) { 82 | Row( 83 | modifier = Modifier, 84 | verticalAlignment = Alignment.CenterVertically, 85 | horizontalArrangement = Arrangement.Center, 86 | ) 87 | { 88 | Text( 89 | text = text, 90 | style = MaterialTheme.typography.bodySmall, 91 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), 92 | fontWeight = FontWeight.Bold, 93 | modifier = Modifier.padding(6.dp, 4.dp, 6.dp, 4.dp) 94 | ) 95 | } 96 | } 97 | } 98 | 99 | @ExperimentalMaterial3Api 100 | @Preview 101 | @Composable 102 | fun ExplicitIconPreview() { 103 | ExplicitIcon(visible = true, modifier = Modifier) 104 | } 105 | 106 | @ExperimentalMaterial3Api 107 | @Preview 108 | @Composable 109 | fun LyricsIconPreview() { 110 | LyricsIcon(visible = true, modifier = Modifier) 111 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/MiniMetadataInfoComponent.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components.songs 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.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.text.font.FontWeight 13 | 14 | @Composable 15 | fun MiniMetadataInfoComponent( 16 | modifier: Modifier = Modifier, 17 | text: String, 18 | textModifier: Modifier = Modifier, 19 | ) { 20 | Column( 21 | modifier = modifier, 22 | horizontalAlignment = Alignment.End 23 | ) { 24 | Surface( 25 | modifier = Modifier, 26 | color = MaterialTheme.colorScheme.secondaryContainer, 27 | shape = MaterialTheme.shapes.extraSmall 28 | ) { 29 | Row( 30 | modifier = Modifier, 31 | verticalAlignment = Alignment.CenterVertically, 32 | horizontalArrangement = Arrangement.Center, 33 | ) 34 | { 35 | Text( 36 | text = text, 37 | style = MaterialTheme.typography.bodySmall, 38 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), 39 | fontWeight = FontWeight.Bold, 40 | modifier = textModifier 41 | ) 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components.songs.metadata_viewer 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.material3.CardDefaults 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.OutlinedCard 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import androidx.compose.ui.unit.dp 23 | import com.bobbyesp.spowlo.ui.components.text.AutoResizableText 24 | 25 | @OptIn(ExperimentalMaterial3Api::class) 26 | @Composable 27 | fun ExtraInfoCard( 28 | modifier: Modifier = Modifier, 29 | onClick: () -> Unit = {}, 30 | headlineText: String = "POPULARITY", 31 | bodyText: String = "69" 32 | ) { 33 | OutlinedCard( 34 | onClick = onClick, 35 | shape = MaterialTheme.shapes.medium, 36 | modifier = modifier.size(width = 175.dp, height = 100.dp), 37 | colors = CardDefaults.outlinedCardColors( 38 | containerColor = Color.Transparent, 39 | ) 40 | ) { 41 | Text( 42 | text = headlineText, 43 | style = MaterialTheme.typography.titleSmall, 44 | fontWeight = FontWeight.Bold, 45 | modifier = Modifier 46 | .align(alignment = Alignment.CenterHorizontally) 47 | .padding(top = 8.dp) 48 | ) 49 | Box( 50 | modifier = Modifier.fillMaxSize(), 51 | contentAlignment = Alignment.Center 52 | ) { 53 | Column( 54 | modifier = Modifier 55 | .align(alignment = Alignment.Center) 56 | .padding(10.dp), 57 | verticalArrangement = Arrangement.Center, 58 | horizontalAlignment = Alignment.CenterHorizontally 59 | ) { 60 | AutoResizableText( 61 | text = bodyText, 62 | style = MaterialTheme.typography.headlineLarge, 63 | modifier = Modifier, 64 | fontWeight = FontWeight.ExtraBold 65 | ) 66 | } 67 | } 68 | } 69 | } 70 | 71 | @OptIn(ExperimentalMaterial3Api::class) 72 | @Composable 73 | fun WideExtraInfoCard( 74 | modifier: Modifier = Modifier, 75 | onClick: () -> Unit = {}, 76 | headlineText: String = "POPULARITY", 77 | bodyText: String = "69" 78 | ) { 79 | OutlinedCard( 80 | onClick = onClick, 81 | shape = MaterialTheme.shapes.medium, 82 | modifier = modifier 83 | .fillMaxWidth() 84 | .height(100.dp), 85 | colors = CardDefaults.outlinedCardColors( 86 | containerColor = Color.Transparent, 87 | ) 88 | ) { 89 | Text( 90 | text = headlineText, 91 | style = MaterialTheme.typography.titleSmall, 92 | fontWeight = FontWeight.Bold, 93 | modifier = Modifier 94 | .align(alignment = Alignment.CenterHorizontally) 95 | .padding(top = 8.dp) 96 | ) 97 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 98 | Text( 99 | text = bodyText, 100 | style = MaterialTheme.typography.headlineLarge, 101 | modifier = Modifier, 102 | fontWeight = FontWeight.ExtraBold 103 | ) 104 | } 105 | } 106 | } 107 | 108 | @Preview 109 | @Composable 110 | fun ExtraInfoCardPreview() { 111 | ExtraInfoCard() 112 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components.songs.search_feat 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.aspectRatio 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 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.draw.clip 18 | import androidx.compose.ui.layout.ContentScale 19 | import androidx.compose.ui.text.font.FontWeight 20 | import androidx.compose.ui.text.style.TextOverflow 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | import com.bobbyesp.spowlo.App 24 | import com.bobbyesp.spowlo.R 25 | import com.bobbyesp.spowlo.ui.common.AsyncImageImpl 26 | import com.bobbyesp.spowlo.ui.components.text.MarqueeText 27 | import com.bobbyesp.spowlo.utils.ChromeCustomTabsUtil 28 | 29 | @Composable 30 | fun SearchingSongComponent( 31 | artworkUrl: String, 32 | songName: String, 33 | artists: String, 34 | spotifyUrl: String, 35 | type: String = App.context.getString(R.string.single), 36 | onClick: () -> Unit = { ChromeCustomTabsUtil.openUrl(spotifyUrl) } 37 | ) { 38 | Column( 39 | Modifier 40 | .fillMaxWidth() 41 | .clickable { onClick() } 42 | .padding(8.dp)) { 43 | Row( 44 | modifier = Modifier.fillMaxWidth(), 45 | verticalAlignment = Alignment.CenterVertically, //This makes all go to the center 46 | ) { 47 | AsyncImageImpl( 48 | modifier = Modifier 49 | .padding(horizontal = 12.dp, vertical = 6.dp) 50 | .size(45.dp) 51 | .aspectRatio(1f, matchHeightConstraintsFirst = true) 52 | .clip(MaterialTheme.shapes.small), 53 | model = artworkUrl, 54 | contentDescription = "Song cover", 55 | contentScale = ContentScale.Crop, 56 | isPreview = false 57 | ) 58 | Row( 59 | modifier = Modifier.fillMaxWidth(), 60 | verticalAlignment = Alignment.CenterVertically, 61 | horizontalArrangement = Arrangement.Center 62 | ) { 63 | Column( 64 | modifier = Modifier 65 | .padding(6.dp) 66 | .weight(1f), //Weight is to make the time not go away from the screen 67 | verticalArrangement = Arrangement.Center, 68 | horizontalAlignment = Alignment.Start 69 | ) { 70 | Row( 71 | verticalAlignment = Alignment.CenterVertically, 72 | horizontalArrangement = Arrangement.Center 73 | ) { 74 | MarqueeText( 75 | text = songName, 76 | color = MaterialTheme.colorScheme.onSurface, 77 | maxLines = 1, 78 | overflow = TextOverflow.Ellipsis, 79 | fontSize = 15.sp, 80 | fontWeight = FontWeight.Bold, 81 | basicGradientColor = MaterialTheme.colorScheme.surface.copy( 82 | alpha = 0.8f 83 | ), 84 | ) 85 | } 86 | Spacer(Modifier.height(6.dp)) 87 | MarqueeText( 88 | text = "$artists • $type", 89 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), 90 | maxLines = 1, 91 | overflow = TextOverflow.Ellipsis, 92 | fontSize = 10.sp, 93 | basicGradientColor = MaterialTheme.colorScheme.surface.copy( 94 | alpha = 0.8f 95 | ), 96 | ) 97 | } 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/components/text/AnimatedTextCount.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.components.text 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.slideInVertically 5 | import androidx.compose.animation.slideOutVertically 6 | import androidx.compose.animation.togetherWith 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.SideEffect 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableIntStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.text.TextStyle 18 | 19 | @Composable 20 | fun AnimatedCounter( 21 | count: Int, 22 | modifier: Modifier = Modifier, 23 | style: TextStyle = MaterialTheme.typography.body1 24 | ) { 25 | var oldCount by remember { 26 | mutableIntStateOf(count) 27 | } 28 | SideEffect { 29 | oldCount = count 30 | } 31 | Row(modifier = modifier) { 32 | val countString = count.toString() 33 | val oldCountString = oldCount.toString() 34 | for (i in countString.indices) { 35 | val oldChar = oldCountString.getOrNull(i) 36 | val newChar = countString[i] 37 | val char = if (oldChar == newChar) { 38 | oldCountString[i] 39 | } else { 40 | countString[i] 41 | } 42 | AnimatedContent( 43 | targetState = char, 44 | transitionSpec = { 45 | slideInVertically { it } togetherWith slideOutVertically { -it } 46 | }, label = "" 47 | ) { character -> 48 | Text( 49 | text = character.toString(), 50 | style = style, 51 | softWrap = false 52 | ) 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/NotificationPermissionDialog.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.dialogs 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.NotificationsActive 5 | import androidx.compose.material3.AlertDialog 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.OutlinedButton 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import com.bobbyesp.spowlo.R 14 | 15 | @Composable 16 | @Preview 17 | fun NotificationPermissionDialog( 18 | onDismissRequest: () -> Unit = {}, 19 | onPermissionGranted: () -> Unit = {} 20 | ) { 21 | AlertDialog( 22 | onDismissRequest = onDismissRequest, 23 | icon = { 24 | Icon( 25 | imageVector = Icons.Outlined.NotificationsActive, 26 | contentDescription = null 27 | ) 28 | }, 29 | text = { 30 | Text(text = stringResource(id = R.string.enable_notifications_desc)) 31 | }, 32 | title = { Text(text = stringResource(id = R.string.enable_notifications)) }, 33 | confirmButton = { 34 | Button(onClick = onPermissionGranted) { 35 | Text(text = stringResource(id = R.string.continue_)) 36 | } 37 | }, 38 | dismissButton = { 39 | OutlinedButton(onClick = onDismissRequest) { 40 | Text(text = stringResource(id = R.string.dismiss)) 41 | } 42 | } 43 | ) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/UpdateDialog.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.dialogs 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.rememberScrollState 6 | import androidx.compose.foundation.verticalScroll 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.outlined.NewReleases 9 | import androidx.compose.material3.AlertDialog 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TextButton 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.rememberCoroutineScope 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.text.TextStyle 24 | import androidx.compose.ui.text.style.TextAlign 25 | import com.bobbyesp.spowlo.R 26 | import com.bobbyesp.spowlo.ui.components.DismissButton 27 | import com.bobbyesp.spowlo.utils.ToastUtil 28 | import com.bobbyesp.spowlo.utils.UpdateUtil 29 | import dev.jeziellago.compose.markdowntext.MarkdownText 30 | import kotlinx.coroutines.Dispatchers 31 | import kotlinx.coroutines.launch 32 | 33 | @Composable 34 | fun UpdateDialog( 35 | onDismissRequest: () -> Unit, 36 | latestRelease: UpdateUtil.LatestRelease, 37 | ) { 38 | var currentDownloadStatus by remember { mutableStateOf(UpdateUtil.DownloadStatus.NotYet as UpdateUtil.DownloadStatus) } 39 | val context = LocalContext.current 40 | 41 | val scope = rememberCoroutineScope() 42 | UpdateDialogImpl( 43 | onDismissRequest = onDismissRequest, 44 | title = latestRelease.name.toString(), 45 | onConfirmUpdate = { 46 | scope.launch(Dispatchers.IO) { 47 | runCatching { 48 | UpdateUtil.downloadApk(latestRelease = latestRelease) 49 | .collect { downloadStatus -> 50 | currentDownloadStatus = downloadStatus 51 | if (downloadStatus is UpdateUtil.DownloadStatus.Finished) { 52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 53 | UpdateUtil.installLatestApk() 54 | } 55 | } 56 | } 57 | }.onFailure { 58 | it.printStackTrace() 59 | currentDownloadStatus = UpdateUtil.DownloadStatus.NotYet 60 | ToastUtil.makeToastSuspend(context.getString(R.string.app_update_failed)) 61 | return@launch 62 | } 63 | } 64 | }, 65 | releaseNote = latestRelease.body.toString(), 66 | downloadStatus = currentDownloadStatus 67 | ) 68 | } 69 | 70 | @Composable 71 | fun UpdateDialogImpl( 72 | onDismissRequest: () -> Unit, 73 | title: String, 74 | onConfirmUpdate: () -> Unit, 75 | releaseNote: String, 76 | downloadStatus: UpdateUtil.DownloadStatus, 77 | ) { 78 | AlertDialog( 79 | onDismissRequest = {}, 80 | title = { Text(title) }, 81 | icon = { Icon(Icons.Outlined.NewReleases, null) }, confirmButton = { 82 | TextButton(onClick = { if (downloadStatus !is UpdateUtil.DownloadStatus.Progress) onConfirmUpdate() }) { 83 | when (downloadStatus) { 84 | is UpdateUtil.DownloadStatus.Progress -> Text("${downloadStatus.percent} %") 85 | else -> Text(stringResource(R.string.update)) 86 | } 87 | } 88 | }, dismissButton = { 89 | DismissButton { onDismissRequest() } 90 | }, text = { 91 | Column(Modifier.verticalScroll(rememberScrollState())) { 92 | MarkdownText( 93 | markdown = releaseNote, 94 | modifier = Modifier.weight(1f), 95 | textAlign = TextAlign.Justify, 96 | style = TextStyle.Default.copy(color = MaterialTheme.colorScheme.onSurface) 97 | ) 98 | } 99 | }) 100 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/ext/PagingExt.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.ext 2 | 3 | import androidx.compose.foundation.lazy.LazyListScope 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.paging.LoadState 7 | import androidx.paging.compose.LazyPagingItems 8 | import com.bobbyesp.spowlo.BuildConfig 9 | 10 | fun LazyListScope.loadStateContent( 11 | items: LazyPagingItems, 12 | itemCount: Int = 7, 13 | loadingContent: @Composable () -> Unit 14 | ) { 15 | items.apply { 16 | when { 17 | loadState.refresh is LoadState.Loading -> { 18 | items(itemCount) { 19 | // Render a loading indicator while refreshing 20 | loadingContent() 21 | } 22 | } 23 | 24 | loadState.append is LoadState.Loading -> { 25 | items(itemCount) { 26 | // Render a loading indicator at the end while loading more items 27 | loadingContent() 28 | } 29 | } 30 | 31 | loadState.refresh is LoadState.Error -> { 32 | val errorMessage = 33 | (loadState.refresh as LoadState.Error).error.message 34 | item { 35 | // Render an error message if refreshing encounters an error 36 | if (errorMessage != null) { 37 | if (BuildConfig.DEBUG) Text(errorMessage) 38 | } 39 | } 40 | } 41 | 42 | loadState.append is LoadState.Error -> { 43 | val errorMessage = 44 | (loadState.append as LoadState.Error).error.message 45 | item { 46 | // Render an error message if loading more items encounters an error 47 | if (errorMessage != null) { 48 | if (BuildConfig.DEBUG) Text(errorMessage) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/icons/artist.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.icons 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.PathFillType 7 | import androidx.compose.ui.graphics.SolidColor 8 | import androidx.compose.ui.graphics.StrokeCap 9 | import androidx.compose.ui.graphics.StrokeJoin 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | import androidx.compose.ui.graphics.vector.group 12 | import androidx.compose.ui.graphics.vector.path 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | 16 | 17 | @Preview 18 | @Composable 19 | private fun VectorPreview() { 20 | Image(Artist, null) 21 | } 22 | 23 | private var _Artist: ImageVector? = null 24 | 25 | public val Artist: ImageVector 26 | get() { 27 | if (_Artist != null) { 28 | return _Artist!! 29 | } 30 | _Artist = ImageVector.Builder( 31 | name = "Artist", 32 | defaultWidth = 24.dp, 33 | defaultHeight = 24.dp, 34 | viewportWidth = 24f, 35 | viewportHeight = 24f 36 | ).apply { 37 | group { 38 | path( 39 | fill = null, 40 | fillAlpha = 1.0f, 41 | stroke = SolidColor(Color(0xFF292929)), 42 | strokeAlpha = 1.0f, 43 | strokeLineWidth = 2.5f, 44 | strokeLineCap = StrokeCap.Butt, 45 | strokeLineJoin = StrokeJoin.Miter, 46 | strokeLineMiter = 1.0f, 47 | pathFillType = PathFillType.NonZero 48 | ) { 49 | moveTo(15f, 7f) 50 | arcTo(3f, 3f, 0f, isMoreThanHalf = false, isPositiveArc = true, 12f, 10f) 51 | arcTo(3f, 3f, 0f, isMoreThanHalf = false, isPositiveArc = true, 9f, 7f) 52 | arcTo(3f, 3f, 0f, isMoreThanHalf = false, isPositiveArc = true, 15f, 7f) 53 | close() 54 | } 55 | path( 56 | fill = null, 57 | fillAlpha = 1.0f, 58 | stroke = SolidColor(Color(0xFF292929)), 59 | strokeAlpha = 1.0f, 60 | strokeLineWidth = 2.5f, 61 | strokeLineCap = StrokeCap.Round, 62 | strokeLineJoin = StrokeJoin.Round, 63 | strokeLineMiter = 1.0f, 64 | pathFillType = PathFillType.NonZero 65 | ) { 66 | moveTo(20f, 18f) 67 | arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 18f, 20f) 68 | arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 16f, 18f) 69 | arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 20f, 18f) 70 | close() 71 | } 72 | path( 73 | fill = null, 74 | fillAlpha = 1.0f, 75 | stroke = SolidColor(Color(0xFF292929)), 76 | strokeAlpha = 1.0f, 77 | strokeLineWidth = 2.5f, 78 | strokeLineCap = StrokeCap.Round, 79 | strokeLineJoin = StrokeJoin.Round, 80 | strokeLineMiter = 1.0f, 81 | pathFillType = PathFillType.NonZero 82 | ) { 83 | moveTo(12.3414f, 20f) 84 | horizontalLineTo(6f) 85 | curveTo(4.8954f, 20f, 4f, 19.1046f, 4f, 18f) 86 | curveTo(4f, 15.7909f, 5.7909f, 14f, 8f, 14f) 87 | horizontalLineTo(13.5278f) 88 | } 89 | path( 90 | fill = null, 91 | fillAlpha = 1.0f, 92 | stroke = SolidColor(Color(0xFF292929)), 93 | strokeAlpha = 1.0f, 94 | strokeLineWidth = 2.5f, 95 | strokeLineCap = StrokeCap.Round, 96 | strokeLineJoin = StrokeJoin.Round, 97 | strokeLineMiter = 1.0f, 98 | pathFillType = PathFillType.NonZero 99 | ) { 100 | moveTo(20f, 18f) 101 | verticalLineTo(11f) 102 | lineTo(22f, 13f) 103 | } 104 | } 105 | }.build() 106 | return _Artist!! 107 | } 108 | 109 | 110 | // https://www.svgrepo.com/svg/489526/music-artist 111 | 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/icons/yt_music.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.icons 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.PathFillType 7 | import androidx.compose.ui.graphics.SolidColor 8 | import androidx.compose.ui.graphics.StrokeCap 9 | import androidx.compose.ui.graphics.StrokeJoin 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | import androidx.compose.ui.graphics.vector.group 12 | import androidx.compose.ui.graphics.vector.path 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | 16 | 17 | @Preview 18 | @Composable 19 | private fun VectorPreview() { 20 | Image(YouTubeMusic, null) 21 | } 22 | 23 | private var _YouTubeMusic: ImageVector? = null 24 | 25 | public val YouTubeMusic: ImageVector 26 | get() { 27 | if (_YouTubeMusic != null) { 28 | return _YouTubeMusic!! 29 | } 30 | _YouTubeMusic = ImageVector.Builder( 31 | name = "YouTubeMusic", 32 | defaultWidth = 24.dp, 33 | defaultHeight = 24.dp, 34 | viewportWidth = 50f, 35 | viewportHeight = 50f 36 | ).apply { 37 | group { 38 | path( 39 | fill = SolidColor(Color.Black), 40 | fillAlpha = 1.0f, 41 | stroke = null, 42 | strokeAlpha = 1.0f, 43 | strokeLineWidth = 1.0f, 44 | strokeLineCap = StrokeCap.Butt, 45 | strokeLineJoin = StrokeJoin.Miter, 46 | strokeLineMiter = 1.0f, 47 | pathFillType = PathFillType.NonZero 48 | ) { 49 | moveTo(25f, 13f) 50 | curveToRelative(-6.617f, 0f, -12f, 5.383f, -12f, 12f) 51 | reflectiveCurveToRelative(5.383f, 12f, 12f, 12f) 52 | reflectiveCurveToRelative(12f, -5.383f, 12f, -12f) 53 | reflectiveCurveTo(31.617f, 13f, 25f, 13f) 54 | close() 55 | moveTo(31.521f, 25.854f) 56 | lineToRelative(-9f, 5.5f) 57 | curveTo(22.361f, 31.451f, 22.181f, 31.5f, 22f, 31.5f) 58 | curveToRelative(-0.168f, 0f, -0.337f, -0.043f, -0.489f, -0.128f) 59 | curveTo(21.195f, 31.195f, 21f, 30.861f, 21f, 30.5f) 60 | verticalLineToRelative(-11f) 61 | curveToRelative(0f, -0.361f, 0.195f, -0.695f, 0.511f, -0.872f) 62 | curveToRelative(0.317f, -0.176f, 0.702f, -0.169f, 1.011f, 0.019f) 63 | lineToRelative(9f, 5.5f) 64 | curveTo(31.818f, 24.328f, 32f, 24.651f, 32f, 25f) 65 | reflectiveCurveTo(31.818f, 25.672f, 31.521f, 25.854f) 66 | close() 67 | } 68 | path( 69 | fill = SolidColor(Color.Black), 70 | fillAlpha = 1.0f, 71 | stroke = null, 72 | strokeAlpha = 1.0f, 73 | strokeLineWidth = 1.0f, 74 | strokeLineCap = StrokeCap.Butt, 75 | strokeLineJoin = StrokeJoin.Miter, 76 | strokeLineMiter = 1.0f, 77 | pathFillType = PathFillType.NonZero 78 | ) { 79 | moveTo(25f, 3f) 80 | curveTo(12.85f, 3f, 3f, 12.85f, 3f, 25f) 81 | curveToRelative(0f, 12.15f, 9.85f, 22f, 22f, 22f) 82 | reflectiveCurveToRelative(22f, -9.85f, 22f, -22f) 83 | curveTo(47f, 12.85f, 37.15f, 3f, 25f, 3f) 84 | close() 85 | moveTo(25f, 39f) 86 | curveToRelative(-7.72f, 0f, -14f, -6.28f, -14f, -14f) 87 | reflectiveCurveToRelative(6.28f, -14f, 14f, -14f) 88 | reflectiveCurveToRelative(14f, 6.28f, 14f, 14f) 89 | reflectiveCurveTo(32.72f, 39f, 25f, 39f) 90 | close() 91 | } 92 | } 93 | }.build() 94 | return _YouTubeMusic!! 95 | } 96 | 97 | 98 | // https://icons8.com/icon/Mw6P3tmWMfOB/youtube-music -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/MarkdownViewerPage.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages 2 | 3 | import android.content.Context 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Scaffold 10 | import androidx.compose.material3.Text 11 | import androidx.compose.material3.TopAppBar 12 | import androidx.compose.material3.TopAppBarDefaults 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.input.nestedscroll.nestedScroll 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.text.style.TextAlign 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import coil.imageLoader 23 | import com.bobbyesp.spowlo.R 24 | import com.bobbyesp.spowlo.ui.components.BackButton 25 | import com.bobbyesp.spowlo.utils.ChromeCustomTabsUtil 26 | import dev.jeziellago.compose.markdowntext.MarkdownText 27 | 28 | @OptIn(ExperimentalMaterial3Api::class) 29 | @Composable 30 | fun MarkdownViewerPage( 31 | markdownFileName: String, 32 | onBackPressed: () -> Unit 33 | ) { 34 | val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() 35 | 36 | //Read markdown file from the "raw" folder following the name of the file 37 | val markdownText: String = when (markdownFileName) { 38 | "index.md" -> readMarkdownFile(LocalContext.current, R.raw.index) 39 | "cli_commands.md" -> readMarkdownFile(LocalContext.current, R.raw.cli_commands) 40 | 41 | else -> readMarkdownFile(LocalContext.current, R.raw.index) 42 | } 43 | 44 | Scaffold(modifier = Modifier 45 | .fillMaxSize() 46 | .nestedScroll(scrollBehavior.nestedScrollConnection), 47 | topBar = { 48 | TopAppBar(title = { 49 | Text( 50 | text = stringResource(id = R.string.markdown_viewer), 51 | style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp) 52 | ) 53 | }, navigationIcon = { 54 | BackButton { onBackPressed() } 55 | }, actions = { 56 | }, scrollBehavior = scrollBehavior 57 | ) 58 | }) { paddings -> 59 | MarkdownText( 60 | modifier = Modifier 61 | .fillMaxSize() 62 | .padding(paddings) 63 | .padding(6.dp), 64 | markdown = markdownText, 65 | textAlign = TextAlign.Justify, 66 | color = if (isSystemInDarkTheme()) Color.White else Color.Black, 67 | onLinkClicked = { url -> 68 | ChromeCustomTabsUtil.openUrl(url) 69 | }, 70 | imageLoader = LocalContext.current.imageLoader, 71 | ) 72 | } 73 | } 74 | 75 | fun readMarkdownFile(context: Context, resourceId: Int): String { 76 | val inputStream = context.resources.openRawResource(resourceId) 77 | return inputStream.bufferedReader().use { it.readText() } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/common_pages/ErrorPage.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.common_pages 2 | 3 | import androidx.compose.foundation.layout.Box 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.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.rounded.Error 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.OutlinedButton 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.platform.LocalClipboardManager 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.text.AnnotatedString 21 | import androidx.compose.ui.unit.dp 22 | import com.bobbyesp.spowlo.R 23 | 24 | @Composable 25 | fun ErrorPage( 26 | onReload: () -> Unit, 27 | exception: String, 28 | modifier: Modifier 29 | ) { 30 | val clipboard = LocalClipboardManager.current 31 | 32 | Box(modifier) { 33 | Column( 34 | Modifier 35 | .align(Alignment.Center) 36 | ) { 37 | Icon( 38 | Icons.Rounded.Error, contentDescription = null, modifier = Modifier 39 | .align(Alignment.CenterHorizontally) 40 | .size(56.dp) 41 | .padding(bottom = 12.dp) 42 | ) 43 | Text( 44 | stringResource(id = R.string.searching_error), 45 | modifier = Modifier.align(Alignment.CenterHorizontally) 46 | ) 47 | } 48 | 49 | Row( 50 | modifier = Modifier 51 | .align(Alignment.BottomCenter) 52 | .padding(bottom = 16.dp) 53 | ) { 54 | OutlinedButton( 55 | onClick = { 56 | clipboard.setText(AnnotatedString("Message: ${exception}\n\n")) 57 | }) { 58 | Text(stringResource(id = R.string.error_copy)) 59 | } 60 | 61 | Spacer(modifier = Modifier.width(8.dp)) 62 | 63 | OutlinedButton( 64 | onClick = { onReload() }) { 65 | Text(stringResource(id = R.string.err_act_reload)) 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/common_pages/LoadingPage.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.common_pages 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material3.CircularProgressIndicator 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.unit.dp 19 | import com.bobbyesp.spowlo.R 20 | 21 | @Composable 22 | fun LoadingPage() { 23 | //create a loading page 24 | Box( 25 | modifier = Modifier 26 | .fillMaxSize() 27 | .background(MaterialTheme.colorScheme.background) 28 | ) { 29 | Column( 30 | modifier = Modifier.align(Alignment.Center), 31 | verticalArrangement = Arrangement.Center, 32 | horizontalAlignment = Alignment.CenterHorizontally 33 | ) { 34 | CircularProgressIndicator( 35 | modifier = Modifier 36 | .size(72.dp) 37 | .padding(6.dp), 38 | strokeWidth = 4.dp 39 | ) 40 | Text( 41 | text = stringResource(id = R.string.page_loading), 42 | modifier = Modifier.align(Alignment.CenterHorizontally), 43 | style = MaterialTheme.typography.headlineSmall, 44 | fontWeight = FontWeight.Bold 45 | ) 46 | } 47 | 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/common_pages/NotImplementedPage.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.common_pages 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.text.font.FontWeight 15 | import com.bobbyesp.spowlo.R 16 | 17 | @Composable 18 | fun NotImplementedPage( 19 | ) { 20 | Box( 21 | modifier = Modifier 22 | .fillMaxSize() 23 | .background(MaterialTheme.colorScheme.background) 24 | ) { 25 | Column( 26 | modifier = Modifier.align(Alignment.Center), 27 | verticalArrangement = Arrangement.Center, 28 | horizontalAlignment = Alignment.CenterHorizontally 29 | ) { 30 | Text( 31 | text = stringResource(id = R.string.not_implemented), modifier = Modifier.align( 32 | Alignment.CenterHorizontally 33 | ), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold 34 | ) 35 | } 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.downloader 2 | 3 | import androidx.compose.material.ExperimentalMaterialApi 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.material3.SheetState 6 | import androidx.lifecycle.ViewModel 7 | import com.bobbyesp.library.domain.model.SpotifySong 8 | import com.bobbyesp.spowlo.Downloader 9 | import com.bobbyesp.spowlo.Downloader.showErrorMessage 10 | import com.bobbyesp.spowlo.R 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.asStateFlow 14 | import kotlinx.coroutines.flow.update 15 | import kotlinx.coroutines.launch 16 | 17 | @OptIn(ExperimentalMaterialApi::class) 18 | class DownloaderViewModel : ViewModel() { 19 | 20 | private val mutableViewStateFlow = MutableStateFlow(ViewState()) 21 | val viewStateFlow = mutableViewStateFlow.asStateFlow() 22 | 23 | private val songInfoFlow = MutableStateFlow(listOf(SpotifySong())) 24 | 25 | data class ViewState( 26 | val url: String = "", 27 | val showDownloadSettingDialog: Boolean = false, 28 | val isUrlSharingTriggered: Boolean = false, 29 | ) 30 | 31 | fun updateUrl(url: String, isUrlSharingTriggered: Boolean = false) = 32 | mutableViewStateFlow.update { 33 | it.copy( 34 | url = url, isUrlSharingTriggered = isUrlSharingTriggered 35 | ) 36 | } 37 | 38 | @OptIn(ExperimentalMaterial3Api::class) 39 | fun hideDialog(scope: CoroutineScope, isDialog: Boolean, sheetState: SheetState) { 40 | scope.launch { 41 | if (isDialog) mutableViewStateFlow.update { it.copy(showDownloadSettingDialog = false) } 42 | else sheetState.hide() 43 | } 44 | } 45 | 46 | @OptIn(ExperimentalMaterial3Api::class) 47 | fun showDialog(scope: CoroutineScope, isDialog: Boolean, sheetState: SheetState) { 48 | scope.launch { 49 | if (isDialog) mutableViewStateFlow.update { it.copy(showDownloadSettingDialog = true) } 50 | else sheetState.show() 51 | } 52 | } 53 | 54 | fun requestMetadata() { 55 | val url = viewStateFlow.value.url 56 | Downloader.clearErrorState() 57 | if (!Downloader.isDownloaderAvailable()) 58 | return 59 | if (url.isBlank()) { 60 | showErrorMessage(R.string.url_empty) 61 | return 62 | } 63 | Downloader.getRequestedMetadata(url) 64 | } 65 | 66 | fun startDownloadSong(skipInfoFetch: Boolean = false) { 67 | 68 | val url = viewStateFlow.value.url 69 | Downloader.clearErrorState() 70 | if (!Downloader.isDownloaderAvailable()) 71 | return 72 | if (url.isBlank()) { 73 | showErrorMessage(R.string.url_empty) 74 | return 75 | } 76 | Downloader.getInfoAndDownload(url, skipInfoFetch = skipInfoFetch) 77 | } 78 | 79 | fun goToMetadataViewer(songs: List) { 80 | songInfoFlow.update { songs } 81 | } 82 | 83 | fun onShareIntentConsumed() { 84 | mutableViewStateFlow.update { it.copy(isUrlSharingTriggered = false) } 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadsHistoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.history 2 | 3 | import androidx.compose.material.ExperimentalMaterialApi 4 | import androidx.compose.material.ModalBottomSheetState 5 | import androidx.compose.material.ModalBottomSheetValue 6 | import androidx.compose.ui.unit.Density 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.bobbyesp.spowlo.App 10 | import com.bobbyesp.spowlo.database.DownloadedSongInfo 11 | import com.bobbyesp.spowlo.utils.DatabaseUtil 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.asStateFlow 17 | import kotlinx.coroutines.flow.map 18 | import kotlinx.coroutines.flow.update 19 | import kotlinx.coroutines.launch 20 | import kotlinx.coroutines.withContext 21 | 22 | @OptIn(ExperimentalMaterialApi::class) 23 | class DownloadsHistoryViewModel : ViewModel() { 24 | 25 | private val _mediaInfoFlow = DatabaseUtil.getMediaInfo() 26 | 27 | private val mediaInfoFlow: Flow> = 28 | _mediaInfoFlow.map { it.reversed() } 29 | 30 | val songsListFlow = mediaInfoFlow 31 | 32 | private val mutableStateFlow = MutableStateFlow(SongsListViewState()) 33 | val stateFlow = mutableStateFlow.asStateFlow() 34 | 35 | val filterSetFlow = _mediaInfoFlow.map { infoList -> 36 | mutableSetOf().apply { 37 | infoList.forEach { 38 | this.add(it.songAuthor) 39 | } 40 | } 41 | } 42 | 43 | data class SongsListViewState( 44 | val activeFilterIndex: Int = -1 45 | ) 46 | 47 | data class SongDetailViewState( 48 | val id: Int = 0, 49 | val title: String = "", 50 | val author: String = "", 51 | val url: String = "", 52 | val artworkUrl: String = "", 53 | val path: String = "", 54 | val duration: Double = 0.0, 55 | val drawerState: ModalBottomSheetState = ModalBottomSheetState( 56 | ModalBottomSheetValue.Hidden, isSkipHalfExpanded = true, 57 | density = Density(context = App.context) 58 | ), 59 | val showDialog: Boolean = false, 60 | ) { 61 | constructor(info: DownloadedSongInfo) : this( 62 | info.id, 63 | info.songName, 64 | info.songAuthor, 65 | info.songUrl, 66 | info.thumbnailUrl, 67 | info.songPath, 68 | info.songDuration 69 | ) 70 | } 71 | 72 | private val _detailViewState = MutableStateFlow(SongDetailViewState()) 73 | val detailViewState = _detailViewState.asStateFlow() 74 | 75 | fun clickAuthorFilter(index: Int) { 76 | if (mutableStateFlow.value.activeFilterIndex == index) mutableStateFlow.update { 77 | it.copy( 78 | activeFilterIndex = -1 79 | ) 80 | } 81 | else mutableStateFlow.update { it.copy(activeFilterIndex = index) } 82 | } 83 | 84 | fun hideDrawer(scope: CoroutineScope): Boolean { 85 | if (_detailViewState.value.drawerState.isVisible) { 86 | scope.launch { 87 | _detailViewState.value.drawerState.hide() 88 | } 89 | return true 90 | } 91 | return false 92 | } 93 | 94 | fun showDrawer(scope: CoroutineScope, item: DownloadedSongInfo) { 95 | scope.launch { 96 | _detailViewState.update { 97 | SongDetailViewState(item) 98 | } 99 | _detailViewState.value.drawerState.show() 100 | } 101 | } 102 | 103 | fun showDialog() { 104 | _detailViewState.update { it.copy(showDialog = true) } 105 | } 106 | 107 | fun hideDialog() { 108 | _detailViewState.update { it.copy(showDialog = false) } 109 | } 110 | 111 | fun removeItem(delete: Boolean) { 112 | viewModelScope.launch { 113 | withContext(Dispatchers.IO) { 114 | DatabaseUtil.deleteInfoListByIdList(listOf(detailViewState.value.id), delete) 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.res.stringResource 5 | import com.bobbyesp.spowlo.R 6 | import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType 7 | 8 | //make a composable that has as parameter a SpotifyData and returns the type of the data as a string with a string resource 9 | @Composable 10 | fun typeOfDataToString(type: SpotifyDataType): String { 11 | return when (type) { 12 | SpotifyDataType.ALBUM -> stringResource(id = R.string.album) 13 | SpotifyDataType.ARTIST -> stringResource(id = R.string.artist) 14 | SpotifyDataType.PLAYLIST -> stringResource(id = R.string.playlist) 15 | SpotifyDataType.TRACK -> stringResource(id = R.string.track) 16 | } 17 | } 18 | 19 | //Assign and return the type of the data from referred to the SpotifyDataType enum 20 | fun typeOfSpotifyDataType(type: String): SpotifyDataType { 21 | return when (type) { 22 | "track" -> SpotifyDataType.TRACK 23 | "album" -> SpotifyDataType.ALBUM 24 | "playlist" -> SpotifyDataType.PLAYLIST 25 | "artist" -> SpotifyDataType.ARTIST 26 | else -> SpotifyDataType.TRACK 27 | } 28 | } 29 | 30 | @Composable 31 | fun dataStringToString(data: String, additional: String): String { 32 | return when (typeOfSpotifyDataType(data)) { 33 | SpotifyDataType.ALBUM -> stringResource(id = R.string.album) + " • " + additional 34 | SpotifyDataType.ARTIST -> stringResource(id = R.string.artist) + " • " + additional 35 | SpotifyDataType.PLAYLIST -> stringResource(id = R.string.playlist) + " • " + additional 36 | SpotifyDataType.TRACK -> stringResource(id = R.string.track) + " • " + additional 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/mod_downloader/ModsDownloaderViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.mod_downloader 2 | 3 | import androidx.compose.material.ExperimentalMaterialApi 4 | import androidx.lifecycle.ViewModel 5 | import com.bobbyesp.spowlo.features.mod_downloader.domain.model.APIResponseDto 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.update 8 | import javax.inject.Inject 9 | 10 | @OptIn(ExperimentalMaterialApi::class) 11 | class ModsDownloaderViewModel @Inject constructor() : ViewModel() { 12 | 13 | val apiResponseFlow = MutableStateFlow(APIResponseDto()) 14 | fun updateApiResponse(apiResponseDto: APIResponseDto) { 15 | apiResponseFlow.update { 16 | apiResponseDto 17 | } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/playlist/PlaylistMetadataPage.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.playlist 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Scaffold 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TopAppBar 14 | import androidx.compose.material3.TopAppBarDefaults 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.rememberCoroutineScope 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.input.nestedscroll.nestedScroll 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 23 | import com.bobbyesp.spowlo.R 24 | import com.bobbyesp.spowlo.ui.components.BackButton 25 | import com.bobbyesp.spowlo.ui.components.songs.PlaylistHeaderItem 26 | import com.bobbyesp.spowlo.ui.components.songs.SongMetadataCard 27 | import com.bobbyesp.spowlo.utils.DownloaderUtil 28 | 29 | @OptIn(ExperimentalMaterial3Api::class) 30 | @Composable 31 | fun PlaylistMetadataPage(onBackPressed: () -> Unit) { 32 | 33 | //val songs = downloaderViewModel.songInfoFlow.collectAsStateWithLifecycle() 34 | val songs = DownloaderUtil.songsState.collectAsStateWithLifecycle() 35 | 36 | val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() 37 | 38 | Scaffold(modifier = Modifier 39 | .fillMaxSize() 40 | .nestedScroll(scrollBehavior.nestedScrollConnection), 41 | topBar = { 42 | TopAppBar(title = { 43 | Text( 44 | text = if (songs.value.size == 1) stringResource(id = R.string.song_metadata) else stringResource( 45 | R.string.playlist_metadata 46 | ), 47 | style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp) 48 | ) 49 | }, navigationIcon = { 50 | BackButton { onBackPressed() } 51 | }, actions = { 52 | }, scrollBehavior = scrollBehavior 53 | ) 54 | }) { paddings -> 55 | LazyColumn( 56 | modifier = Modifier.padding(paddings), contentPadding = PaddingValues(24.dp), 57 | verticalArrangement = Arrangement.spacedBy(12.dp) 58 | ) { 59 | item { 60 | if (songs.value[0].song_list != null) { 61 | PlaylistHeaderItem(playlist = songs.value[0], modifier = Modifier.padding()) 62 | } 63 | } 64 | items(songs.value.size) { index -> 65 | SongMetadataCard(song = songs.value[index]) 66 | Spacer(modifier = Modifier.padding(10.dp)) 67 | } 68 | 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/CookiesSettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.settings.cookies 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.bobbyesp.spowlo.database.CookieProfile 6 | import com.bobbyesp.spowlo.utils.DatabaseUtil 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.asStateFlow 10 | import kotlinx.coroutines.flow.update 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | class CookiesSettingsViewModel @Inject constructor() : ViewModel() { 15 | 16 | companion object { 17 | const val NEW_PROFILE_ID = 0 18 | } 19 | 20 | data class ViewState( 21 | val showEditDialog: Boolean = false, 22 | val showDeleteDialog: Boolean = false, 23 | val editingCookieProfile: CookieProfile = CookieProfile( 24 | id = NEW_PROFILE_ID, 25 | url = "", 26 | content = "" 27 | ) 28 | ) 29 | 30 | val cookiesFlow = DatabaseUtil.getCookiesFlow() 31 | 32 | private val mutableStateFlow = MutableStateFlow(ViewState()) 33 | val stateFlow = mutableStateFlow.asStateFlow() 34 | 35 | fun showEditCookieDialog( 36 | cookieProfile: CookieProfile = CookieProfile( 37 | id = NEW_PROFILE_ID, 38 | url = "", 39 | content = "" 40 | ) 41 | ) { 42 | mutableStateFlow.update { 43 | it.copy( 44 | editingCookieProfile = cookieProfile, 45 | showEditDialog = true 46 | ) 47 | } 48 | } 49 | 50 | fun showDeleteCookieDialog(cookieProfile: CookieProfile) { 51 | mutableStateFlow.update { 52 | it.copy( 53 | editingCookieProfile = cookieProfile, 54 | showDeleteDialog = true 55 | ) 56 | } 57 | } 58 | 59 | fun deleteCookieProfile(cookieProfile: CookieProfile = stateFlow.value.editingCookieProfile) { 60 | viewModelScope.launch(Dispatchers.IO) { DatabaseUtil.deleteCookieProfile(cookieProfile) } 61 | } 62 | 63 | fun generateNewCookies(content: String) { 64 | viewModelScope.launch(Dispatchers.IO) { 65 | mutableStateFlow.update { 66 | val newProfile = it.editingCookieProfile.copy(content = content) 67 | DatabaseUtil.updateCookieProfile(newProfile) 68 | it.copy(editingCookieProfile = newProfile) 69 | } 70 | } 71 | } 72 | 73 | fun updateUrl(url: String) = 74 | mutableStateFlow.update { it.copy(editingCookieProfile = it.editingCookieProfile.copy(url = url)) } 75 | 76 | fun updateContent(content: String) = 77 | mutableStateFlow.update { 78 | it.copy( 79 | editingCookieProfile = it.editingCookieProfile.copy( 80 | content = content 81 | ) 82 | ) 83 | } 84 | 85 | fun updateCookieProfile() { 86 | viewModelScope.launch(Dispatchers.IO) { 87 | mutableStateFlow.update { 88 | if (it.editingCookieProfile.id == NEW_PROFILE_ID) { 89 | DatabaseUtil.insertCookieProfile(it.editingCookieProfile) 90 | } else { 91 | DatabaseUtil.updateCookieProfile(it.editingCookieProfile) 92 | } 93 | it.copy(showEditDialog = false) 94 | } 95 | } 96 | } 97 | 98 | fun hideDialog() { 99 | mutableStateFlow.update { 100 | it.copy( 101 | showEditDialog = false, 102 | showDeleteDialog = false 103 | ) 104 | } 105 | } 106 | 107 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/documentation/DocumentationPage.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.pages.settings.documentation 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.Scaffold 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TopAppBarDefaults 10 | import androidx.compose.material3.rememberTopAppBarState 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.input.nestedscroll.nestedScroll 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.unit.dp 17 | import androidx.navigation.NavController 18 | import com.bobbyesp.spowlo.R 19 | import com.bobbyesp.spowlo.ui.components.BackButton 20 | import com.bobbyesp.spowlo.ui.components.HorizontalDivider 21 | import com.bobbyesp.spowlo.ui.components.InlineEnterItem 22 | import com.bobbyesp.spowlo.ui.components.LargeTopAppBar 23 | import com.bobbyesp.spowlo.ui.components.PreferenceInfo 24 | 25 | @OptIn(ExperimentalMaterial3Api::class) 26 | @Composable 27 | fun DocumentationPage( 28 | onBackPressed: () -> Unit, 29 | navController: NavController 30 | ) { 31 | val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( 32 | rememberTopAppBarState(), 33 | canScroll = { true } 34 | ) 35 | 36 | Scaffold( 37 | modifier = Modifier 38 | .fillMaxSize() 39 | .nestedScroll(scrollBehavior.nestedScrollConnection), 40 | topBar = { 41 | LargeTopAppBar( 42 | title = { 43 | Text( 44 | modifier = Modifier, 45 | text = stringResource(id = R.string.documentation), 46 | fontWeight = FontWeight.Bold 47 | ) 48 | }, navigationIcon = { 49 | BackButton { 50 | onBackPressed() 51 | } 52 | }, scrollBehavior = scrollBehavior 53 | ) 54 | }, content = { paddingValues -> 55 | Column( 56 | modifier = Modifier 57 | .fillMaxSize() 58 | .padding(paddingValues) 59 | ) { 60 | InlineEnterItem(title = stringResource(id = R.string.index)) { 61 | val uri = "markdown_viewer/index.md" 62 | navController.navigate(uri) 63 | } 64 | InlineEnterItem(title = stringResource(id = R.string.commands)) { 65 | val uri = "markdown_viewer/cli_commands.md" 66 | navController.navigate(uri) 67 | } 68 | 69 | HorizontalDivider(Modifier.padding(vertical = 6.dp)) 70 | PreferenceInfo( 71 | modifier = Modifier 72 | .padding(horizontal = 4.dp), 73 | text = stringResource(id = R.string.documentation_info) 74 | ) 75 | } 76 | } 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/theme/ColorScheme.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.theme 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.unit.dp 14 | import com.bobbyesp.spowlo.ui.common.LocalDarkTheme 15 | 16 | @Composable 17 | infix fun Color.withNight(nightColor: Color): Color { 18 | return if (LocalDarkTheme.current.isDarkTheme()) nightColor else this 19 | } 20 | 21 | //Create a blur effect 22 | 23 | @Composable 24 | fun BlurEffect( 25 | modifier: Modifier = Modifier, 26 | color: Color = MaterialTheme.colorScheme.surface, 27 | content: @Composable () -> Unit 28 | ) { 29 | Box( 30 | modifier = modifier 31 | .background(color = color) 32 | .padding(16.dp) 33 | .clip(RoundedCornerShape(16.dp)) 34 | .background( 35 | color = Color.Transparent, 36 | shape = RoundedCornerShape(16.dp) 37 | ) 38 | ) { 39 | content() 40 | } 41 | } 42 | 43 | const val DEFAULT_SEED_COLOR = 0xFF415f76.toInt() 44 | 45 | @Composable 46 | fun contraryColor(): Color { 47 | return if (isSystemInDarkTheme()) Color.White else Color.Black 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.theme 2 | 3 | import androidx.compose.material3.Shapes 4 | 5 | val Shapes = Shapes( 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.theme 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.view.Window 7 | import androidx.compose.foundation.isSystemInDarkTheme 8 | import androidx.compose.material3.LocalTextStyle 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.ProvideTextStyle 11 | import androidx.compose.material3.dynamicDarkColorScheme 12 | import androidx.compose.material3.dynamicLightColorScheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.toArgb 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.platform.LocalView 18 | import androidx.compose.ui.text.style.LineBreak 19 | import androidx.core.view.WindowCompat 20 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 21 | import com.google.android.material.color.DynamicColors 22 | import com.google.android.material.color.MaterialColors 23 | import com.kyant.monet.dynamicColorScheme 24 | 25 | fun Color.applyOpacity(enabled: Boolean): Color { 26 | return if (enabled) this else this.copy(alpha = 0.62f) 27 | } 28 | 29 | @Composable 30 | fun Color.harmonizeWith(other: Color) = 31 | Color(MaterialColors.harmonize(this.toArgb(), other.toArgb())) 32 | 33 | @Composable 34 | fun Color.harmonizeWithPrimary(): Color = 35 | this.harmonizeWith(other = MaterialTheme.colorScheme.primary) 36 | 37 | 38 | private tailrec fun Context.findWindow(): Window? = 39 | when (this) { 40 | is Activity -> window 41 | is ContextWrapper -> baseContext.findWindow() 42 | else -> null 43 | } 44 | 45 | @Composable 46 | fun SpowloTheme( 47 | darkTheme: Boolean = isSystemInDarkTheme(), 48 | isHighContrastModeEnabled: Boolean = false, 49 | isDynamicColorEnabled: Boolean = false, 50 | content: @Composable () -> Unit 51 | ) { 52 | val colorScheme = when { 53 | DynamicColors.isDynamicColorAvailable() && isDynamicColorEnabled -> { 54 | val context = LocalContext.current 55 | if (darkTheme) { 56 | dynamicDarkColorScheme(context) 57 | } else { 58 | dynamicLightColorScheme(context) 59 | } 60 | } 61 | 62 | else -> dynamicColorScheme(!darkTheme) 63 | }.run { 64 | if (isHighContrastModeEnabled && darkTheme) copy( 65 | surface = Color.Black, 66 | background = Color.Black, 67 | ) 68 | else this 69 | } 70 | val window = LocalView.current.context.findWindow() 71 | val view = LocalView.current 72 | 73 | window?.let { 74 | WindowCompat.getInsetsController(it, view).isAppearanceLightStatusBars = darkTheme 75 | } 76 | 77 | rememberSystemUiController(window).setSystemBarsColor(Color.Transparent, !darkTheme) 78 | 79 | ProvideTextStyle( 80 | value = LocalTextStyle.current.copy( 81 | lineBreak = LineBreak.Paragraph 82 | ) 83 | ) { 84 | MaterialTheme( 85 | colorScheme = colorScheme, 86 | typography = Typography, 87 | shapes = Shapes, 88 | content = content 89 | ) 90 | } 91 | } 92 | 93 | @Composable 94 | fun PreviewThemeLight( 95 | content: @Composable () -> Unit 96 | ) { 97 | MaterialTheme( 98 | colorScheme = dynamicColorScheme(), 99 | typography = Typography, 100 | shapes = Shapes, 101 | content = content 102 | ) 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.ExperimentalTextApi 5 | import androidx.compose.ui.text.TextStyle 6 | import androidx.compose.ui.text.font.FontFamily 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.text.style.LineBreak 9 | import androidx.compose.ui.unit.sp 10 | 11 | // Set of Material typography styles 12 | val Typography = 13 | Typography().run { 14 | copy( 15 | bodyLarge = bodyLarge.applyLinebreak(), 16 | bodyMedium = bodyMedium.applyLinebreak(), 17 | bodySmall = bodySmall.applyLinebreak(), 18 | titleLarge = titleLarge, 19 | titleMedium = titleMedium, 20 | titleSmall = titleSmall, 21 | headlineSmall = headlineSmall, 22 | headlineMedium = headlineMedium, 23 | headlineLarge = headlineLarge, 24 | displaySmall = displaySmall, 25 | displayMedium = displayMedium, 26 | displayLarge = displayLarge, 27 | labelLarge = labelLarge, 28 | labelMedium = labelMedium, 29 | labelSmall = labelSmall 30 | ) 31 | } 32 | 33 | @OptIn(ExperimentalTextApi::class) 34 | private fun TextStyle.applyLinebreak(): TextStyle = this.copy(lineBreak = LineBreak.Paragraph) 35 | 36 | @OptIn(ExperimentalTextApi::class) 37 | val preferenceTitle = TextStyle( 38 | fontFamily = FontFamily.Default, 39 | fontWeight = FontWeight.Normal, 40 | fontSize = 20.sp, lineHeight = 24.sp, 41 | lineBreak = LineBreak.Paragraph, 42 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/utils/ChromeCustomTabsUtil.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.utils 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import androidx.browser.customtabs.CustomTabsIntent 6 | import com.bobbyesp.spowlo.App 7 | 8 | object ChromeCustomTabsUtil { 9 | 10 | fun openUrl(url: String) { 11 | val builder = CustomTabsIntent.Builder() 12 | val customTabsIntent = builder.build() 13 | customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 14 | customTabsIntent.launchUrl(App.context, Uri.parse(url)) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/utils/DatabaseUtil.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.utils 2 | 3 | import androidx.room.Room 4 | import com.bobbyesp.spowlo.App.Companion.applicationScope 5 | import com.bobbyesp.spowlo.App.Companion.context 6 | import com.bobbyesp.spowlo.database.AppDatabase 7 | import com.bobbyesp.spowlo.database.Backup 8 | import com.bobbyesp.spowlo.database.CommandShortcut 9 | import com.bobbyesp.spowlo.database.CommandTemplate 10 | import com.bobbyesp.spowlo.database.CookieProfile 11 | import com.bobbyesp.spowlo.database.DownloadedSongInfo 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.launch 14 | import kotlinx.serialization.encodeToString 15 | import kotlinx.serialization.json.Json 16 | 17 | object DatabaseUtil { 18 | private val format = Json { prettyPrint = true } 19 | private const val DATABASE_NAME = "app_database" 20 | private val db = Room.databaseBuilder( 21 | context, AppDatabase::class.java, DATABASE_NAME 22 | ).build() 23 | 24 | private val dao = db.songsInfoDao() 25 | fun insertInfo(vararg infoList: DownloadedSongInfo) { 26 | applicationScope.launch(Dispatchers.IO) { 27 | infoList.forEach { dao.insertInfoDistinctByPath(it) } 28 | } 29 | } 30 | 31 | fun getMediaInfo() = dao.getAllMedia() 32 | 33 | fun getTemplateFlow() = dao.getTemplateFlow() 34 | 35 | fun getCookiesFlow() = dao.getCookieProfileFlow() 36 | 37 | fun getShortcuts() = dao.getCommandShortcuts() 38 | 39 | fun clearHistory() = dao.deleteAllMediaFromDb() 40 | 41 | suspend fun deleteShortcut(shortcut: CommandShortcut) = dao.deleteShortcut(shortcut) 42 | suspend fun insertShortcut(shortcut: CommandShortcut) = dao.insertShortcut(shortcut) 43 | 44 | suspend fun getCookieById(id: Int) = dao.getCookieById(id) 45 | suspend fun deleteCookieProfile(profile: CookieProfile) = dao.deleteCookieProfile(profile) 46 | 47 | suspend fun insertCookieProfile(profile: CookieProfile) = dao.insertCookieProfile(profile) 48 | 49 | suspend fun updateCookieProfile(profile: CookieProfile) = dao.updateCookieProfile(profile) 50 | private suspend fun getTemplateList() = dao.getTemplateList() 51 | private suspend fun getShortcutList() = dao.getShortcutList() 52 | suspend fun deleteInfoListByIdList(idList: List, deleteFile: Boolean = false) = 53 | dao.deleteInfoListByIdList(idList, deleteFile) 54 | 55 | suspend fun getInfoById(id: Int): DownloadedSongInfo = dao.getInfoById(id) 56 | suspend fun deleteInfoById(id: Int) = dao.deleteInfoById(id) 57 | 58 | suspend fun insertTemplate(commandTemplate: CommandTemplate) = 59 | dao.insertTemplate(commandTemplate) 60 | 61 | suspend fun updateTemplate(commandTemplate: CommandTemplate) { 62 | dao.updateTemplate(commandTemplate) 63 | } 64 | 65 | suspend fun deleteTemplateById(id: Int) = dao.deleteTemplateById(id) 66 | suspend fun exportTemplatesToJson(): String { 67 | return format.encodeToString( 68 | Backup( 69 | templates = getTemplateList(), shortcuts = getShortcutList() 70 | ) 71 | ) 72 | } 73 | 74 | suspend fun importTemplatesFromJson(json: String): Int { 75 | val templateList = getTemplateList() 76 | val shortcutList = getShortcutList() 77 | var cnt = 0 78 | try { 79 | format.decodeFromString(json).run { 80 | templates.filterNot { 81 | templateList.contains(it) 82 | }.run { 83 | dao.importTemplates(this) 84 | cnt += size 85 | } 86 | dao.insertAllShortcuts(shortcuts.filterNot { 87 | shortcutList.contains(it) 88 | }.apply { cnt += size }) 89 | } 90 | 91 | } catch (e: Exception) { 92 | e.printStackTrace() 93 | } 94 | return cnt 95 | } 96 | 97 | private const val TAG = "DatabaseUtil" 98 | 99 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/spowlo/utils/ListUtil.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo.utils 2 | 3 | import java.util.concurrent.locks.ReentrantReadWriteLock 4 | import kotlin.concurrent.read 5 | import kotlin.concurrent.write 6 | import kotlin.math.min 7 | import kotlin.random.Random 8 | 9 | fun List.subListNonStrict(length: Int, start: Int = 0) = 10 | subList(start, min(start + length, size)) 11 | 12 | fun MutableList.swap(to: Collection) { 13 | with(this) { 14 | clear() 15 | addAll(to) 16 | } 17 | } 18 | 19 | /** 20 | * Returns the second element, or `null` if the list has less than 2 elements. 21 | */ 22 | fun List.secondOrNull(): T? { 23 | return if (isEmpty()) null else this[1] 24 | } 25 | 26 | /** 27 | * Returns the third element, or `null` if the list has less than 3 elements. 28 | */ 29 | fun List.thirdOrNull(): T? { 30 | return if (size < 3) null else this[2] 31 | } 32 | 33 | /** 34 | * Returns all the elements, or `null` if the list has no elements. 35 | */ 36 | fun List.allOrNull(): List? { 37 | return ifEmpty { null } 38 | } 39 | 40 | fun List.randomSubList(length: Int) = List(length) { get(Random.nextInt(size)) } 41 | 42 | fun List.strictEquals(to: List): Boolean { 43 | if (size != to.size) return false 44 | for (i in indices) { 45 | if (get(i) != to[i]) return false 46 | } 47 | return true 48 | } 49 | 50 | fun List.indexOfOrNull(value: T) = indexOfOrNull { it == value } 51 | fun List.indexOfOrNull(predicate: (T) -> Boolean): Int? { 52 | for (i in indices) { 53 | if (predicate(get(i))) return i 54 | } 55 | return null 56 | } 57 | 58 | fun List.distinctList() = distinct().toList() 59 | 60 | fun List.mutate(fn: MutableList.() -> Unit): List { 61 | val out = toMutableList() 62 | fn.invoke(out) 63 | return out.toList() 64 | } 65 | 66 | class ConcurrentList : MutableList { 67 | private val list = mutableListOf() 68 | private val lock = ReentrantReadWriteLock() 69 | 70 | override val size: Int get() = lock.read { list.size } 71 | 72 | override operator fun set(index: Int, element: T) = lock.write { list.set(index, element) } 73 | override operator fun get(index: Int) = lock.read { list[index] } 74 | 75 | override fun contains(element: T) = lock.read { list.contains(element) } 76 | override fun containsAll(elements: Collection) = lock.read { list.containsAll(elements) } 77 | override fun indexOf(element: T) = lock.read { list.indexOf(element) } 78 | override fun lastIndexOf(element: T) = lock.read { list.lastIndexOf(element) } 79 | override fun isEmpty() = lock.read { list.isEmpty() } 80 | override fun subList(fromIndex: Int, toIndex: Int) = 81 | lock.read { list.subList(fromIndex, toIndex) } 82 | 83 | override fun add(element: T) = lock.write { list.add(element) } 84 | override fun add(index: Int, element: T) = lock.write { list.add(index, element) } 85 | override fun addAll(elements: Collection) = lock.write { list.addAll(elements) } 86 | override fun addAll(index: Int, elements: Collection) = 87 | lock.write { list.addAll(index, elements) } 88 | 89 | override fun clear() = lock.write { list.clear() } 90 | override fun remove(element: T) = lock.write { list.remove(element) } 91 | override fun removeAll(elements: Collection) = lock.write { list.removeAll(elements) } 92 | override fun removeAt(index: Int) = lock.write { list.removeAt(index) } 93 | override fun retainAll(elements: Collection) = lock.write { list.retainAll(elements) } 94 | 95 | // NOTE: `write` lock since it returns `MutableIterator`s 96 | override fun iterator() = lock.write { list.iterator() } 97 | override fun listIterator() = lock.write { list.listIterator() } 98 | override fun listIterator(index: Int) = lock.write { list.listIterator(index) } 99 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/github_mark.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | 13 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icons8_youtube.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_cancel_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_content_copy_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sample.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/drawable/sample.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/sample1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/drawable/sample1.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/sample2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/drawable/sample2.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/sample3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/drawable/sample3.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/spotify_logo.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/telegram_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/wolf_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/drawable/wolf_avatar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/youtube_music_icons8.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1ED760 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/xml/locales_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/test/java/com/bobbyesp/spowlo/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.spowlo 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | buildscript { 4 | repositories { 5 | maven { 6 | url = uri("libs/maven-repo") 7 | } 8 | mavenCentral() 9 | google() 10 | } 11 | } 12 | plugins { 13 | alias(libs.plugins.android.application) apply false 14 | alias(libs.plugins.hilt) apply false 15 | alias(libs.plugins.kotlin.gradlePlugin) apply false 16 | alias(libs.plugins.kotlin.serialization) apply false 17 | alias(libs.plugins.ksp) apply false 18 | alias(libs.plugins.compose.compiler) apply false 19 | } 20 | 21 | tasks.register("clean", Delete::class) { 22 | delete(rootProject.layout.buildDirectory) 23 | } -------------------------------------------------------------------------------- /color/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /color/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | alias(libs.plugins.compose.compiler) 5 | } 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_17 8 | targetCompatibility = JavaVersion.VERSION_17 9 | } 10 | kotlin { 11 | jvmToolchain(21) 12 | } 13 | android { 14 | compileSdk = 35 15 | defaultConfig { 16 | minSdk = 21 17 | } 18 | namespace = "com.bobbyesp.spowlo.color" 19 | compileOptions { 20 | sourceCompatibility = JavaVersion.VERSION_17 21 | sourceCompatibility = JavaVersion.VERSION_17 22 | } 23 | buildTypes { 24 | all { 25 | proguardFiles( 26 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 27 | ) 28 | isMinifyEnabled = false 29 | } 30 | } 31 | } 32 | dependencies { 33 | api(platform(libs.androidx.compose.bom)) 34 | api(libs.androidx.compose.ui) 35 | api(libs.androidx.compose.runtime) 36 | api(libs.androidx.core.ktx) 37 | api(libs.androidx.compose.foundation) 38 | api(libs.androidx.compose.material3) 39 | } -------------------------------------------------------------------------------- /color/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class com.kyant.monet.** { *; } 2 | -keep class io.material.hct.** { *; } 3 | -dontwarn java.lang.invoke.StringConcatFactory -------------------------------------------------------------------------------- /color/src/main/java/com/kyant/monet/ColorSpec.kt: -------------------------------------------------------------------------------- 1 | package com.kyant.monet 2 | 3 | data class ColorSpec( 4 | val chroma: (Double) -> Double = { it }, 5 | val hueShift: (Double) -> Double = { 0.0 } 6 | ) 7 | -------------------------------------------------------------------------------- /color/src/main/java/com/kyant/monet/Monet.kt: -------------------------------------------------------------------------------- 1 | package com.kyant.monet 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.ColorScheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.lightColorScheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.staticCompositionLocalOf 9 | import androidx.compose.ui.graphics.Color 10 | import com.kyant.monet.TonalPalettes.Companion.toTonalPalettes 11 | 12 | val LocalTonalPalettes = staticCompositionLocalOf { 13 | Color(0xFF007FAC).toTonalPalettes() 14 | } 15 | 16 | inline val Number.a1: Color 17 | @Composable 18 | get() = LocalTonalPalettes.current accent1 toDouble() 19 | 20 | inline val Number.a2: Color 21 | @Composable get() = LocalTonalPalettes.current accent2 toDouble() 22 | 23 | inline val Number.a3: Color 24 | @Composable get() = LocalTonalPalettes.current accent3 toDouble() 25 | 26 | inline val Number.n1: Color 27 | @Composable get() = LocalTonalPalettes.current neutral1 toDouble() 28 | 29 | inline val Number.n2: Color 30 | @Composable get() = LocalTonalPalettes.current neutral2 toDouble() 31 | 32 | @Composable 33 | fun dynamicColorScheme(isLight: Boolean = !isSystemInDarkTheme()): ColorScheme { 34 | return if (isLight) { 35 | lightColorScheme( 36 | background = 98.n1, 37 | inverseOnSurface = 95.n1, 38 | inversePrimary = 80.a1, 39 | inverseSurface = 20.n1, 40 | onBackground = 10.n1, 41 | onPrimary = 100.a1, 42 | onPrimaryContainer = 10.a1, 43 | onSecondary = 100.a2, 44 | onSecondaryContainer = 10.a2, 45 | onSurface = 10.n1, 46 | onSurfaceVariant = 30.n2, 47 | onTertiary = 100.a3, 48 | onTertiaryContainer = 10.a3, 49 | outline = 50.n2, 50 | outlineVariant = 80.n2, 51 | primary = 40.a1, 52 | primaryContainer = 90.a1, 53 | // scrim = 0.n1, 54 | secondary = 40.a2, 55 | secondaryContainer = 90.a2, 56 | surface = 98.n1, 57 | surfaceVariant = 90.n2, 58 | tertiary = 40.a3, 59 | tertiaryContainer = 90.a3, 60 | surfaceBright = 98.n1, 61 | surfaceDim = 87.n1, 62 | surfaceContainerLowest = 100.n1, 63 | surfaceContainerLow = 96.n1, 64 | surfaceContainer = 94.n1, 65 | surfaceContainerHigh = 92.n1, 66 | surfaceContainerHighest = 90.n1, 67 | ) 68 | } else { 69 | darkColorScheme( 70 | background = 6.n1, 71 | inverseOnSurface = 20.n1, 72 | inversePrimary = 40.a1, 73 | inverseSurface = 90.n1, 74 | onBackground = 90.n1, 75 | onPrimary = 20.a1, 76 | onPrimaryContainer = 90.a1, 77 | onSecondary = 20.a2, 78 | onSecondaryContainer = 90.a2, 79 | onSurface = 90.n1, 80 | onSurfaceVariant = 80.n2, 81 | onTertiary = 20.a3, 82 | onTertiaryContainer = 90.a3, 83 | outline = 60.n2, 84 | outlineVariant = 30.n2, 85 | primary = 80.a1, 86 | primaryContainer = 30.a1, 87 | // scrim = 0.n1, 88 | secondary = 80.a2, 89 | secondaryContainer = 30.a2, 90 | surface = 6.n1, 91 | surfaceVariant = 30.n2, 92 | tertiary = 80.a3, 93 | tertiaryContainer = 30.a3, 94 | surfaceBright = 24.n1, 95 | surfaceDim = 6.n1, 96 | surfaceContainerLowest = 4.n1, 97 | surfaceContainerLow = 10.n1, 98 | surfaceContainer = 12.n1, 99 | surfaceContainerHigh = 17.n1, 100 | surfaceContainerHighest = 22.n1, 101 | ) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /color/src/main/java/com/kyant/monet/TonalPalettes.kt: -------------------------------------------------------------------------------- 1 | package com.kyant.monet 2 | 3 | import androidx.compose.material3.ColorScheme 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.toArgb 6 | import io.material.hct.Hct 7 | 8 | typealias TonalPalette = Map 9 | 10 | class TonalPalettes( 11 | val keyColor: Color, 12 | val style: PaletteStyle = PaletteStyle.TonalSpot, 13 | private val accent1: TonalPalette, 14 | private val accent2: TonalPalette, 15 | private val accent3: TonalPalette, 16 | private val neutral1: TonalPalette, 17 | private val neutral2: TonalPalette 18 | ) { 19 | infix fun accent1(tone: Double): Color = accent1.getOrElse(tone) { 20 | keyColor.transform(tone, style.accent1Spec) 21 | } 22 | 23 | infix fun accent2(tone: Double): Color = accent2.getOrElse(tone) { 24 | keyColor.transform(tone, style.accent2Spec) 25 | } 26 | 27 | infix fun accent3(tone: Double): Color = accent3.getOrElse(tone) { 28 | keyColor.transform(tone, style.accent3Spec) 29 | } 30 | 31 | infix fun neutral1(tone: Double): Color = neutral1.getOrElse(tone) { 32 | keyColor.transform(tone, style.neutral1Spec) 33 | } 34 | 35 | infix fun neutral2(tone: Double): Color = neutral2.getOrElse(tone) { 36 | keyColor.transform(tone, style.neutral2Spec) 37 | } 38 | 39 | companion object { 40 | private val M3TonalValues = doubleArrayOf( 41 | 0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 85.0, 90.0, 95.0, 99.0, 100.0 42 | ) 43 | private val M3SurfaceTonalValues = doubleArrayOf( 44 | 0.0, 45 | 4.0, 46 | 6.0, 47 | 10.0, 48 | 12.0, 49 | 17.0, 50 | 20.0, 51 | 22.0, 52 | 24.0, 53 | 30.0, 54 | 40.0, 55 | 50.0, 56 | 60.0, 57 | 70.0, 58 | 80.0, 59 | 85.0, 60 | 87.0, 61 | 90.0, 62 | 92.0, 63 | 94.0, 64 | 95.0, 65 | 96.0, 66 | 98.0, 67 | 99.0, 68 | 100.0 69 | ) 70 | 71 | fun Color.toTonalPalettes( 72 | style: PaletteStyle = PaletteStyle.TonalSpot, 73 | tonalValues: DoubleArray = M3TonalValues 74 | ): TonalPalettes = TonalPalettes( 75 | keyColor = this, 76 | style = style, 77 | accent1 = tonalValues.associateWith { transform(it, style.accent1Spec) }, 78 | accent2 = tonalValues.associateWith { transform(it, style.accent2Spec) }, 79 | accent3 = tonalValues.associateWith { transform(it, style.accent3Spec) }, 80 | neutral1 = M3SurfaceTonalValues.associateWith { transform(it, style.neutral1Spec) }, 81 | neutral2 = tonalValues.associateWith { transform(it, style.neutral2Spec) } 82 | ) 83 | 84 | 85 | private fun Color.toTonalPalette( 86 | tonalValues: DoubleArray = M3TonalValues 87 | ): TonalPalette = 88 | tonalValues.associateWith { transform(it, ColorSpec()) } 89 | 90 | 91 | /** 92 | * Convert an existing `ColorScheme` to an MD3 `TonalPalettes` 93 | * 94 | * Notice: This function is `PaletteStyle` independent 95 | * 96 | * @see ColorScheme 97 | * @see TonalPalettes 98 | */ 99 | fun ColorScheme.toTonalPalettes( 100 | tonalValues: DoubleArray = M3TonalValues 101 | ): TonalPalettes = TonalPalettes( 102 | keyColor = primary, 103 | accent1 = primary.toTonalPalette(tonalValues), 104 | accent2 = secondary.toTonalPalette(tonalValues), 105 | accent3 = tertiary.toTonalPalette(tonalValues), 106 | neutral1 = surface.toTonalPalette(M3SurfaceTonalValues), 107 | neutral2 = surfaceVariant.toTonalPalette(tonalValues), 108 | ) 109 | 110 | private fun Color.transform(tone: Double, spec: ColorSpec): Color { 111 | return Color(Hct.fromInt(this.toArgb()).apply { 112 | setTone(tone) 113 | setChroma(spec.chroma(this.chroma)) 114 | setHue(spec.hueShift(this.hue) + this.hue) 115 | }.toInt()) 116 | } 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /color/src/main/java/io/material/utils/MathUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // This file is automatically generated. Do not modify it. 17 | package io.material.utils 18 | 19 | import kotlin.math.abs 20 | 21 | /** Utility methods for mathematical operations. */ 22 | object MathUtils { 23 | /** 24 | * The signum function. 25 | * 26 | * @return 1 if num > 0, -1 if num < 0, and 0 if num = 0 27 | */ 28 | fun signum(num: Double): Int { 29 | return if (num < 0) { 30 | -1 31 | } else if (num == 0.0) { 32 | 0 33 | } else { 34 | 1 35 | } 36 | } 37 | 38 | /** 39 | * The linear interpolation function. 40 | * 41 | * @return start if amount = 0 and stop if amount = 1 42 | */ 43 | fun lerp(start: Double, stop: Double, amount: Double): Double { 44 | return (1.0 - amount) * start + amount * stop 45 | } 46 | 47 | /** 48 | * Clamps an integer between two integers. 49 | * 50 | * @return input when min <= input <= max, and either min or max otherwise. 51 | */ 52 | fun clampInt(min: Int, max: Int, input: Int): Int { 53 | if (input < min) { 54 | return min 55 | } else if (input > max) { 56 | return max 57 | } 58 | return input 59 | } 60 | 61 | /** 62 | * Clamps an integer between two floating-point numbers. 63 | * 64 | * @return input when min <= input <= max, and either min or max otherwise. 65 | */ 66 | fun clampDouble(min: Double, max: Double, input: Double): Double { 67 | if (input < min) { 68 | return min 69 | } else if (input > max) { 70 | return max 71 | } 72 | return input 73 | } 74 | 75 | /** 76 | * Sanitizes a degree measure as an integer. 77 | * 78 | * @return a degree measure between 0 (inclusive) and 360 (exclusive). 79 | */ 80 | fun sanitizeDegreesInt(degrees: Int): Int { 81 | var degrees = degrees 82 | degrees %= 360 83 | if (degrees < 0) { 84 | degrees += 360 85 | } 86 | return degrees 87 | } 88 | 89 | /** 90 | * Sanitizes a degree measure as a floating-point number. 91 | * 92 | * @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive). 93 | */ 94 | fun sanitizeDegreesDouble(degrees: Double): Double { 95 | var degrees = degrees 96 | degrees %= 360.0 97 | if (degrees < 0) { 98 | degrees += 360.0 99 | } 100 | return degrees 101 | } 102 | 103 | /** 104 | * Sign of direction change needed to travel from one angle to another. 105 | * 106 | * 107 | * For angles that are 180 degrees apart from each other, both directions have the same travel 108 | * distance, so either direction is shortest. The value 1.0 is returned in this case. 109 | * 110 | * @param from The angle travel starts from, in degrees. 111 | * @param to The angle travel ends at, in degrees. 112 | * @return -1 if decreasing from leads to the shortest travel distance, 1 if increasing from leads 113 | * to the shortest travel distance. 114 | */ 115 | fun rotationDirection(from: Double, to: Double): Double { 116 | val increasingDifference = sanitizeDegreesDouble(to - from) 117 | return if (increasingDifference <= 180.0) 1.0 else -1.0 118 | } 119 | 120 | /** Distance of two points on a circle, represented using degrees. */ 121 | fun differenceDegrees(a: Double, b: Double): Double { 122 | return 180.0 - abs(abs(a - b) - 180.0) 123 | } 124 | 125 | /** Multiplies a 1x3 row vector with a 3x3 matrix. */ 126 | fun matrixMultiply(row: DoubleArray, matrix: Array): DoubleArray { 127 | val a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2] 128 | val b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2] 129 | val c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2] 130 | return doubleArrayOf(a, b, c) 131 | } 132 | } -------------------------------------------------------------------------------- /color/src/main/java/io/material/utils/StringUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.material.utils 17 | 18 | /** Utility methods for string representations of colors. */ 19 | internal object StringUtils { 20 | /** 21 | * Hex string representing color, ex. #ff0000 for red. 22 | * 23 | * @param argb ARGB representation of a color. 24 | */ 25 | fun hexFromArgb(argb: Int): String { 26 | val red = ColorUtils.redFromArgb(argb) 27 | val blue = ColorUtils.blueFromArgb(argb) 28 | val green = ColorUtils.greenFromArgb(argb) 29 | return String.format("#%02x%02x%02x", red, green, blue) 30 | } 31 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Spowlo/0c206665e0a5ba9e84ebeedcd290161e59af0b6a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jan 31 20:28:41 CET 2023 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 | maven { 4 | url = uri("app/libs/maven-repo") 5 | } 6 | gradlePluginPortal() 7 | google() 8 | mavenCentral() 9 | maven ("https://jitpack.io") 10 | } 11 | } 12 | dependencyResolutionManagement { 13 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 14 | repositories { 15 | maven { 16 | url = uri("app/libs/maven-repo") 17 | } 18 | google() 19 | mavenCentral() 20 | maven ("https://jitpack.io") 21 | } 22 | } 23 | rootProject.name = "Spowlo" 24 | include (":app") 25 | include(":color") 26 | --------------------------------------------------------------------------------