├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/appInsightsSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.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 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
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 | [](https://t.me/spowlo_chatroom)
10 | 
11 | 
12 |
13 | 
14 | 
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 |
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 |
5 |
6 |
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 |
--------------------------------------------------------------------------------