├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── acszo │ │ └── redomi │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── acszo │ │ │ └── redomi │ │ │ ├── App.kt │ │ │ ├── ConvertSongActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── UniversalLinkActivity.kt │ │ │ ├── data │ │ │ ├── DataStoreConst.kt │ │ │ └── SettingsDataStore.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ ├── model │ │ │ ├── AppDetails.kt │ │ │ ├── DownloadStatus.kt │ │ │ ├── Platform.kt │ │ │ ├── Providers.kt │ │ │ └── Release.kt │ │ │ ├── repository │ │ │ ├── GithubRepository.kt │ │ │ └── SongLinkRepository.kt │ │ │ ├── service │ │ │ ├── Api.kt │ │ │ ├── GithubService.kt │ │ │ └── SongLinkService.kt │ │ │ ├── ui │ │ │ ├── common │ │ │ │ ├── AnimationSpecs.kt │ │ │ │ ├── Paddings.kt │ │ │ │ └── ScaffoldWithLargeTopBar.kt │ │ │ ├── component │ │ │ │ ├── Buttons.kt │ │ │ │ ├── ClickableItem.kt │ │ │ │ ├── Dialog.kt │ │ │ │ ├── Icons.kt │ │ │ │ └── Modifiers.kt │ │ │ ├── nav │ │ │ │ ├── Pages.kt │ │ │ │ ├── RootNavigation.kt │ │ │ │ └── Route.kt │ │ │ ├── page │ │ │ │ ├── bottom_sheet │ │ │ │ │ ├── ActionsMenu.kt │ │ │ │ │ ├── BottomSheet.kt │ │ │ │ │ ├── BottomSheetError.kt │ │ │ │ │ ├── Lists.kt │ │ │ │ │ └── SongCard.kt │ │ │ │ └── settings │ │ │ │ │ ├── SettingsItem.kt │ │ │ │ │ ├── SettingsPage.kt │ │ │ │ │ ├── apps │ │ │ │ │ ├── AppsCheckBoxList.kt │ │ │ │ │ ├── AppsPage.kt │ │ │ │ │ ├── BottomInfo.kt │ │ │ │ │ └── Tabs.kt │ │ │ │ │ ├── layout │ │ │ │ │ └── LayoutPage.kt │ │ │ │ │ └── update │ │ │ │ │ ├── AnnotatedString.kt │ │ │ │ │ └── UpdatePage.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ ├── utils │ │ │ ├── ClipboardUtils.kt │ │ │ ├── IntentUtil.kt │ │ │ └── UpdateUtil.kt │ │ │ └── viewmodel │ │ │ ├── DataStoreViewModel.kt │ │ │ ├── SongLinkViewModel.kt │ │ │ └── UpdateViewModel.kt │ └── res │ │ ├── color-v31 │ │ ├── m3_ref_palette_dynamic_neutral_variant6.xml │ │ └── m3_ref_palette_dynamic_neutral_variant98.xml │ │ ├── drawable-v24 │ │ ├── ic_amazon_music.webp │ │ ├── ic_apple_music.webp │ │ ├── ic_deezer.webp │ │ ├── ic_napster.webp │ │ ├── ic_soundcloud.webp │ │ ├── ic_spotify.webp │ │ ├── ic_tidal.webp │ │ ├── ic_youtube.webp │ │ └── ic_youtube_music.webp │ │ ├── drawable │ │ ├── anim_ic_not_found.xml │ │ ├── ic_album.xml │ │ ├── ic_anghami.webp │ │ ├── ic_back_arrow.xml │ │ ├── ic_category_filled.xml │ │ ├── ic_category_outline.xml │ │ ├── ic_color_lens_filled.xml │ │ ├── ic_color_lens_outline.xml │ │ ├── ic_description.xml │ │ ├── ic_done_all.xml │ │ ├── ic_format_list_bulleted.xml │ │ ├── ic_github.xml │ │ ├── ic_grid_view.xml │ │ ├── ic_info_filled.xml │ │ ├── ic_info_outline.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_link.xml │ │ ├── ic_menu.xml │ │ ├── ic_music_note.xml │ │ ├── ic_new_releases_inside.xml │ │ ├── ic_new_releases_outline.xml │ │ ├── ic_new_releases_outside.xml │ │ ├── ic_play.xml │ │ ├── ic_refresh.xml │ │ ├── ic_remove_done.xml │ │ ├── ic_settings.xml │ │ ├── ic_share.xml │ │ ├── ic_splash_screen.xml │ │ ├── ic_update.xml │ │ └── ic_yandex.webp │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── resources.properties │ │ ├── values-ar-rSA │ │ └── strings.xml │ │ ├── values-bs-rBA │ │ └── strings.xml │ │ ├── values-cs-rCZ │ │ └── strings.xml │ │ ├── values-de-rDE │ │ └── strings.xml │ │ ├── values-es-rES │ │ └── strings.xml │ │ ├── values-fr-rFR │ │ └── strings.xml │ │ ├── values-hr-rHR │ │ └── strings.xml │ │ ├── values-it-rIT │ │ └── strings.xml │ │ ├── values-ja-rJP │ │ └── strings.xml │ │ ├── values-night-v31 │ │ └── colors.xml │ │ ├── values-night-v34 │ │ └── colors.xml │ │ ├── values-pl-rPL │ │ └── strings.xml │ │ ├── values-pt-rPT │ │ └── strings.xml │ │ ├── values-ru-rRU │ │ └── strings.xml │ │ ├── values-sr-rSP │ │ └── strings.xml │ │ ├── values-tr-rTR │ │ └── strings.xml │ │ ├── values-v29 │ │ └── themes.xml │ │ ├── values-v31 │ │ └── colors.xml │ │ ├── values-v34 │ │ └── colors.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ └── filepaths.xml │ ├── repo │ └── AndroidManifest.xml │ └── test │ └── java │ └── com │ └── acszo │ └── redomi │ └── ExampleUnitTest.kt ├── build.gradle ├── crowdin.yml ├── fastlane └── metadata │ └── android │ └── en-US │ ├── changelogs │ └── 10305.txt │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── screenshot_1.png │ │ ├── screenshot_2.png │ │ ├── screenshot_3.png │ │ ├── screenshot_4.png │ │ └── screenshot_5.png │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── preview ├── logo.svg ├── open.gif ├── screenshot_6.png └── share.gif ├── settings.gradle └── squircle ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml └── java └── com └── acszo └── squircle └── SquircleShape.kt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: acszo 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🕷️ Bug report 2 | description: Create a report to help me fix problems I created 😭🙏 3 | labels: [bug] 4 | body: 5 | - type: textarea 6 | id: descripe-steps-bug 7 | attributes: 8 | label: Describe the bug 9 | description: What did you do when it happened? 10 | placeholder: | 11 | Example: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | id: expected-behavior 21 | attributes: 22 | label: Expected behavior 23 | description: A clear and concise description of what you expected to happen 24 | placeholder: | 25 | Example: 26 | "This should happen..." 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: device-app-info 32 | attributes: 33 | label: Device 34 | description: | 35 | Inside Redomi click on **Version** and past it here 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: screen-shots 41 | attributes: 42 | label: Screenshots 43 | description: | 44 | Add an image or video to help explaining your problem 45 | 46 | - type: textarea 47 | id: logs 48 | attributes: 49 | label: Logs 50 | description: | 51 | Use `adb logcat` or other ways to provide logs 52 | 53 | - type: textarea 54 | id: additional-context 55 | attributes: 56 | label: Additional context 57 | placeholder: | 58 | Add any other context about the problem here -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: ⭐ Feature request 2 | description: Suggest a cool idea you have for the app 😎 3 | labels: [enhancement] 4 | body: 5 | - type: textarea 6 | id: feature-description 7 | attributes: 8 | label: Feature description 9 | description: What feature you want in the app? It can be something new or even improve existing stuff 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | id: why-do-you-want-it 15 | attributes: 16 | label: Why do you want it? 17 | description: Shortly explain why the app or the user could benefit from it 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: additional-context 23 | attributes: 24 | label: Additional context 25 | placeholder: | 26 | Add any other context about the feature here -------------------------------------------------------------------------------- /.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/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 76 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Redomi

4 |

Unofficial song.link client made in Jetpack Compose and Material You

5 | 6 | ![Supported versions](https://img.shields.io/badge/Support-9%2B-green?logo=android) 7 | [![Download count](https://img.shields.io/github/downloads/acszo/Redomi/total?label=Downloads&logo=github)](https://github.com/acszo/Redomi/releases/latest/) 8 | [![Crowdin](https://badges.crowdin.net/redomi/localized.svg)](https://crowdin.com/project/redomi) 9 | 10 | [Get it on GitHub](https://github.com/acszo/Redomi/releases/latest) 11 | [](https://f-droid.org/en/packages/com.acszo.redomi.repo) 12 | [](https://apt.izzysoft.de/fdroid/index/apk/com.acszo.redomi.repo) 13 | 14 |
15 | 16 | ## Description 17 | 18 | Don't you hate when you receive music links from friends who use different platforms than you, and you can't tap and play them in your favourite music service????
19 | This app is here to solve that problem!
20 | And you can also share the songs you want to anyone by converting the link to their preferred platform.

21 | All of this is done like it's integrated into your system! 22 | 23 | ## Features 24 | 25 | - Open supported links from different music services in your favourite streaming app 26 | - Convert links and share songs with others on their preferred platforms 27 | - Customize Sharesheet in different ways: 28 | - Icon shapes 29 | - Choose which apps to display 30 | - Decide between a horizontal or vertical list, and its grid size 31 | - Material You 32 | - Light/Dark Theme 33 | 34 | ## In action 😎 35 | 36 |

37 | 38 | 39 |

40 | 41 | ## Screenshots 42 | 43 |

44 | 45 | 46 | 47 | 48 | 49 | 50 |

51 | 52 | ## HOW TO MAKE IT WORK (on Android 12+) 53 | 54 | By default Redomi is unable to open links, do as follows: 55 | 1. Long-press on Redomi and open its info page 56 | 2. Navigate to "Set/Open by default" (this may have a different naming on some devices) 57 | 3. Check all the music platforms that are *NOT* installed 58 | 59 | ## Supported services (+ 3rd party apps) 60 | 61 | - Youtube music 62 | - Youtube 63 | - Spotify 64 | - Deezer 65 | - Tidal 66 | - Amazon Music 67 | - Apple Music 68 | - Soundcloud 69 | - Napster 70 | - Yandex Music 71 | - Anghami 72 | 73 | 3rd party clients are also compatible, as long as they're the default app to handle the corresponding link 74 | 75 | ## Translations 76 | 77 | You can contribute by translating via [Crowdin](https://crowdin.com/project/redomi) 78 | 79 | ## Credits 80 | 81 | Obviously [Song.link](https://song.link/) or else it would have not been possible, and also [Ty3uK/songlink-android](https://github.com/Ty3uK/songlink-android) for giving me inspiration 82 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class com.acszo.redomi.model.Providers { *; } 2 | -keep class com.acszo.redomi.model.Link { *; } 3 | -keep class com.acszo.redomi.model.Song { *; } 4 | -keep class com.acszo.redomi.model.Release { *; } 5 | -keep class com.acszo.redomi.model.Asset { *; } 6 | 7 | -repackageclasses -------------------------------------------------------------------------------- /app/src/androidTest/java/com/acszo/redomi/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.acszo.redomi", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 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 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 80 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/App.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App: Application() 8 | 9 | val isGithubBuild: Boolean get() = BuildConfig.FLAVOR == "github" 10 | 11 | val versionName: String get() = BuildConfig.VERSION_NAME -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ConvertSongActivity.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.runtime.getValue 9 | import androidx.hilt.navigation.compose.hiltViewModel 10 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 11 | import com.acszo.redomi.data.AppList 12 | import com.acszo.redomi.data.IconShape 13 | import com.acszo.redomi.data.ListOrientation 14 | import com.acszo.redomi.ui.page.bottom_sheet.BottomSheet 15 | import com.acszo.redomi.ui.theme.RedomiTheme 16 | import com.acszo.redomi.viewmodel.DataStoreViewModel 17 | import com.acszo.redomi.viewmodel.SongLinkViewModel 18 | import com.acszo.redomi.viewmodel.UpdateViewModel 19 | import dagger.hilt.android.AndroidEntryPoint 20 | 21 | /* 22 | * ACTION_SEND -> shows sharing apps -> shows action menu 23 | * ACTION_VIEW -> shows opening apps -> opens app 24 | * */ 25 | @AndroidEntryPoint 26 | class ConvertSongActivity: ComponentActivity() { 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | 31 | setContent { 32 | val isActionSend = (intent.action == Intent.ACTION_SEND) 33 | 34 | val intentData = intent?.let { 35 | if (isActionSend) it.getStringExtra(Intent.EXTRA_TEXT) else it.data 36 | }.toString() 37 | 38 | val appList = if (isActionSend) AppList.SHARING else AppList.OPENING 39 | 40 | val songLinkViewModel: SongLinkViewModel = hiltViewModel() 41 | val updateViewModel: UpdateViewModel = hiltViewModel() 42 | val dataStoreViewModel: DataStoreViewModel = hiltViewModel() 43 | 44 | LaunchedEffect(Unit) { 45 | songLinkViewModel.getPlatformsLink(intentData, appList.key) 46 | if (isGithubBuild) updateViewModel.checkUpdate(versionName) 47 | } 48 | 49 | val bottomSheetUiState by songLinkViewModel.bottomSheetUiState.collectAsStateWithLifecycle() 50 | val isUpdateAvailable by updateViewModel.isUpdateAvailable.collectAsStateWithLifecycle() 51 | val theme by dataStoreViewModel.themeMode.collectAsStateWithLifecycle() 52 | val iconShape by dataStoreViewModel.iconShape.collectAsStateWithLifecycle() 53 | val listOrientation by dataStoreViewModel.listOrientation.collectAsStateWithLifecycle() 54 | val gridSize by dataStoreViewModel.gridSize.collectAsStateWithLifecycle() 55 | 56 | RedomiTheme( 57 | theme = theme 58 | ) { 59 | BottomSheet( 60 | onDismiss = { this.finish() }, 61 | uiState = bottomSheetUiState, 62 | isActionSend = isActionSend, 63 | isUpdateAvailable = isUpdateAvailable, 64 | iconShape = IconShape.entries[iconShape].shape, 65 | listOrientation = ListOrientation.entries[listOrientation], 66 | gridSize = gridSize, 67 | refresh = { songLinkViewModel.getPlatformsLink(intentData, appList.key) } 68 | ) 69 | } 70 | } 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.runtime.LaunchedEffect 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.rememberCoroutineScope 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 11 | import androidx.core.view.WindowCompat 12 | import androidx.hilt.navigation.compose.hiltViewModel 13 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 14 | import com.acszo.redomi.ui.nav.RootNavigation 15 | import com.acszo.redomi.ui.theme.RedomiTheme 16 | import com.acszo.redomi.utils.UpdateUtil.deleteApk 17 | import com.acszo.redomi.viewmodel.DataStoreViewModel 18 | import com.acszo.redomi.viewmodel.UpdateViewModel 19 | import dagger.hilt.android.AndroidEntryPoint 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.launch 22 | 23 | @AndroidEntryPoint 24 | class MainActivity : ComponentActivity() { 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | 29 | installSplashScreen() 30 | WindowCompat.setDecorFitsSystemWindows(window, false) 31 | 32 | setContent { 33 | val context = LocalContext.current 34 | val scope = rememberCoroutineScope() 35 | 36 | val dataStoreViewModel: DataStoreViewModel = hiltViewModel() 37 | val updateViewModel: UpdateViewModel = hiltViewModel() 38 | 39 | LaunchedEffect(Unit) { 40 | dataStoreViewModel.getIsFirstTime() 41 | if (isGithubBuild) { 42 | updateViewModel.checkUpdate(versionName) 43 | scope.launch(Dispatchers.IO) { 44 | deleteApk(context) 45 | } 46 | } 47 | } 48 | 49 | val isUpdateAvailable by updateViewModel.isUpdateAvailable.collectAsStateWithLifecycle() 50 | val theme by dataStoreViewModel.themeMode.collectAsStateWithLifecycle() 51 | 52 | RedomiTheme( 53 | theme = theme 54 | ) { 55 | RootNavigation( 56 | dataStoreViewModel = dataStoreViewModel, 57 | updateViewModel = updateViewModel, 58 | isUpdateAvailable = isUpdateAvailable, 59 | ) 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/UniversalLinkActivity.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import com.acszo.redomi.utils.IntentUtil 7 | 8 | class UniversalLinkActivity: ComponentActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | val universalLink = "https://song.link/${intent?.getStringExtra(Intent.EXTRA_TEXT)}" 14 | 15 | IntentUtil.onIntentSend(this@UniversalLinkActivity, universalLink) 16 | this.finish() 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/data/DataStoreConst.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.data 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.foundation.shape.CircleShape 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.Shape 9 | import com.acszo.redomi.R 10 | import com.acszo.redomi.data.DataStoreConst.OPENING_APPS 11 | import com.acszo.redomi.data.DataStoreConst.SHARING_APPS 12 | import com.acszo.squircle.SquircleShape 13 | 14 | object DataStoreConst { 15 | 16 | const val OPENING_APPS = "opening_apps" 17 | const val SHARING_APPS = "sharing_apps" 18 | const val FIRST_TIME = "first_time" 19 | const val LIST_ORIENTATION = "list_orientation" 20 | const val GRID_SIZE = "grid_size" 21 | const val ICON_SHAPE = "icon_shape" 22 | const val THEME_MODE = "theme_mode" 23 | 24 | const val SMALL_GRID = 2 25 | const val MEDIUM_GRID = 3 26 | const val BIG_GRID = 4 27 | 28 | } 29 | 30 | interface Resource { 31 | @get:StringRes val toRes: Int 32 | } 33 | 34 | enum class AppList(val key: String): Resource { 35 | OPENING(OPENING_APPS), 36 | SHARING(SHARING_APPS); 37 | 38 | override val toRes: Int 39 | get() = when (this) { 40 | OPENING -> R.string.opening 41 | SHARING -> R.string.sharing 42 | } 43 | } 44 | 45 | enum class ListOrientation: Resource { 46 | HORIZONTAL, 47 | VERTICAL; 48 | 49 | override val toRes: Int 50 | get() = when (this) { 51 | HORIZONTAL -> R.string.horizontal 52 | VERTICAL -> R.string.vertical 53 | } 54 | } 55 | 56 | enum class IconShape(val shape: Shape): Resource { 57 | SQUIRCLE(SquircleShape), // 👍👍👍 58 | CIRCLE(CircleShape), // 👍👍 59 | SQUARE(RoundedCornerShape(25)); // 👎 60 | 61 | override val toRes: Int 62 | get() = when (this) { 63 | SQUIRCLE -> R.string.squircle_icon 64 | CIRCLE -> R.string.circle_icon 65 | SQUARE -> R.string.square_icon 66 | } 67 | } 68 | 69 | enum class Theme: Resource { 70 | SYSTEM_THEME, 71 | DARK_THEME, 72 | LIGHT_THEME; 73 | 74 | @Composable 75 | fun mode(): Boolean 76 | = when (this) { 77 | SYSTEM_THEME -> isSystemInDarkTheme() 78 | DARK_THEME -> true 79 | LIGHT_THEME -> false 80 | } 81 | 82 | override val toRes: Int 83 | get() = when (this) { 84 | SYSTEM_THEME -> R.string.system_theme 85 | DARK_THEME -> R.string.dark_theme 86 | LIGHT_THEME -> R.string.light_theme 87 | } 88 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/data/SettingsDataStore.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.data 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.booleanPreferencesKey 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.core.intPreferencesKey 9 | import androidx.datastore.preferences.core.stringSetPreferencesKey 10 | import androidx.datastore.preferences.preferencesDataStore 11 | import com.acszo.redomi.model.Platform.platforms 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.map 14 | 15 | class SettingsDataStore( 16 | private val context: Context 17 | ) { 18 | 19 | companion object { 20 | private val Context.dataStore: DataStore by preferencesDataStore("SettingsDataStore") 21 | } 22 | 23 | fun getBoolean(key: String): Flow { 24 | return context.dataStore.data 25 | .map { preferences -> 26 | preferences[booleanPreferencesKey(key)] 27 | } 28 | } 29 | 30 | suspend fun setBoolean(key: String, value: Boolean) { 31 | context.dataStore.edit { preferences -> 32 | preferences[booleanPreferencesKey(key)] = value 33 | } 34 | } 35 | 36 | fun getInt(key: String): Flow { 37 | return context.dataStore.data 38 | .map { preferences -> 39 | preferences[intPreferencesKey(key)] 40 | } 41 | } 42 | 43 | suspend fun setInt(key: String, value: Int) { 44 | context.dataStore.edit { preferences -> 45 | preferences[intPreferencesKey(key)] = value 46 | } 47 | } 48 | 49 | fun getSetOfStrings(key: String): Flow> { 50 | return context.dataStore.data 51 | .map { preferences -> 52 | preferences[stringSetPreferencesKey(key)] ?: platforms.keys 53 | } 54 | } 55 | 56 | suspend fun setSetOfStrings(key: String, value: Set) { 57 | context.dataStore.edit { preferences -> 58 | preferences[stringSetPreferencesKey(key)] = value 59 | } 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.di 2 | 3 | import android.content.Context 4 | import com.acszo.redomi.data.SettingsDataStore 5 | import com.acszo.redomi.repository.GithubRepository 6 | import com.acszo.redomi.repository.SongLinkRepository 7 | import com.acszo.redomi.service.Api.BASE_URL_GITHUB 8 | import com.acszo.redomi.service.Api.BASE_URL_SONG_LINK 9 | import com.acszo.redomi.service.GithubService 10 | import com.acszo.redomi.service.SongLinkService 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.android.qualifiers.ApplicationContext 15 | import dagger.hilt.components.SingletonComponent 16 | import kotlinx.serialization.json.Json 17 | import okhttp3.MediaType.Companion.toMediaType 18 | import retrofit2.Converter 19 | import retrofit2.Retrofit 20 | import retrofit2.converter.kotlinx.serialization.asConverterFactory 21 | import javax.inject.Singleton 22 | 23 | @Module 24 | @InstallIn(SingletonComponent::class) 25 | object AppModule { 26 | 27 | @Provides 28 | @Singleton 29 | fun jsonConverter(): Converter.Factory { 30 | val contentType = "application/json".toMediaType() 31 | val json = Json { ignoreUnknownKeys = true } 32 | return json.asConverterFactory(contentType) 33 | } 34 | 35 | @Provides 36 | @Singleton 37 | fun provideSongLinkService( 38 | jsonConverter: Converter.Factory 39 | ): SongLinkService = Retrofit.Builder() 40 | .baseUrl(BASE_URL_SONG_LINK) 41 | .addConverterFactory(jsonConverter) 42 | .build() 43 | .create(SongLinkService::class.java) 44 | 45 | @Provides 46 | @Singleton 47 | fun provideGithubService( 48 | jsonConverter: Converter.Factory 49 | ): GithubService = Retrofit.Builder() 50 | .baseUrl(BASE_URL_GITHUB) 51 | .addConverterFactory(jsonConverter) 52 | .build() 53 | .create(GithubService::class.java) 54 | 55 | @Provides 56 | @Singleton 57 | fun provideSongRepository( 58 | songLinkService: SongLinkService 59 | ): SongLinkRepository = SongLinkRepository(songLinkService) 60 | 61 | @Provides 62 | @Singleton 63 | fun provideGithubRepository( 64 | githubService: GithubService 65 | ): GithubRepository = GithubRepository(githubService) 66 | 67 | @Provides 68 | @Singleton 69 | fun provideSettingsDataStore( 70 | @ApplicationContext context: Context 71 | ): SettingsDataStore = SettingsDataStore(context) 72 | 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/model/AppDetails.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.model 2 | 3 | import androidx.annotation.DrawableRes 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class AppDetails( 8 | val title: String, 9 | @DrawableRes val icon: Int, 10 | val searchUrl: String 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/model/DownloadStatus.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.model 2 | 3 | sealed class DownloadStatus { 4 | 5 | data class Downloading(val progress: Int): DownloadStatus() 6 | data object Finished: DownloadStatus() 7 | 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/model/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.model 2 | 3 | import com.acszo.redomi.R 4 | import com.acszo.redomi.service.Api.SEARCH_QUERY_YT_MUSIC 5 | import com.acszo.redomi.service.Api.SEARCH_QUERY_YOUTUBE 6 | import com.acszo.redomi.service.Api.SEARCH_QUERY_SPOTIFY 7 | import com.acszo.redomi.service.Api.SEARCH_QUERY_DEEZER 8 | import com.acszo.redomi.service.Api.SEARCH_QUERY_TIDAL 9 | import com.acszo.redomi.service.Api.SEARCH_QUERY_AMAZON_MUSIC 10 | import com.acszo.redomi.service.Api.SEARCH_QUERY_APPLE_MUSIC 11 | import com.acszo.redomi.service.Api.SEARCH_QUERY_SOUNDCLOUD 12 | import com.acszo.redomi.service.Api.SEARCH_QUERY_NAPSTER 13 | import com.acszo.redomi.service.Api.SEARCH_QUERY_YANDEX 14 | import com.acszo.redomi.service.Api.SEARCH_QUERY_ANGHAMI 15 | 16 | object Platform { 17 | 18 | val platforms = mapOf( 19 | "youtubeMusic" to AppDetails("Youtube Music", R.drawable.ic_youtube_music, SEARCH_QUERY_YT_MUSIC), 20 | "youtube" to AppDetails("Youtube", R.drawable.ic_youtube, SEARCH_QUERY_YOUTUBE), 21 | "spotify" to AppDetails("Spotify", R.drawable.ic_spotify, SEARCH_QUERY_SPOTIFY), 22 | "deezer" to AppDetails("Deezer", R.drawable.ic_deezer, SEARCH_QUERY_DEEZER), 23 | "tidal" to AppDetails("Tidal", R.drawable.ic_tidal, SEARCH_QUERY_TIDAL), 24 | "amazonMusic" to AppDetails("Amazon Music", R.drawable.ic_amazon_music, SEARCH_QUERY_AMAZON_MUSIC), 25 | "appleMusic" to AppDetails("Apple Music", R.drawable.ic_apple_music, SEARCH_QUERY_APPLE_MUSIC), 26 | "soundcloud" to AppDetails("Soundcloud", R.drawable.ic_soundcloud, SEARCH_QUERY_SOUNDCLOUD), 27 | "napster" to AppDetails("Napster", R.drawable.ic_napster, SEARCH_QUERY_NAPSTER), 28 | "yandex" to AppDetails("Yandex Music", R.drawable.ic_yandex, SEARCH_QUERY_YANDEX), 29 | "anghami" to AppDetails("Anghami", R.drawable.ic_anghami, SEARCH_QUERY_ANGHAMI) 30 | ) 31 | 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/model/Providers.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Providers( 7 | val entityUniqueId: String, 8 | val linksByPlatform: Map, 9 | val entitiesByUniqueId: Map 10 | ) 11 | 12 | @Serializable 13 | data class Link( 14 | val url: String, 15 | val entityUniqueId: String 16 | ) 17 | 18 | @Serializable 19 | data class Song( 20 | val isMatched: Boolean = true, 21 | val platform: String = "", 22 | val link: String = "", 23 | val type: String?, 24 | val title: String?, 25 | val artistName: String?, 26 | val thumbnailUrl: String? 27 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/model/Release.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Release( 8 | val assets: List, 9 | val body: String, 10 | val name: String, 11 | @SerialName("tag_name") 12 | val tagName: String, 13 | ) 14 | 15 | @Serializable 16 | data class Asset( 17 | @SerialName("browser_download_url") 18 | val browserDownloadUrl: String, 19 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/repository/GithubRepository.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.repository 2 | 3 | import com.acszo.redomi.model.Release 4 | import com.acszo.redomi.service.ApiResult 5 | import com.acszo.redomi.service.GithubService 6 | import com.acszo.redomi.service.apiCall 7 | import okhttp3.ResponseBody 8 | 9 | class GithubRepository( 10 | private val githubService: GithubService 11 | ) { 12 | 13 | suspend fun getLatest(): ApiResult = apiCall { githubService.getLatest() } 14 | 15 | suspend fun getLatestApk(assetUrl: String): ResponseBody = githubService.getLatestApk(assetUrl) 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/repository/SongLinkRepository.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.repository 2 | 3 | import com.acszo.redomi.model.Providers 4 | import com.acszo.redomi.service.ApiResult 5 | import com.acszo.redomi.service.SongLinkService 6 | import com.acszo.redomi.service.apiCall 7 | 8 | class SongLinkRepository( 9 | private val songLinkService: SongLinkService 10 | ) { 11 | 12 | suspend fun getSongs(url: String): ApiResult = apiCall { songLinkService.getSongs(url) } 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/service/Api.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.service 2 | 3 | import com.acszo.redomi.R 4 | import okio.IOException 5 | import retrofit2.Response 6 | import java.net.SocketTimeoutException 7 | 8 | object Api { 9 | 10 | const val BASE_URL_SONG_LINK = "https://api.song.link/v1-alpha.1/" 11 | const val BASE_URL_GITHUB = "https://api.github.com/repos/acszo/Redomi/" 12 | 13 | const val SEARCH_QUERY_YT_MUSIC = "https://music.youtube.com/search?q=" 14 | const val SEARCH_QUERY_YOUTUBE = "https://www.youtube.com/results?search_query=" 15 | const val SEARCH_QUERY_SPOTIFY = "https://open.spotify.com/search/results/" 16 | const val SEARCH_QUERY_DEEZER = "https://www.deezer.com/search/" 17 | const val SEARCH_QUERY_TIDAL = "https://listen.tidal.com/search?q=" 18 | const val SEARCH_QUERY_AMAZON_MUSIC = "https://music.amazon.com/search/" 19 | const val SEARCH_QUERY_APPLE_MUSIC = "https://music.apple.com/search?term=" 20 | const val SEARCH_QUERY_SOUNDCLOUD = "https://soundcloud.com/search?q=" 21 | const val SEARCH_QUERY_NAPSTER = "https://napster.com/search?query=" 22 | const val SEARCH_QUERY_YANDEX = "https://music.yandex.ru/search?text=" 23 | const val SEARCH_QUERY_ANGHAMI = "https://play.anghami.com/search/" 24 | 25 | } 26 | 27 | sealed class ApiResult { 28 | data class Success(val data: T): ApiResult() 29 | data class Error(val code: Int): ApiResult() 30 | data class Exception(val message: Int): ApiResult() 31 | } 32 | 33 | suspend fun apiCall( 34 | execute: suspend() -> Response 35 | ): ApiResult { 36 | return try { 37 | val response = execute() 38 | val body = response.body() 39 | if (response.isSuccessful && body != null) { 40 | ApiResult.Success(body) 41 | } else { 42 | ApiResult.Error(response.code()) 43 | } 44 | } catch(_: SocketTimeoutException) { 45 | ApiResult.Exception(R.string.error_timeout) 46 | } catch (_: IOException) { 47 | ApiResult.Exception(R.string.error_no_internet_connection) 48 | } catch (_: Exception) { 49 | ApiResult.Exception(R.string.error_generic) 50 | } as ApiResult 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/service/GithubService.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.service 2 | 3 | import com.acszo.redomi.model.Release 4 | import okhttp3.ResponseBody 5 | import retrofit2.Response 6 | import retrofit2.http.GET 7 | import retrofit2.http.Streaming 8 | import retrofit2.http.Url 9 | 10 | interface GithubService { 11 | 12 | @GET("releases/latest") 13 | suspend fun getLatest(): Response 14 | 15 | @GET 16 | @Streaming 17 | suspend fun getLatestApk(@Url assetUrl: String): ResponseBody 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/service/SongLinkService.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.service 2 | 3 | import com.acszo.redomi.model.Providers 4 | import retrofit2.Response 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | 8 | interface SongLinkService { 9 | 10 | @GET("links") 11 | suspend fun getSongs(@Query("url") url: String): Response 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/common/AnimationSpecs.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.common 2 | 3 | import androidx.compose.animation.EnterTransition 4 | import androidx.compose.animation.ExitTransition 5 | import androidx.compose.animation.core.FastOutLinearInEasing 6 | import androidx.compose.animation.core.LinearEasing 7 | import androidx.compose.animation.core.LinearOutSlowInEasing 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.animation.fadeIn 10 | import androidx.compose.animation.fadeOut 11 | import androidx.compose.animation.slideInHorizontally 12 | import androidx.compose.animation.slideInVertically 13 | import androidx.compose.animation.slideOutHorizontally 14 | import androidx.compose.animation.slideOutVertically 15 | 16 | const val initialOffset = 0.10f 17 | 18 | enum class TransitionDirection(val value: Int) { 19 | FORWARD(1), 20 | BACKWARD(-1) 21 | } 22 | 23 | fun enterHorizontalTransition( 24 | direction: TransitionDirection 25 | ): EnterTransition = fadeIn( 26 | animationSpec = tween( 27 | durationMillis = 210, 28 | delayMillis = 90, 29 | easing = LinearOutSlowInEasing 30 | ) 31 | ) + slideInHorizontally( 32 | animationSpec = tween(durationMillis = 300) 33 | ) { 34 | direction.value * (it * initialOffset).toInt() 35 | } 36 | 37 | 38 | fun exitHorizontalTransition( 39 | direction: TransitionDirection 40 | ): ExitTransition = fadeOut( 41 | animationSpec = tween( 42 | durationMillis = 90, 43 | easing = FastOutLinearInEasing 44 | ) 45 | ) + slideOutHorizontally( 46 | animationSpec = tween(durationMillis = 300) 47 | ) { 48 | direction.value * (it * initialOffset).toInt() 49 | } 50 | 51 | fun enterVerticalTransition(): EnterTransition = 52 | slideInVertically(initialOffsetY = { -40 }) + fadeIn(initialAlpha = 0.3f) 53 | 54 | fun exitVerticalTransition(): ExitTransition = 55 | slideOutVertically(targetOffsetY = { -40 }) + fadeOut(animationSpec = tween(200)) 56 | 57 | fun enterFadeInTransition(): EnterTransition = 58 | fadeIn( 59 | animationSpec = tween( 60 | durationMillis = 200, 61 | easing = LinearEasing 62 | ) 63 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/common/Paddings.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.common 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.WindowInsets 5 | import androidx.compose.foundation.layout.asPaddingValues 6 | import androidx.compose.foundation.layout.calculateEndPadding 7 | import androidx.compose.foundation.layout.calculateStartPadding 8 | import androidx.compose.foundation.layout.navigationBars 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.platform.LocalLayoutDirection 11 | import androidx.compose.ui.unit.Dp 12 | import androidx.compose.ui.unit.dp 13 | 14 | fun PaddingValues.addHorizontalPadding( 15 | padding: Dp 16 | ): PaddingValues = PaddingValues( 17 | start = padding, 18 | top = this.calculateTopPadding(), 19 | end = padding, 20 | bottom = this.calculateBottomPadding() 21 | ) 22 | 23 | @Composable 24 | fun PaddingValues.addNavigationBarsPadding(): PaddingValues { 25 | val layoutDirection = LocalLayoutDirection.current 26 | return PaddingValues( 27 | start = this.calculateStartPadding(layoutDirection), 28 | top = this.calculateTopPadding(), 29 | end = this.calculateEndPadding(layoutDirection), 30 | bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() 31 | ) 32 | } 33 | 34 | @Composable 35 | fun PaddingValues.removeTopPadding(): PaddingValues { 36 | val layoutDirection = LocalLayoutDirection.current 37 | return PaddingValues( 38 | start = this.calculateStartPadding(layoutDirection), 39 | top = 0.dp, 40 | end = this.calculateEndPadding(layoutDirection), 41 | bottom = this.calculateBottomPadding() 42 | ) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/common/ScaffoldWithLargeTopBar.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.common 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.WindowInsets 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.material3.TopAppBarScrollBehavior 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.graphicsLayer 17 | import androidx.compose.ui.input.nestedscroll.nestedScroll 18 | import androidx.compose.ui.unit.dp 19 | 20 | @OptIn(ExperimentalMaterial3Api::class) 21 | @Composable 22 | fun ScaffoldWithLargeTopAppBar( 23 | title: String, 24 | description: String? = null, 25 | backButton: @Composable () -> Unit = {}, 26 | content: @Composable (PaddingValues, @Composable () -> Unit) -> Unit 27 | ) { 28 | val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() 29 | 30 | Scaffold( 31 | modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), 32 | topBar = { 33 | SmallTopAppBar( 34 | title = title, 35 | scrollBehavior = scrollBehavior, 36 | navigationIcon = { backButton() } 37 | ) 38 | }, 39 | contentWindowInsets = WindowInsets(0, 0, 0, 0), 40 | ) { 41 | content(it.addHorizontalPadding(28.dp).addNavigationBarsPadding()) { 42 | Column { 43 | PageTitle( 44 | title = title, 45 | scrollBehavior = scrollBehavior 46 | ) 47 | if (description != null) { 48 | PageDescription(description = description) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | @OptIn(ExperimentalMaterial3Api::class) 56 | @Composable 57 | fun SmallTopAppBar( 58 | title: String, 59 | scrollBehavior: TopAppBarScrollBehavior, 60 | navigationIcon: @Composable () -> Unit = {} 61 | ) { 62 | TopAppBar( 63 | title = { 64 | Text( 65 | text = title, 66 | modifier = Modifier.graphicsLayer { alpha = scrollBehavior.state.overlappedFraction }, 67 | color = MaterialTheme.colorScheme.onSurface, 68 | ) 69 | }, 70 | scrollBehavior = scrollBehavior, 71 | navigationIcon = { navigationIcon() } 72 | ) 73 | } 74 | 75 | @OptIn(ExperimentalMaterial3Api::class) 76 | @Composable 77 | fun PageTitle( 78 | title: String, 79 | scrollBehavior: TopAppBarScrollBehavior, 80 | ) { 81 | Text( 82 | text = title, 83 | modifier = Modifier 84 | .padding(top = 18.dp) 85 | .padding(vertical = 28.dp) 86 | .graphicsLayer { alpha = (1f - scrollBehavior.state.overlappedFraction) }, 87 | color = MaterialTheme.colorScheme.onSurface, 88 | style = MaterialTheme.typography.displaySmall, 89 | ) 90 | } 91 | 92 | @Composable 93 | fun PageDescription( 94 | description: String 95 | ) { 96 | Text( 97 | text = description, 98 | color = MaterialTheme.colorScheme.onSurfaceVariant 99 | ) 100 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/component/ClickableItem.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.component 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.fillMaxHeight 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun ClickableItem( 15 | modifier: Modifier = Modifier, 16 | content: @Composable ColumnScope.() -> Unit, 17 | ) { 18 | Column( 19 | modifier = modifier 20 | .fillMaxHeight() 21 | .padding(vertical = 10.dp, horizontal = 5.dp), 22 | horizontalAlignment = Alignment.CenterHorizontally, 23 | verticalArrangement = Arrangement.SpaceAround, 24 | content = content 25 | ) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/component/Dialog.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.component 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.material3.AlertDialog 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.material3.TextButton 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.Dp 18 | import androidx.compose.ui.unit.dp 19 | 20 | @Composable 21 | fun DefaultDialog( 22 | @DrawableRes icon: Int, 23 | title: String, 24 | verticalSpaceBy: Dp = 0.dp, 25 | content: @Composable ColumnScope.() -> Unit, 26 | onDismissRequest: () -> Unit = {}, 27 | onConfirmAction: () -> Unit, 28 | ) { 29 | AlertDialog( 30 | icon = { 31 | Icon( 32 | painter = painterResource(id = icon), 33 | tint = MaterialTheme.colorScheme.secondary, 34 | contentDescription = null 35 | ) 36 | }, 37 | title = { Text(text = title) }, 38 | text = { 39 | Column( 40 | verticalArrangement = Arrangement.spacedBy(verticalSpaceBy) 41 | ) { 42 | content() 43 | } 44 | }, 45 | onDismissRequest = onDismissRequest, 46 | confirmButton = { 47 | TextButton( 48 | onClick = onConfirmAction, 49 | ) { 50 | Text(text = stringResource(id = android.R.string.ok)) 51 | } 52 | } 53 | ) 54 | } 55 | 56 | @Composable 57 | fun IconDescription( 58 | @DrawableRes icon: Int, 59 | description: String, 60 | ) { 61 | Row( 62 | verticalAlignment = Alignment.CenterVertically, 63 | horizontalArrangement = Arrangement.spacedBy(16.dp) 64 | ) { 65 | Icon( 66 | painter = painterResource(id = icon), 67 | tint = MaterialTheme.colorScheme.secondary, 68 | contentDescription = null 69 | ) 70 | Text(text = description) 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/component/Icons.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.component 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.animation.core.Animatable 5 | import androidx.compose.animation.core.LinearOutSlowInEasing 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.layout.offset 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.graphicsLayer 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.unit.Dp 20 | import androidx.compose.ui.unit.dp 21 | import com.acszo.redomi.R 22 | 23 | @Composable 24 | fun RotatingIcon( 25 | @DrawableRes icon: Int, 26 | modifier: Modifier = Modifier, 27 | size: Dp, 28 | tint: Color = MaterialTheme.colorScheme.secondary, 29 | startValue: Float, 30 | contentDescription: String? = null 31 | ) { 32 | val rotation = remember { Animatable(startValue) } 33 | 34 | LaunchedEffect(Unit) { 35 | rotation.animateTo( 36 | targetValue = 180f, 37 | animationSpec = tween(300, easing = LinearOutSlowInEasing), 38 | ) 39 | } 40 | 41 | Icon( 42 | painter = painterResource(id = icon), 43 | modifier = modifier 44 | .size(size) 45 | .graphicsLayer { rotationZ = rotation.value }, 46 | tint = tint, 47 | contentDescription = contentDescription 48 | ) 49 | } 50 | 51 | @Composable 52 | fun NewReleaseIcon() { 53 | val context = LocalContext.current 54 | val display = context.resources.displayMetrics 55 | val width = display.widthPixels.dp / display.density 56 | val height = display.heightPixels.dp / display.density 57 | val widthOffset = width / 1.5f 58 | val heightOffset = height / 2.2f 59 | 60 | RotatingIcon( 61 | icon = R.drawable.ic_new_releases_outside, 62 | modifier = Modifier.offset(width - widthOffset, height - heightOffset), 63 | size = width, 64 | tint = MaterialTheme.colorScheme.secondaryContainer, 65 | startValue = 90f, 66 | ) 67 | 68 | Icon( 69 | painter = painterResource(id = R.drawable.ic_new_releases_inside), 70 | modifier = Modifier 71 | .size(width) 72 | .offset(width - widthOffset, height - heightOffset), 73 | tint = MaterialTheme.colorScheme.secondaryContainer, 74 | contentDescription = null, 75 | ) 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/component/Modifiers.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.component 2 | 3 | import androidx.compose.foundation.layout.requiredWidth 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.draw.drawWithContent 7 | import androidx.compose.ui.graphics.BlendMode 8 | import androidx.compose.ui.graphics.Brush 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.CompositingStrategy 11 | import androidx.compose.ui.graphics.graphicsLayer 12 | import androidx.compose.ui.platform.LocalConfiguration 13 | import androidx.compose.ui.unit.dp 14 | 15 | fun Modifier.fadingEdge() = this 16 | .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) 17 | .drawWithContent { 18 | drawContent() 19 | drawRect( 20 | brush = Brush.verticalGradient(0.95f to Color.Red, 1f to Color.Transparent), 21 | blendMode = BlendMode.DstIn 22 | ) 23 | } 24 | 25 | @Composable 26 | fun Modifier.ignoreHorizontalPadding() = this 27 | .requiredWidth(LocalConfiguration.current.screenWidthDp.dp) -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/nav/Pages.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.nav 2 | 3 | object Pages { 4 | 5 | const val SETTINGS_PAGE: String = "settings_page" 6 | const val APPS_PAGE: String = "apps_page" 7 | const val LAYOUT_PAGE: String = "layout_page" 8 | const val UPDATE_PAGE: String = "update_page" 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/nav/RootNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.nav 2 | 3 | import androidx.compose.animation.AnimatedContentScope 4 | import androidx.compose.runtime.Composable 5 | import androidx.navigation.NavBackStackEntry 6 | import androidx.navigation.NavGraphBuilder 7 | import androidx.navigation.compose.NavHost 8 | import androidx.navigation.compose.composable 9 | import androidx.navigation.compose.rememberNavController 10 | import com.acszo.redomi.isGithubBuild 11 | import com.acszo.redomi.ui.common.TransitionDirection 12 | import com.acszo.redomi.ui.common.enterHorizontalTransition 13 | import com.acszo.redomi.ui.common.exitHorizontalTransition 14 | import com.acszo.redomi.ui.component.BackButton 15 | import com.acszo.redomi.ui.page.settings.apps.AppsPage 16 | import com.acszo.redomi.ui.page.settings.layout.LayoutPage 17 | import com.acszo.redomi.ui.page.settings.SettingsPage 18 | import com.acszo.redomi.ui.page.settings.update.UpdatePage 19 | import com.acszo.redomi.viewmodel.DataStoreViewModel 20 | import com.acszo.redomi.viewmodel.UpdateViewModel 21 | 22 | @Composable 23 | fun RootNavigation( 24 | dataStoreViewModel: DataStoreViewModel, 25 | updateViewModel: UpdateViewModel, 26 | isUpdateAvailable: Boolean 27 | ) { 28 | val navController = rememberNavController() 29 | 30 | NavHost( 31 | navController = navController, 32 | startDestination = Route.SettingsPage.route, 33 | ) { 34 | navigationComposable( 35 | route = Route.SettingsPage.route 36 | ) { 37 | SettingsPage( 38 | dataStoreViewModel = dataStoreViewModel, 39 | navController = navController, 40 | isUpdateAvailable = isUpdateAvailable 41 | ) 42 | } 43 | navigationComposable( 44 | route = Route.AppsPage.route 45 | ) { 46 | AppsPage( 47 | dataStoreViewModel = dataStoreViewModel, 48 | ) { 49 | BackButton { navController.popBackStack() } 50 | } 51 | } 52 | navigationComposable( 53 | route = Route.LayoutPage.route 54 | ) { 55 | LayoutPage( 56 | dataStoreViewModel = dataStoreViewModel, 57 | ) { 58 | BackButton { navController.popBackStack() } 59 | } 60 | } 61 | 62 | if (isGithubBuild) { 63 | navigationComposable( 64 | route = Route.UpdatePage.route 65 | ) { 66 | UpdatePage( 67 | updateViewModel = updateViewModel 68 | ) { 69 | BackButton { navController.popBackStack() } 70 | } 71 | } 72 | } 73 | } 74 | 75 | } 76 | 77 | fun NavGraphBuilder.navigationComposable( 78 | route: String, 79 | content: @Composable (AnimatedContentScope.(NavBackStackEntry) -> Unit) 80 | ) = composable( 81 | route = route, 82 | enterTransition = { enterHorizontalTransition(TransitionDirection.FORWARD) }, 83 | exitTransition = { exitHorizontalTransition(TransitionDirection.BACKWARD) }, 84 | popEnterTransition = { enterHorizontalTransition(TransitionDirection.BACKWARD) }, 85 | popExitTransition = { exitHorizontalTransition(TransitionDirection.FORWARD) }, 86 | content = content, 87 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/nav/Route.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.nav 2 | 3 | import com.acszo.redomi.ui.nav.Pages.APPS_PAGE 4 | import com.acszo.redomi.ui.nav.Pages.LAYOUT_PAGE 5 | import com.acszo.redomi.ui.nav.Pages.SETTINGS_PAGE 6 | import com.acszo.redomi.ui.nav.Pages.UPDATE_PAGE 7 | 8 | sealed class Route(var route: String) { 9 | 10 | data object SettingsPage: Route(SETTINGS_PAGE) 11 | data object AppsPage: Route(APPS_PAGE) 12 | data object LayoutPage: Route(LAYOUT_PAGE) 13 | data object UpdatePage: Route(UPDATE_PAGE) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/bottom_sheet/ActionsMenu.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.bottom_sheet 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Row 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.foundation.shape.CircleShape 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.MaterialTheme 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.draw.clip 21 | import androidx.compose.ui.platform.ClipboardManager 22 | import androidx.compose.ui.platform.LocalClipboardManager 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.compose.ui.res.painterResource 25 | import androidx.compose.ui.res.stringResource 26 | import androidx.compose.ui.unit.dp 27 | import com.acszo.redomi.R 28 | import com.acszo.redomi.ui.component.ClickableItem 29 | import com.acszo.redomi.utils.ClipboardUtils.copyText 30 | import com.acszo.redomi.utils.IntentUtil.onIntentSend 31 | import com.acszo.redomi.utils.IntentUtil.onIntentView 32 | 33 | @Composable 34 | fun ActionsMenu( 35 | url: String, 36 | onDismiss: () -> Unit 37 | ) { 38 | val context = LocalContext.current 39 | val clipboardManager: ClipboardManager = LocalClipboardManager.current 40 | 41 | Row( 42 | modifier = Modifier 43 | .padding(vertical = 15.dp) 44 | .height(150.dp) 45 | .fillMaxWidth(), 46 | horizontalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterHorizontally) 47 | ) { 48 | ActionsItem( 49 | icon = R.drawable.ic_play, 50 | title = stringResource(id = R.string.open) 51 | ) { 52 | onIntentView(context, url) 53 | onDismiss() 54 | } 55 | 56 | ActionsItem( 57 | icon = R.drawable.ic_link, 58 | title = stringResource(id = android.R.string.copy) 59 | ) { 60 | copyText(clipboardManager, url) 61 | onDismiss() 62 | } 63 | 64 | ActionsItem( 65 | icon = R.drawable.ic_share, 66 | title = stringResource(id = R.string.share) 67 | ) { 68 | onIntentSend(context, url) 69 | onDismiss() 70 | } 71 | } 72 | } 73 | 74 | @Composable 75 | private fun ActionsItem( 76 | @DrawableRes icon: Int, 77 | title: String, 78 | onClickAction: () -> Unit 79 | ) { 80 | ClickableItem { 81 | Box( 82 | modifier = Modifier 83 | .clip(CircleShape) 84 | .clickable { onClickAction() } 85 | .background(MaterialTheme.colorScheme.secondaryContainer) 86 | .size(70.dp) 87 | .padding(8.dp), 88 | contentAlignment = Alignment.Center 89 | ) { 90 | Icon( 91 | painter = painterResource(id = icon), 92 | modifier = Modifier 93 | .size(50.dp) 94 | .padding(8.dp), 95 | tint = MaterialTheme.colorScheme.onSecondaryContainer, 96 | contentDescription = title 97 | ) 98 | } 99 | 100 | Text( 101 | text = title, 102 | style = MaterialTheme.typography.bodyLarge 103 | ) 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/bottom_sheet/BottomSheetError.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.bottom_sheet 2 | 3 | import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi 4 | import androidx.compose.animation.graphics.res.animatedVectorResource 5 | import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter 6 | import androidx.compose.animation.graphics.vector.AnimatedImageVector 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.material3.ButtonDefaults 12 | import androidx.compose.material3.FilledTonalButton 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.LaunchedEffect 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.res.painterResource 25 | import androidx.compose.ui.res.stringResource 26 | import androidx.compose.ui.unit.dp 27 | import com.acszo.redomi.R 28 | import kotlinx.coroutines.delay 29 | 30 | @OptIn(ExperimentalAnimationGraphicsApi::class) 31 | @Composable 32 | fun BottomSheetError( 33 | message: String, 34 | onClick: () -> Unit 35 | ) { 36 | val animatedIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_not_found) 37 | var atEnd by remember { mutableStateOf(false) } 38 | 39 | LaunchedEffect(Unit) { 40 | delay(500) 41 | atEnd = true 42 | } 43 | 44 | Column( 45 | modifier = Modifier.size(200.dp), 46 | horizontalAlignment = Alignment.CenterHorizontally, 47 | verticalArrangement = Arrangement.SpaceEvenly, 48 | ) { 49 | Icon( 50 | painter = rememberAnimatedVectorPainter(animatedIcon, atEnd), 51 | modifier = Modifier.size(80.dp), 52 | tint = MaterialTheme.colorScheme.secondary, 53 | contentDescription = message, 54 | ) 55 | 56 | Text( 57 | text = message, 58 | color = MaterialTheme.colorScheme.error, 59 | style = MaterialTheme.typography.titleMedium, 60 | ) 61 | 62 | FilledTonalButton( 63 | onClick = onClick, 64 | contentPadding = ButtonDefaults.ButtonWithIconContentPadding, 65 | ) { 66 | Icon( 67 | painter = painterResource(id = R.drawable.ic_refresh), 68 | contentDescription = null, 69 | ) 70 | Text( 71 | text = stringResource(id = R.string.retry), 72 | modifier = Modifier.padding(start = 8.dp) 73 | ) 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/bottom_sheet/Lists.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.bottom_sheet 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.lazy.LazyRow 10 | import androidx.compose.foundation.lazy.grid.GridCells 11 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 12 | import androidx.compose.foundation.lazy.grid.items 13 | import androidx.compose.foundation.lazy.items 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.graphics.Shape 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.unit.dp 22 | import com.acszo.redomi.data.ListOrientation 23 | import com.acszo.redomi.model.Platform.platforms 24 | import com.acszo.redomi.model.Song 25 | import com.acszo.redomi.ui.component.ClickableItem 26 | 27 | @Composable 28 | fun AppsList( 29 | listOrientation: ListOrientation, 30 | songs: List, 31 | iconShape: Shape, 32 | gridSize: Int, 33 | onClick: (song: Song) -> Unit 34 | ) { 35 | when (listOrientation) { 36 | ListOrientation.HORIZONTAL -> { 37 | HorizontalList( 38 | songs = songs, 39 | iconShape = iconShape, 40 | onClick = { onClick(it) } 41 | ) 42 | } 43 | 44 | ListOrientation.VERTICAL -> { 45 | VerticalList( 46 | songs = songs, 47 | iconShape = iconShape, 48 | gridSize = gridSize, 49 | onClick = { onClick(it) } 50 | ) 51 | } 52 | } 53 | } 54 | 55 | @Composable 56 | private fun HorizontalList( 57 | songs: List, 58 | iconShape: Shape, 59 | onClick: (song: Song) -> Unit 60 | ) { 61 | LazyRow( 62 | modifier = Modifier 63 | .padding(vertical = 15.dp) 64 | .height(150.dp), 65 | contentPadding = PaddingValues(horizontal = 10.dp), 66 | ) { 67 | items(items = songs) { 68 | AppItem( 69 | platform = it.platform, 70 | iconShape = iconShape, 71 | onClick = { onClick(it) } 72 | ) 73 | } 74 | } 75 | } 76 | 77 | @Composable 78 | private fun VerticalList( 79 | songs: List, 80 | iconShape: Shape, 81 | gridSize: Int, 82 | onClick: (song: Song) -> Unit 83 | ) { 84 | LazyVerticalGrid( 85 | modifier = Modifier.padding(vertical = 15.dp), 86 | columns = GridCells.Fixed(gridSize), 87 | contentPadding = PaddingValues(horizontal = 10.dp), 88 | ) { 89 | items(items = songs) { 90 | AppItem( 91 | platform = it.platform, 92 | iconShape = iconShape, 93 | onClick = { onClick(it) } 94 | ) 95 | } 96 | } 97 | } 98 | 99 | @Composable 100 | private fun AppItem( 101 | platform: String, 102 | iconShape: Shape, 103 | onClick: () -> Unit 104 | ) { 105 | val app = platforms[platform]!! 106 | val titleWords = app.title.split(' ') 107 | 108 | ClickableItem( 109 | modifier = Modifier 110 | .clip(MaterialTheme.shapes.large) 111 | .clickable { onClick() } 112 | ) { 113 | Image( 114 | painter = painterResource(id = app.icon), 115 | modifier = Modifier 116 | .size(80.dp) 117 | .padding(8.dp) 118 | .clip(iconShape), 119 | contentDescription = app.title, 120 | ) 121 | Text(text = titleWords[0]) 122 | Text(text = if (titleWords.size > 1) titleWords[1] else "") 123 | } 124 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/settings/SettingsItem.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.settings 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 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.material3.Icon 12 | import androidx.compose.material3.MaterialTheme 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.res.painterResource 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.unit.dp 20 | import com.acszo.redomi.R 21 | import com.acszo.redomi.ui.component.ignoreHorizontalPadding 22 | 23 | @Composable 24 | fun SettingsItem( 25 | @DrawableRes icon: Int, 26 | title: String, 27 | description: String, 28 | isAlertIconVisible: Boolean = false, 29 | onClick: () -> Unit 30 | ) { 31 | Row( 32 | modifier = Modifier 33 | .fillMaxWidth() 34 | .clickable { onClick() } 35 | .ignoreHorizontalPadding() 36 | .padding(horizontal = 28.dp, vertical = 16.dp), 37 | verticalAlignment = Alignment.CenterVertically 38 | ) { 39 | Icon( 40 | painter = painterResource(id = icon), 41 | modifier = Modifier.size(24.dp), 42 | tint = MaterialTheme.colorScheme.secondary, 43 | contentDescription = title, 44 | ) 45 | Column( 46 | modifier = Modifier.padding(horizontal = 16.dp) 47 | ) { 48 | Text( 49 | text = title, 50 | color = MaterialTheme.colorScheme.onSurface, 51 | style = MaterialTheme.typography.titleLarge 52 | ) 53 | Text( 54 | text = description, 55 | color = MaterialTheme.colorScheme.onSurfaceVariant, 56 | style = MaterialTheme.typography.bodyMedium, 57 | ) 58 | } 59 | Spacer(modifier = Modifier.weight(1f)) 60 | if (isAlertIconVisible) { 61 | Icon( 62 | painter = painterResource(id = R.drawable.ic_new_releases_outline), 63 | modifier = Modifier.size(24.dp), 64 | tint = MaterialTheme.colorScheme.error, 65 | contentDescription = stringResource(id = R.string.update), 66 | ) 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/settings/apps/AppsCheckBoxList.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.settings.apps 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.lazy.LazyListScope 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.material3.Checkbox 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.graphics.Shape 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.unit.dp 22 | import com.acszo.redomi.R 23 | import com.acszo.redomi.data.IconShape 24 | import com.acszo.redomi.model.Platform.platforms 25 | 26 | fun LazyListScope.appsCheckBoxList( 27 | apps: Set, 28 | iconShape: Int, 29 | onCheck: (apps: Set) -> Unit, 30 | ) = items(platforms.toList()) { (id, app) -> 31 | AppCheckBoxItem( 32 | icon = app.icon, 33 | iconShape = IconShape.entries[iconShape].shape, 34 | title = app.title, 35 | isChecked = apps.contains(id), 36 | onCheckedAction = { 37 | onCheck(apps + id) 38 | }, 39 | onUnCheckedAction = { 40 | if (apps.size > 1) onCheck(apps - id) 41 | } 42 | ) 43 | } 44 | 45 | @Composable 46 | private fun AppCheckBoxItem( 47 | @DrawableRes icon: Int, 48 | iconShape: Shape, 49 | title: String, 50 | isChecked: Boolean, 51 | onCheckedAction: () -> Unit, 52 | onUnCheckedAction: () -> Unit, 53 | ) { 54 | Row( 55 | modifier = Modifier.padding(vertical = 8.dp), 56 | verticalAlignment = Alignment.CenterVertically, 57 | horizontalArrangement = Arrangement.spacedBy(18.dp), 58 | ) { 59 | Icon( 60 | painter = painterResource(id = R.drawable.ic_menu), 61 | modifier = Modifier.size(20.dp), 62 | tint = MaterialTheme.colorScheme.secondary, 63 | contentDescription = title, 64 | ) 65 | Image( 66 | painter = painterResource(id = icon), 67 | modifier = Modifier 68 | .size(40.dp) 69 | .clip(iconShape), 70 | contentDescription = title, 71 | ) 72 | Text( 73 | text = title, 74 | modifier = Modifier.weight(1f), 75 | ) 76 | Checkbox( 77 | checked = isChecked, 78 | onCheckedChange = { isChecked -> 79 | if (isChecked) onCheckedAction() 80 | else onUnCheckedAction() 81 | }, 82 | ) 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/settings/apps/AppsPage.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.settings.apps 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.material3.HorizontalDivider 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.dp 21 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 22 | import com.acszo.redomi.R 23 | import com.acszo.redomi.data.AppList 24 | import com.acszo.redomi.ui.common.ScaffoldWithLargeTopAppBar 25 | import com.acszo.redomi.ui.common.removeTopPadding 26 | import com.acszo.redomi.viewmodel.DataStoreViewModel 27 | 28 | @OptIn(ExperimentalFoundationApi::class) 29 | @Composable 30 | fun AppsPage( 31 | dataStoreViewModel: DataStoreViewModel, 32 | backButton: @Composable () -> Unit 33 | ) { 34 | val selectedTab = remember { mutableStateOf(AppList.OPENING) } 35 | 36 | LaunchedEffect(Unit) { 37 | dataStoreViewModel.getOpeningApps() 38 | dataStoreViewModel.getSharingApps() 39 | } 40 | 41 | val iconShape by dataStoreViewModel.iconShape.collectAsStateWithLifecycle() 42 | val openingApps by dataStoreViewModel.openingApps.collectAsStateWithLifecycle() 43 | val sharingApps by dataStoreViewModel.sharingApps.collectAsStateWithLifecycle() 44 | 45 | ScaffoldWithLargeTopAppBar( 46 | title = stringResource(id = R.string.apps), 47 | description = stringResource(id = R.string.apps_description_page), 48 | backButton = { backButton() } 49 | ) { padding, pageTitleWithDescription -> 50 | LazyColumn( 51 | modifier = Modifier 52 | .padding(top = padding.calculateTopPadding()) 53 | .fillMaxSize(), 54 | contentPadding = padding.removeTopPadding(), 55 | ) { 56 | item { 57 | pageTitleWithDescription() 58 | } 59 | 60 | item { 61 | Spacer(modifier = Modifier.height(14.dp)) 62 | } 63 | 64 | stickyHeader { 65 | Column( 66 | modifier = Modifier 67 | .background(MaterialTheme.colorScheme.surface) 68 | .padding(vertical = 14.dp), 69 | ) { 70 | Tabs(selectedTab = selectedTab) 71 | } 72 | } 73 | 74 | item { 75 | Spacer(modifier = Modifier.height(14.dp)) 76 | } 77 | 78 | if (selectedTab.value == AppList.OPENING) { 79 | appsCheckBoxList( 80 | apps = openingApps, 81 | iconShape = iconShape, 82 | onCheck = { dataStoreViewModel.setOpeningApps(it) }, 83 | ) 84 | } else { 85 | appsCheckBoxList( 86 | apps = sharingApps, 87 | iconShape = iconShape, 88 | onCheck = { dataStoreViewModel.setSharingApps(it) }, 89 | ) 90 | } 91 | 92 | item { 93 | Spacer(modifier = Modifier.height(28.dp)) 94 | } 95 | 96 | item { 97 | HorizontalDivider() 98 | } 99 | 100 | item { 101 | Spacer(modifier = Modifier.height(28.dp)) 102 | } 103 | 104 | item { 105 | BottomInfo( 106 | text = stringResource( 107 | id = if (selectedTab.value == AppList.OPENING) R.string.opening_apps_tab_info 108 | else R.string.sharing_apps_tab_info 109 | ) 110 | ) 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/settings/apps/BottomInfo.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.settings.apps 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import com.acszo.redomi.R 15 | 16 | @Composable 17 | fun BottomInfo( 18 | text: String 19 | ) { 20 | Column( 21 | modifier = Modifier.padding(bottom = 28.dp), 22 | verticalArrangement = Arrangement.spacedBy(16.dp), 23 | ) { 24 | Icon( 25 | painter = painterResource(id = R.drawable.ic_info_outline), 26 | tint = MaterialTheme.colorScheme.onSurfaceVariant, 27 | contentDescription = stringResource(id = R.string.info), 28 | ) 29 | Text( 30 | text = text, 31 | color = MaterialTheme.colorScheme.onSurfaceVariant, 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/settings/apps/Tabs.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.settings.apps 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.animateDpAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Arrangement 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.MutableState 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.clip 24 | import androidx.compose.ui.draw.drawBehind 25 | import androidx.compose.ui.layout.layout 26 | import androidx.compose.ui.layout.onGloballyPositioned 27 | import androidx.compose.ui.platform.LocalDensity 28 | import androidx.compose.ui.res.stringResource 29 | import androidx.compose.ui.text.font.FontWeight 30 | import androidx.compose.ui.unit.dp 31 | import com.acszo.redomi.data.AppList 32 | import com.acszo.redomi.ui.component.selectedBoxColor 33 | import com.acszo.redomi.ui.component.selectedTextColor 34 | 35 | @Composable 36 | fun Tabs( 37 | selectedTab: MutableState 38 | ) { 39 | val width = remember { mutableStateOf(0.dp) } 40 | val density = LocalDensity.current 41 | 42 | Row( 43 | modifier = Modifier 44 | .fillMaxWidth() 45 | .clip(MaterialTheme.shapes.extraLarge) 46 | .background(MaterialTheme.colorScheme.surfaceVariant), 47 | horizontalArrangement = Arrangement.SpaceEvenly, 48 | verticalAlignment = Alignment.CenterVertically, 49 | ) { 50 | AppList.entries.forEach { tab -> 51 | val tabColor by animateColorAsState( 52 | targetValue = selectedBoxColor(selectedTab.value == tab), 53 | animationSpec = tween(100, 0, LinearEasing), 54 | label = "" 55 | ) 56 | 57 | val animatedPadding by animateDpAsState( 58 | targetValue = if (selectedTab.value == tab) 0.dp else width.value / 8, 59 | label = "" 60 | ) 61 | 62 | // goofy double clipping 🔥🔥🔥 63 | Box( 64 | modifier = Modifier 65 | .fillMaxWidth() 66 | .weight(1f) 67 | .onGloballyPositioned { 68 | width.value = with(density) { 69 | it.size.width.toDp() 70 | } 71 | } 72 | .clip(MaterialTheme.shapes.extraLarge) 73 | .clickable { 74 | selectedTab.value = tab 75 | } 76 | .clip(MaterialTheme.shapes.extraLarge) 77 | .layout { measurable, constraints -> 78 | val size = width.value.toPx() - animatedPadding.toPx() 79 | val placeable = measurable.measure( 80 | constraints.copy(minWidth = size.toInt()) 81 | ) 82 | layout(placeable.width, placeable.height) { 83 | placeable.placeRelative(0, 0) 84 | } 85 | } 86 | .drawBehind { drawRect(tabColor) }, 87 | contentAlignment = Alignment.Center, 88 | ) { 89 | Text( 90 | text = stringResource(id = tab.toRes), 91 | modifier = Modifier.padding(vertical = 15.dp), 92 | maxLines = 1, 93 | color = selectedTextColor(selectedTab.value == tab), 94 | style = MaterialTheme.typography.bodyLarge.copy( 95 | fontWeight = FontWeight.W500, 96 | ) 97 | ) 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/settings/layout/LayoutPage.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.settings.layout 2 | 3 | import androidx.compose.animation.AnimatedVisibility 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.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 19 | import com.acszo.redomi.R 20 | import com.acszo.redomi.data.DataStoreConst.BIG_GRID 21 | import com.acszo.redomi.data.DataStoreConst.SMALL_GRID 22 | import com.acszo.redomi.data.ListOrientation 23 | import com.acszo.redomi.ui.common.ScaffoldWithLargeTopAppBar 24 | import com.acszo.redomi.ui.common.enterVerticalTransition 25 | import com.acszo.redomi.ui.common.exitVerticalTransition 26 | import com.acszo.redomi.ui.component.AnimatedRadiusButton 27 | import com.acszo.redomi.ui.component.RadioButtonItemPage 28 | import com.acszo.redomi.viewmodel.DataStoreViewModel 29 | 30 | @Composable 31 | fun LayoutPage( 32 | dataStoreViewModel: DataStoreViewModel, 33 | backButton: @Composable () -> Unit 34 | ) { 35 | val listOrientation by dataStoreViewModel.listOrientation.collectAsStateWithLifecycle() 36 | val gridSize by dataStoreViewModel.gridSize.collectAsStateWithLifecycle() 37 | 38 | ScaffoldWithLargeTopAppBar( 39 | title = stringResource(id = R.string.layout), 40 | description = stringResource(id = R.string.layout_description_page), 41 | backButton = { backButton() } 42 | ) { padding, pageTitleWithDescription -> 43 | LazyColumn( 44 | verticalArrangement = Arrangement.spacedBy(28.dp), 45 | contentPadding = padding, 46 | ) { 47 | item { 48 | pageTitleWithDescription() 49 | } 50 | 51 | item { 52 | Text( 53 | text = stringResource(id = R.string.list), 54 | color = MaterialTheme.colorScheme.primary, 55 | style = MaterialTheme.typography.titleMedium, 56 | ) 57 | } 58 | 59 | item { 60 | ListOrientation.entries.forEach { item -> 61 | RadioButtonItemPage( 62 | item = item, 63 | isSelected = item == ListOrientation.entries[listOrientation], 64 | onClick = dataStoreViewModel::setListOrientation 65 | ) 66 | } 67 | } 68 | 69 | item { 70 | AnimatedVisibility( 71 | visible = listOrientation == ListOrientation.VERTICAL.ordinal, 72 | enter = enterVerticalTransition(), 73 | exit = exitVerticalTransition(), 74 | ) { 75 | Column( 76 | modifier = Modifier.padding(bottom = 28.dp), 77 | verticalArrangement = Arrangement.spacedBy(28.dp) 78 | ) { 79 | Text( 80 | text = stringResource(id = R.string.layout_grid_size), 81 | color = MaterialTheme.colorScheme.primary, 82 | style = MaterialTheme.typography.titleMedium, 83 | ) 84 | Row( 85 | modifier = Modifier.fillMaxWidth(), 86 | horizontalArrangement = Arrangement.SpaceAround, 87 | verticalAlignment = Alignment.CenterVertically 88 | ) { 89 | for (grid in SMALL_GRID..BIG_GRID) { 90 | AnimatedRadiusButton( 91 | isSelected = gridSize == grid, 92 | size = 80.dp, 93 | text = grid.toString() 94 | ) { 95 | dataStoreViewModel.setGridSize(grid) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/page/settings/update/AnnotatedString.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.page.settings.update 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.text.SpanStyle 9 | import androidx.compose.ui.text.buildAnnotatedString 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.text.withStyle 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun AnnotatedString(string: String) { 16 | Text( 17 | text = buildAnnotatedString { 18 | string.lineSequence().forEach { line -> 19 | val find = "# " 20 | if (line.contains(find)) { 21 | val newLine = line.replace(find, "") 22 | withStyle( 23 | style = SpanStyle( 24 | fontWeight = FontWeight.Bold, 25 | fontSize = MaterialTheme.typography.titleMedium.fontSize, 26 | ) 27 | ) { 28 | append("$newLine\n") 29 | } 30 | } else { 31 | append("$line\n") 32 | } 33 | } 34 | }, 35 | modifier = Modifier.padding(horizontal = 10.dp), 36 | ) 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val PrimaryDark = Color(0xFF0061A5) 6 | val SecondaryDark = Color(0xFF535F70) 7 | val TertiaryDark = Color(0xFF6B5778) 8 | 9 | val PrimaryLight = Color(0xFF9FCAFF) 10 | val SecondaryLight = Color(0xFFBBC7DB) 11 | val TertiaryLight = Color(0xFFD7BEE4) -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.darkColorScheme 7 | import androidx.compose.material3.dynamicDarkColorScheme 8 | import androidx.compose.material3.dynamicLightColorScheme 9 | import androidx.compose.material3.lightColorScheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.SideEffect 12 | import androidx.compose.ui.graphics.toArgb 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.core.view.WindowCompat 16 | import com.acszo.redomi.MainActivity 17 | import com.acszo.redomi.data.Theme 18 | 19 | private val darkColorScheme = darkColorScheme( 20 | primary = PrimaryLight, 21 | secondary = SecondaryLight, 22 | tertiary = TertiaryLight 23 | ) 24 | 25 | private val lightColorScheme = lightColorScheme( 26 | primary = PrimaryDark, 27 | secondary = SecondaryDark, 28 | tertiary = TertiaryDark 29 | ) 30 | 31 | @Composable 32 | fun RedomiTheme( 33 | theme: Int, 34 | content: @Composable () -> Unit 35 | ) { 36 | val getTheme = Theme.entries[theme].mode() 37 | 38 | val colorScheme = when { 39 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 40 | val context = LocalContext.current 41 | if (getTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 42 | } 43 | 44 | getTheme -> darkColorScheme 45 | else -> lightColorScheme 46 | } 47 | 48 | val view = LocalView.current 49 | if (!view.isInEditMode) { 50 | SideEffect { 51 | val activity = view.context as Activity 52 | val window = (activity).window 53 | when (activity) { 54 | is MainActivity -> { 55 | window.decorView.setBackgroundColor(colorScheme.surface.toArgb()) 56 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !getTheme 57 | } 58 | } 59 | } 60 | } 61 | 62 | MaterialTheme( 63 | colorScheme = colorScheme, 64 | typography = Typography, 65 | content = content 66 | ) 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | val Typography = Typography( 10 | bodyLarge = TextStyle( 11 | fontFamily = FontFamily.Default, 12 | fontWeight = FontWeight.Normal, 13 | fontSize = 16.sp, 14 | lineHeight = 24.sp, 15 | letterSpacing = 0.5.sp 16 | ) 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/utils/ClipboardUtils.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.utils 2 | 3 | import androidx.compose.ui.platform.ClipboardManager 4 | import androidx.compose.ui.text.AnnotatedString 5 | 6 | object ClipboardUtils { 7 | 8 | fun copyText(clipboardManager: ClipboardManager, text: String) { 9 | clipboardManager.setText(AnnotatedString(text)) 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/utils/IntentUtil.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.net.Uri 8 | import android.os.Build 9 | import android.provider.Settings 10 | import com.acszo.redomi.MainActivity 11 | 12 | object IntentUtil { 13 | 14 | fun onIntentView(context: Context, url: String) { 15 | val uri = Uri.parse(url) 16 | var intent = Intent(Intent.ACTION_VIEW) 17 | .setData(uri) 18 | .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 19 | 20 | // Checks the default app that resolves the uri 21 | val resolveInfo = context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) 22 | val packageName = resolveInfo?.activityInfo?.packageName ?: "" 23 | 24 | // Force to open in browser, when Redomi is the default to resolve the uri 25 | if (packageName == context.packageName) { 26 | intent = Intent.makeMainSelectorActivity( 27 | Intent.ACTION_MAIN, 28 | Intent.CATEGORY_APP_BROWSER 29 | ).setData(uri) 30 | } 31 | context.startActivity(intent) 32 | } 33 | 34 | fun onIntentSend(context: Context, url: String) { 35 | val intent = Intent(Intent.ACTION_SEND) 36 | .putExtra(Intent.EXTRA_TEXT, url) 37 | .setType("text/plain") 38 | context.startActivity(Intent.createChooser(intent, null)) 39 | } 40 | 41 | @SuppressLint("InlinedApi") 42 | fun onIntentOpenDefaultsApp(context: Context) { 43 | val uri = Uri.parse("package:${context.packageName}") 44 | 45 | // Work around for One UI 4, because ACTION_APP_OPEN_BY_DEFAULT_SETTING crashes, kpop company moment 🫰 46 | val settings = if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S && Build.MANUFACTURER.equals("samsung", ignoreCase = true)) { 47 | Settings.ACTION_APPLICATION_DETAILS_SETTINGS 48 | } else { 49 | Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS 50 | } 51 | context.startActivity(Intent(settings, uri)) 52 | } 53 | 54 | fun onIntentSettingsPage(context: Context) { 55 | context.startActivity(Intent(context, MainActivity::class.java)) 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/utils/UpdateUtil.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.core.content.FileProvider 6 | import java.io.File 7 | 8 | object UpdateUtil { 9 | 10 | private const val MIME_APK = "application/vnd.android.package-archive" 11 | 12 | fun Context.getApk() = File(getExternalFilesDir("apk"), "latest-release") 13 | 14 | private fun apkUri(context: Context) = FileProvider.getUriForFile( 15 | context, 16 | "${context.packageName}.provider", 17 | context.getApk() 18 | ) 19 | 20 | // much shorter than my previous PackageInstaller solution, but not safe. 21 | // Fellas hope that I don't get hacked 👍 22 | fun installApk(context: Context) { 23 | val intent = Intent(Intent.ACTION_VIEW) 24 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) 25 | .setDataAndType(apkUri(context), MIME_APK) 26 | context.startActivity(intent) 27 | } 28 | 29 | fun deleteApk(context: Context) = context.runCatching { 30 | val apk = context.getApk() 31 | if (apk.exists()) { 32 | apk.delete() 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/viewmodel/DataStoreViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.acszo.redomi.data.DataStoreConst.FIRST_TIME 6 | import com.acszo.redomi.data.DataStoreConst.GRID_SIZE 7 | import com.acszo.redomi.data.DataStoreConst.ICON_SHAPE 8 | import com.acszo.redomi.data.DataStoreConst.LIST_ORIENTATION 9 | import com.acszo.redomi.data.DataStoreConst.MEDIUM_GRID 10 | import com.acszo.redomi.data.DataStoreConst.OPENING_APPS 11 | import com.acszo.redomi.data.DataStoreConst.SHARING_APPS 12 | import com.acszo.redomi.data.DataStoreConst.THEME_MODE 13 | import com.acszo.redomi.data.IconShape 14 | import com.acszo.redomi.data.ListOrientation 15 | import com.acszo.redomi.data.SettingsDataStore 16 | import com.acszo.redomi.data.Theme 17 | import dagger.hilt.android.lifecycle.HiltViewModel 18 | import kotlinx.coroutines.flow.MutableStateFlow 19 | import kotlinx.coroutines.flow.StateFlow 20 | import kotlinx.coroutines.flow.asStateFlow 21 | import kotlinx.coroutines.flow.collectLatest 22 | import kotlinx.coroutines.launch 23 | import javax.inject.Inject 24 | 25 | @HiltViewModel 26 | class DataStoreViewModel @Inject constructor( 27 | private val settingsDataStore: SettingsDataStore 28 | ): ViewModel() { 29 | 30 | private val _openingApps: MutableStateFlow> = MutableStateFlow(emptySet()) 31 | val openingApps: StateFlow> = _openingApps.asStateFlow() 32 | 33 | private val _sharingApps: MutableStateFlow> = MutableStateFlow(emptySet()) 34 | val sharingApps: StateFlow> = _sharingApps.asStateFlow() 35 | 36 | private val _isFirstTime: MutableStateFlow = MutableStateFlow(false) 37 | val isFirstTime: StateFlow = _isFirstTime.asStateFlow() 38 | 39 | private val _listOrientation: MutableStateFlow = MutableStateFlow(ListOrientation.HORIZONTAL.ordinal) 40 | val listOrientation: StateFlow = _listOrientation.asStateFlow() 41 | 42 | private val _gridSize: MutableStateFlow = MutableStateFlow(MEDIUM_GRID) 43 | val gridSize: StateFlow = _gridSize.asStateFlow() 44 | 45 | private val _iconShape: MutableStateFlow = MutableStateFlow(IconShape.SQUIRCLE.ordinal) 46 | val iconShape: StateFlow = _iconShape.asStateFlow() 47 | 48 | private val _themeMode: MutableStateFlow = MutableStateFlow(Theme.SYSTEM_THEME.ordinal) 49 | val themeMode: StateFlow = _themeMode.asStateFlow() 50 | 51 | init { 52 | viewModelScope.launch { 53 | settingsDataStore.getInt(LIST_ORIENTATION).collectLatest { 54 | _listOrientation.value = it ?: ListOrientation.HORIZONTAL.ordinal 55 | } 56 | } 57 | 58 | viewModelScope.launch { 59 | settingsDataStore.getInt(GRID_SIZE).collectLatest { 60 | _gridSize.value = it ?: MEDIUM_GRID 61 | } 62 | } 63 | 64 | viewModelScope.launch { 65 | settingsDataStore.getInt(ICON_SHAPE).collectLatest { 66 | _iconShape.value = it ?: IconShape.SQUIRCLE.ordinal 67 | } 68 | } 69 | 70 | viewModelScope.launch { 71 | settingsDataStore.getInt(THEME_MODE).collectLatest { 72 | _themeMode.value = it ?: Theme.SYSTEM_THEME.ordinal 73 | } 74 | } 75 | } 76 | 77 | fun getOpeningApps() = viewModelScope.launch { 78 | settingsDataStore.getSetOfStrings(OPENING_APPS).collectLatest { 79 | _openingApps.value = it 80 | } 81 | } 82 | 83 | fun setOpeningApps(openingApps: Set) = viewModelScope.launch { 84 | settingsDataStore.setSetOfStrings(OPENING_APPS, openingApps) 85 | } 86 | 87 | fun getSharingApps() = viewModelScope.launch { 88 | settingsDataStore.getSetOfStrings(SHARING_APPS).collectLatest { 89 | _sharingApps.value = it 90 | } 91 | } 92 | 93 | fun setSharingApps(sharingApps: Set) = viewModelScope.launch { 94 | settingsDataStore.setSetOfStrings(SHARING_APPS, sharingApps) 95 | } 96 | 97 | fun getIsFirstTime() = viewModelScope.launch { 98 | settingsDataStore.getBoolean(FIRST_TIME).collectLatest { 99 | _isFirstTime.value = it != false 100 | } 101 | } 102 | 103 | fun setIsFirstTime() = viewModelScope.launch { 104 | settingsDataStore.setBoolean(FIRST_TIME, false) 105 | } 106 | 107 | fun setListOrientation(listOrientation: Int) = viewModelScope.launch { 108 | settingsDataStore.setInt(LIST_ORIENTATION, listOrientation) 109 | } 110 | 111 | fun setGridSize(gridSize: Int) = viewModelScope.launch { 112 | settingsDataStore.setInt(GRID_SIZE, gridSize) 113 | } 114 | 115 | fun setIconShape(iconShape: Int) = viewModelScope.launch { 116 | settingsDataStore.setInt(ICON_SHAPE, iconShape) 117 | } 118 | 119 | fun setThemeMode(themeMode: Int) = viewModelScope.launch { 120 | settingsDataStore.setInt(THEME_MODE, themeMode) 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/viewmodel/SongLinkViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.viewmodel 2 | 3 | import android.net.Uri 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.acszo.redomi.data.SettingsDataStore 7 | import com.acszo.redomi.model.Platform.platforms 8 | import com.acszo.redomi.model.Song 9 | import com.acszo.redomi.repository.SongLinkRepository 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.asStateFlow 13 | import kotlinx.coroutines.flow.first 14 | import kotlinx.coroutines.flow.update 15 | import kotlinx.coroutines.launch 16 | import com.acszo.redomi.R 17 | import com.acszo.redomi.service.ApiResult 18 | import javax.inject.Inject 19 | 20 | data class BottomSheetUiState( 21 | val sourceSong: Song? = null, 22 | val songs: List = emptyList(), 23 | val isLoading: Boolean = false, 24 | val error: Int? = null 25 | ) 26 | 27 | @HiltViewModel 28 | class SongLinkViewModel @Inject constructor( 29 | private val settingsDataStore: SettingsDataStore, 30 | private val songLinkRepository: SongLinkRepository 31 | ): ViewModel() { 32 | 33 | private val _bottomSheetUiState = MutableStateFlow(BottomSheetUiState()) 34 | val bottomSheetUiState = _bottomSheetUiState.asStateFlow() 35 | 36 | fun getPlatformsLink(url: String, key: String) = viewModelScope.launch { 37 | _bottomSheetUiState.update { it.copy(isLoading = true) } 38 | val response = songLinkRepository.getSongs(url) 39 | 40 | when(response) { 41 | is ApiResult.Success -> { 42 | val data = response.data 43 | val sourceSong = data.entitiesByUniqueId[data.entityUniqueId] 44 | val query = sourceSong?.run { Uri.encode("$title - $artistName") } 45 | 46 | val selectedApps = settingsDataStore.getSetOfStrings(key).first() 47 | val orderedApps = platforms.keys.filter { selectedApps.contains(it) } 48 | 49 | val songsInfo = orderedApps.mapNotNull { platform -> 50 | val linksByPlatform = data.linksByPlatform[platform] 51 | 52 | if (linksByPlatform != null) { 53 | data.entitiesByUniqueId[linksByPlatform.entityUniqueId] 54 | ?.copy(platform = platform, link = linksByPlatform.url) 55 | } else { 56 | Song( 57 | isMatched = false, 58 | platform = platform, 59 | link = "${platforms[platform]?.searchUrl}$query", 60 | type = null, 61 | title = sourceSong?.title, 62 | artistName = sourceSong?.artistName, 63 | thumbnailUrl = null 64 | ) 65 | } 66 | } 67 | 68 | _bottomSheetUiState.update { 69 | it.copy(sourceSong = sourceSong, songs = songsInfo, isLoading = false, error = null) 70 | } 71 | } 72 | is ApiResult.Error -> { 73 | var message = when { 74 | response.code in 400..499 -> R.string.error_incorrect_url 75 | response.code in 500..599 -> R.string.error_server 76 | else -> R.string.error_generic 77 | } 78 | 79 | _bottomSheetUiState.update { 80 | it.copy(error = message, isLoading = false) 81 | } 82 | } 83 | is ApiResult.Exception -> { 84 | _bottomSheetUiState.update { 85 | it.copy(error = response.message, isLoading = false) 86 | } 87 | } 88 | } 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /app/src/main/java/com/acszo/redomi/viewmodel/UpdateViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi.viewmodel 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.acszo.redomi.R 7 | import com.acszo.redomi.model.DownloadStatus 8 | import com.acszo.redomi.model.Release 9 | import com.acszo.redomi.repository.GithubRepository 10 | import com.acszo.redomi.service.ApiResult 11 | import com.acszo.redomi.utils.UpdateUtil.getApk 12 | import dagger.hilt.android.lifecycle.HiltViewModel 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.distinctUntilChanged 18 | import kotlinx.coroutines.flow.emptyFlow 19 | import kotlinx.coroutines.flow.flow 20 | import kotlinx.coroutines.flow.flowOn 21 | import kotlinx.coroutines.flow.update 22 | import kotlinx.coroutines.launch 23 | import kotlinx.coroutines.withContext 24 | import okhttp3.ResponseBody 25 | import javax.inject.Inject 26 | 27 | data class UpdatePageUiState( 28 | val latestRelease: Release? = null, 29 | val isLoading: Boolean = false, 30 | val error: Int? = null, 31 | ) 32 | 33 | @HiltViewModel 34 | class UpdateViewModel @Inject constructor( 35 | private val githubRepository: GithubRepository 36 | ): ViewModel() { 37 | 38 | private val _updatePageUiState = MutableStateFlow(UpdatePageUiState()) 39 | val updatePageUiState = _updatePageUiState.asStateFlow() 40 | 41 | private val _isUpdateAvailable: MutableStateFlow = MutableStateFlow(false) 42 | val isUpdateAvailable = _isUpdateAvailable.asStateFlow() 43 | 44 | fun checkUpdate(currentVersion: String) = viewModelScope.launch { 45 | _updatePageUiState.update { it.copy(isLoading = true) } 46 | val response = githubRepository.getLatest() 47 | 48 | when (response) { 49 | is ApiResult.Success -> { 50 | val latestRelease = response.data 51 | _isUpdateAvailable.update { 52 | currentVersion < latestRelease.tagName 53 | } 54 | _updatePageUiState.update { 55 | it.copy(latestRelease = latestRelease, isLoading = false, error = null) 56 | } 57 | } 58 | is ApiResult.Error -> { 59 | var message = when { 60 | response.code in 400..499 -> R.string.update_info_failed 61 | response.code in 500..599 -> R.string.error_server 62 | else -> R.string.error_generic 63 | } 64 | 65 | _updatePageUiState.update { 66 | it.copy(error = message, isLoading = false) 67 | } 68 | } 69 | is ApiResult.Exception -> { 70 | _updatePageUiState.update { 71 | it.copy(error = response.message, isLoading = false) 72 | } 73 | } 74 | } 75 | } 76 | 77 | suspend fun downloadApk(context: Context, release: Release): Flow = withContext(Dispatchers.IO) { 78 | val assetUrl = release.assets[0].browserDownloadUrl 79 | try { 80 | return@withContext githubRepository.getLatestApk(assetUrl).saveApk(context) 81 | } catch (e: Exception) { 82 | print(e.message) 83 | } 84 | emptyFlow() 85 | } 86 | 87 | private fun ResponseBody.saveApk(context: Context): Flow = flow { 88 | emit(DownloadStatus.Downloading(0)) 89 | 90 | val apkFile = context.getApk() 91 | byteStream().use { inputStream -> 92 | apkFile.outputStream().use { outputStream -> 93 | val totalBytes = contentLength() 94 | val buffer = ByteArray(DEFAULT_BUFFER_SIZE) 95 | var progressBytes = 0L 96 | var bytes = inputStream.read(buffer) 97 | 98 | while (bytes >= 0) { 99 | outputStream.write(buffer, 0, bytes) 100 | progressBytes += bytes 101 | bytes = inputStream.read(buffer) 102 | emit(DownloadStatus.Downloading(((progressBytes * 100) / totalBytes).toInt())) 103 | } 104 | } 105 | } 106 | 107 | emit(DownloadStatus.Finished) 108 | }.flowOn(Dispatchers.IO).distinctUntilChanged() 109 | 110 | } -------------------------------------------------------------------------------- /app/src/main/res/color-v31/m3_ref_palette_dynamic_neutral_variant6.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/color-v31/m3_ref_palette_dynamic_neutral_variant98.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_amazon_music.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable-v24/ic_amazon_music.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_apple_music.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable-v24/ic_apple_music.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_deezer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable-v24/ic_deezer.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_napster.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable-v24/ic_napster.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_soundcloud.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable-v24/ic_soundcloud.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_spotify.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable-v24/ic_spotify.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_tidal.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable-v24/ic_tidal.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_youtube.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable-v24/ic_youtube.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_youtube_music.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable-v24/ic_youtube_music.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_album.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_anghami.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable/ic_anghami.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back_arrow.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_category_filled.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_category_outline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_color_lens_filled.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_color_lens_outline.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_description.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_all.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_format_list_bulleted.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_grid_view.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_filled.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_outline.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_link.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_music_note.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_new_releases_inside.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_new_releases_outline.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_new_releases_outside.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_remove_done.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_splash_screen.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_update.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_yandex.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/app/src/main/res/drawable/ic_yandex.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US -------------------------------------------------------------------------------- /app/src/main/res/values-ar-rSA/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | تحويل الأغنية 4 | مشاركة الرابط العالمي 5 | فتح 6 | مشاركة 7 | غلاف الأغنية 8 | إعدادات 9 | عودة 10 | التطبيقات 11 | Select order and which apps to view 12 | نسق 13 | قائمة 14 | قائمة %s 15 | أفقي 16 | رَأسِيّ 17 | شكل الأيقونة 18 | دائري مربّع 19 | دائرة 20 | مربع 21 | الثيم 22 | Check out the repository at %s 23 | تحديث 24 | التحقق من التحديثات المتاحة 25 | إصدار 26 | معلومات 27 | Opening 28 | Sharing 29 | Choose the order and which apps you want to see when converting a song. 30 | Opening a link shows these selected apps. Selecting only one app will open it directly. 31 | Sharing from a music platform shows these selected apps. Selecting only one app will open the actions directly. 32 | Choose the way you want to display the apps you see when converting a song. 33 | حجم الشبكة 34 | Updates are checked every time you start the app. 35 | هذا الإصدار 36 | إصدار جديد 37 | تفقد التحديث 38 | للتحديث 39 | الإفتراضي للنظام 40 | داكن 41 | ضوء 42 | الدليل 43 | By default %s can\'t open external links. You will get redirected to the settings to make it work. 44 | Check the links so %s can open them correctly. 45 | Leave unchecked the ones that are from music services you use. (e.g. if you use Spotify leave it unchecked) 46 | تعذر الحصول على معلومات التحديث 47 | عنوان Url المعطى غير صحيح 48 | لا يوجد اتصال بالإنترنت 49 | حدث خطأ ما 50 | خطأ من الخادم 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-bs-rBA/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pretvori pjesmu 4 | Dijelite univerzalni link 5 | Otvori 6 | Dijeli 7 | Omot pesme 8 | Postavke 9 | Nazad 10 | Aplikacije 11 | Select order and which apps to view 12 | Izgled 13 | Lista 14 | %s Lista 15 | Horizontalna 16 | Vertikalna 17 | Oblik ikone 18 | Kružni kvadrat 19 | Krug 20 | Kvadrat 21 | Tema 22 | Check out the repository at %s 23 | Ažuriraj 24 | Provjerite dostupna ažuriranja 25 | Verzija 26 | Informacije 27 | Opening 28 | Sharing 29 | Choose the order and which apps you want to see when converting a song. 30 | Opening a link shows these selected apps. Selecting only one app will open it directly. 31 | Sharing from a music platform shows these selected apps. Selecting only one app will open the actions directly. 32 | Choose the way you want to display the apps you see when converting a song. 33 | Veličina mreže 34 | Updates are checked every time you start the app. 35 | Ova verzija 36 | Nova verzija 37 | Provjeri ažuriranja 38 | Ažurirati 39 | Sistemski zadano 40 | Tamno 41 | Svjetlo 42 | Vodič 43 | By default %s can\'t open external links. You will get redirected to the settings to make it work. 44 | Check the links so %s can open them correctly. 45 | Leave unchecked the ones that are from music services you use. (e.g. if you use Spotify leave it unchecked) 46 | Ne mogu dobiti podatke o ažuriranju 47 | Navedeni url je netačan 48 | Nema internetske veze 49 | Došlo je do greške 50 | Serverska greška 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-cs-rCZ/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Převést skladbu 4 | Sdílet univerzální odkaz 5 | Otevřít 6 | Sdílet 7 | Kryt skladby 8 | Nastavení 9 | Zpět 10 | Aplikace 11 | Vyberte pořadí a které aplikace se mají zobrazit 12 | Rozložení 13 | Seznam 14 | %s seznam 15 | Horizontální 16 | Vertikální 17 | Tvar ikony 18 | Superelipsa 19 | Kruh 20 | Čtverec 21 | Téma 22 | Podívejte se na repozitář na %s 23 | Aktualizovat 24 | Zkontrolovat dostupné aktualizace 25 | Verze 26 | Informace 27 | Otevírání 28 | Sdílení 29 | Vyberte pořadí a které aplikace chcete vidět při konvertování skladby. 30 | Po otevření odkazu se zobrazí tyto vybrané aplikace. Vybráním jediné aplikace jej otevřete přímo. 31 | Sdílení z hudební platformy zobrazí tyto vybrané aplikace. Vybráním jediné aplikace jej otevřete přímo. 32 | Vyberte způsob, kterým chcete zobrazit aplikace, které budete vidět při konvertování skladby. 33 | Velikost mřížky 34 | Aktualizace jsou kontrolovány vždy, když spustíte aplikaci. 35 | Tato verze 36 | Nová verze 37 | Zkontrolovat aktualizace 38 | Aktualizovat 39 | Výchozí nastavení 40 | Tmavý 41 | Světlý 42 | Průvodce 43 | Ve výchozím nastavení aplikace %s nedokáže otevírat externí odkazy. Budete přesměrováni do nastavení, kde tuto funkci můžete povolit. 44 | Zaškrtněte odkazy, které chcete otevírat přes aplikaci %s. 45 | Nezaškrtávejte odkazy z hudebních služeb, které používáte. (např. pokud používáte Spotify, nezaškrtávejte jej) 46 | Získání informací o aktualizaci se nezdařilo 47 | Zadaná adresa url je nesprávná 48 | Žádné připojení k internetu 49 | Vyskytla se chyba 50 | Chyba serveru 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-de-rDE/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Song konvertieren 4 | Universellen Link teilen 5 | Öffnen 6 | Teilen 7 | Titelbild 8 | Einstellungen 9 | Zurück 10 | Anwendungen 11 | Wähle die Reihenfolge und welche Anwendungen angezeigt werden sollen 12 | Anordnung 13 | Liste 14 | %se Liste 15 | Horizontal 16 | Vertikal 17 | Symbolform 18 | Squircle 19 | Kreis 20 | Quadrat 21 | Erscheinungsbild 22 | Schau dir die repository bei %s an 23 | Update 24 | Nach verfügbaren Update suchen 25 | Version 26 | Info 27 | Öffnen 28 | Teilen 29 | Wähle die Reihenfolge und welche Anwendungen du angezeigt bekommen möchtest, wenn du ein Lied konvertierst. 30 | Wenn Sie einen Link öffnen, werden diese ausgewählten Anwendungen angezeigt. Wenn nur eine Anwendung ausgewählt wird, wird sie direkt geöffnet. 31 | Beim Teilen von einer Musikplattform werden diese ausgewählten Anwendungen angezeigt. Wenn du nur eine App auswählst, wird das Fenster der Aktionen geöffnet. 32 | Wähle die Art und Weise, wie du die Apps angezeigt haben möchtest, die du bei der Konvertierung eines Titels siehst. 33 | Rastergröße 34 | Auf Updates wird bei jedem Start der App geprüft. 35 | Diese Version 36 | Neue Version 37 | Auf Aktualisierungen prüfen 38 | Update 39 | Systemstandard 40 | Dunkel 41 | Hell 42 | Anleitung 43 | Standardmäßig kann %s keine externen Links öffnen. Du wirst zu den Einstellungen weitergeleitet, damit es funktioniert. 44 | Überprüfe die Links, damit %s sie korrekt öffnen kann. 45 | Lasse diejenigen Links aus, die du als Musikdienst bereits nutzt (wenn du z.B. Spotify nutzt, brauchst du es hier nicht auswählen) 46 | Update-Informationen konnten nicht geladen werden 47 | Die angegebene url ist falsch 48 | Keine Internetverbindung 49 | Ein Fehler ist aufgetreten 50 | Serverfehler 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-es-rES/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Convertir canción 4 | Compartir enlace universal 5 | Abrir 6 | Compartir 7 | Tapa de la canción 8 | Ajustes 9 | Atrás 10 | Aplicaciones 11 | Select order and which apps to view 12 | Disposición 13 | Lista 14 | Lista %s 15 | Horizontal 16 | Vertical 17 | Forma de icono 18 | Squircle 19 | Círculo 20 | Cuadrado 21 | Tema 22 | Check out the repository at %s 23 | Actualizar 24 | Buscar actualización disponible 25 | Versión 26 | Información 27 | Opening 28 | Sharing 29 | Choose the order and which apps you want to see when converting a song. 30 | Opening a link shows these selected apps. Selecting only one app will open it directly. 31 | Sharing from a music platform shows these selected apps. Selecting only one app will open the actions directly. 32 | Choose the way you want to display the apps you see when converting a song. 33 | Tamaño de grilla 34 | Updates are checked every time you start the app. 35 | Esta versión 36 | Nueva versión 37 | Buscar actualizaciones 38 | Actualizar 39 | Predeterminado del sistema 40 | Oscuro 41 | Claro 42 | Guia 43 | By default %s can\'t open external links. You will get redirected to the settings to make it work. 44 | Check the links so %s can open them correctly. 45 | Leave unchecked the ones that are from music services you use. (e.g. if you use Spotify leave it unchecked) 46 | No se pudo cargar la información de actualización 47 | La url dada es incorrecta 48 | Sin conexión a internet 49 | Un error ha ocurrido 50 | Error de servidor 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr-rFR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Convertir la musique 4 | Partager le lien universel 5 | Ouvrir 6 | Partager 7 | Pochette de la musique 8 | Paramètres 9 | Retour 10 | Applications 11 | Sélectionner l\'ordre et les applications à afficher 12 | Mise en Page 13 | Liste 14 | Liste %s 15 | Horizontale 16 | Verticale 17 | Forme d\'icône 18 | Carrercle 19 | Cercle 20 | Carré 21 | Thème 22 | Consultez le dépôt à %s 23 | Mise à jour 24 | Vérifier les mises à jour disponibles 25 | Version 26 | Information 27 | Ouvrir 28 | Partager 29 | Choisissez l\'ordre et les applications que vous souhaitez voir lors de la conversion d\'une musique. 30 | Ouvrir un lien montre ces applications sélectionnées. Sélectionner une seule application l\'ouvrira directement. 31 | Le partage depuis une plateforme musicale montre ces applications sélectionnées. La sélection d\'une seule application ouvrira les actions directement. 32 | Choisissez la façon dont vous souhaitez afficher les applications que vous voyez lors de la conversion d\'une musique. 33 | Taille de la grille 34 | Les mises à jour sont vérifiées à chaque fois que vous démarrez l\'application. 35 | Cette version 36 | Nouvelle version 37 | Rechercher les mises à jour 38 | Mettre à jour 39 | Par défaut 40 | Sombre 41 | Clair 42 | Guide 43 | Par défaut, %s ne peut pas ouvrir de liens externes. Vous serez redirigé vers les paramètres pour le faire fonctionner. 44 | Vérifiez les liens pour que %s puisse les ouvrir correctement. 45 | Laissez décocher ceux qui proviennent des services de musique que vous utilisez. (par exemple si vous utilisez Spotify ne le laissez pas coché) 46 | Impossible de charger les informations de mise à jour 47 | L\'url donnée est incorrecte 48 | Aucune connexion internet 49 | Une erreur est survenue 50 | Erreur du serveur 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-hr-rHR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pretvori pjesmu 4 | Dijelite univerzalnu vezu 5 | Otvori 6 | Dijeli 7 | Naslovnica pjesme 8 | Postavke 9 | Nazad 10 | Aplikacije 11 | Select order and which apps to view 12 | Izgled 13 | Lista 14 | %s Lista 15 | Horizontalna 16 | Vertikalna 17 | Oblik ikona 18 | Zaobljeni kvadrat 19 | Krug 20 | Kvadrat 21 | Tema 22 | Check out the repository at %s 23 | Ažuriraj 24 | Provjerite dostupna ažuriranja 25 | Verzija 26 | Informacije 27 | Opening 28 | Sharing 29 | Choose the order and which apps you want to see when converting a song. 30 | Opening a link shows these selected apps. Selecting only one app will open it directly. 31 | Sharing from a music platform shows these selected apps. Selecting only one app will open the actions directly. 32 | Choose the way you want to display the apps you see when converting a song. 33 | Veličina rešetke 34 | Updates are checked every time you start the app. 35 | Ova verzija 36 | Nova verzija 37 | Projveri ažuriranja 38 | Ažurirati 39 | Zadana postavka 40 | Tamno 41 | Svijetlo 42 | Vodič 43 | By default %s can\'t open external links. You will get redirected to the settings to make it work. 44 | Check the links so %s can open them correctly. 45 | Leave unchecked the ones that are from music services you use. (e.g. if you use Spotify leave it unchecked) 46 | Ne mogu dobiti informacije o ažuriranju 47 | Navedeni url nije točan 48 | Nema internetske veze 49 | Dogodila se pogreška 50 | Serverska greška 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-it-rIT/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Converti canzone 4 | Condividi link universale 5 | Apri 6 | Condividi 7 | Cover Canzone 8 | Impostazioni 9 | Indietro 10 | Applicazioni 11 | Seleziona l\'ordine e quali applicazioni visualizzare 12 | Aspetto 13 | Lista 14 | Lista %s 15 | Orizzontale 16 | Verticale 17 | Forma icona 18 | Supercerchio 19 | Cerchio 20 | Quadrato 21 | Tema 22 | Fai un salto nella repository in %s 23 | Aggiornamento 24 | Verifica la presenza di aggiornamenti 25 | Versione 26 | Informazioni 27 | Apri 28 | Condividi 29 | Scegli l\'ordine e quali applicazioni vuoi mostrare quando converti una canzone. 30 | Aprendo un link vengono mostrate queste applicazioni selezionate. Selezionando una sola app la apre direttamente. 31 | Condividendo da una piattaforma musicale mostra queste applicazioni selezionate. Selezionando una sola app apre le azioni direttamente. 32 | Decidi come visualizzare le applicazioni che vedi quando converti una canzone. 33 | Dimensione griglia 34 | Gli aggiornamenti vengono controllati ad ogni avvio dell\'applicazione. 35 | Questa versione 36 | Nuova versione 37 | Controlla aggiornamenti 38 | Aggiorna 39 | Predefinito 40 | Scuro 41 | Chiaro 42 | Guida 43 | Di base %s non può aprire link esterni. Verrai reindirizzato alle impostazioni per farlo funzionare. 44 | Seleziona i link così che %s possa aprirli correttamente. 45 | Lascia deselezionati quelli che fanno parte dei servizi di musica che usi. (es. se usi Spotify lascialo deselezionato) 46 | Impossibile caricare le informazioni sull\'aggiornamento 47 | L\'url fornito non è corretto 48 | Nessuna connessione internet 49 | Si è verificato un errore 50 | Errore del server 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja-rJP/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 曲の変換 4 | ユニバーサルリンクを共有 5 | 開ける 6 | 共有 7 | 曲のカバー 8 | 設定 9 | 戻る 10 | アプリ 11 | Select order and which apps to view 12 | レイアウト 13 | リスト 14 | %s リスト 15 | 水平 16 | 垂直 17 | アイコンの形状 18 | 角丸四角形 19 | 20 | 四角 21 | テーマ 22 | Check out the repository at %s 23 | アップデート 24 | 利用可能な更新を確認します 25 | バージョン 26 | 情報 27 | Opening 28 | Sharing 29 | Choose the order and which apps you want to see when converting a song. 30 | Opening a link shows these selected apps. Selecting only one app will open it directly. 31 | Sharing from a music platform shows these selected apps. Selecting only one app will open the actions directly. 32 | Choose the way you want to display the apps you see when converting a song. 33 | グリッドサイズ 34 | Updates are checked every time you start the app. 35 | このバージョン 36 | 新しいバージョン 37 | アップデートを確認 38 | 更新する 39 | システムのデフォルト 40 | ダーク 41 | ライト 42 | ガイド 43 | By default %s can\'t open external links. You will get redirected to the settings to make it work. 44 | Check the links so %s can open them correctly. 45 | Leave unchecked the ones that are from music services you use. (e.g. if you use Spotify leave it unchecked) 46 | アップデート情報を取得できませんでした 47 | 指定された URL が間違っています 48 | インターネット接続なし 49 | エラーが発生しました 50 | サーバーエラー 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-night-v31/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @color/m3_ref_palette_dynamic_neutral_variant6 4 | @android:color/system_accent2_200 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-night-v34/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_surface_dark 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-pl-rPL/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Konwertuj utwór 4 | Udostępnij uniwersalny link 5 | Otwórz 6 | Udostępnij 7 | Okładka piosenki 8 | Ustawienia 9 | Wstecz 10 | Aplikacje 11 | Select order and which apps to view 12 | Układ 13 | Lista 14 | Lista %s 15 | Pozioma 16 | Pionowa 17 | Kształt ikon 18 | Obły kwadrat 19 | Koło 20 | Kwadrat 21 | Motyw 22 | Check out the repository at %s 23 | Aktualizacja 24 | Sprawdź dostępne aktualizacje 25 | Wersja 26 | Informacje 27 | Opening 28 | Sharing 29 | Choose the order and which apps you want to see when converting a song. 30 | Opening a link shows these selected apps. Selecting only one app will open it directly. 31 | Sharing from a music platform shows these selected apps. Selecting only one app will open the actions directly. 32 | Choose the way you want to display the apps you see when converting a song. 33 | Rozmiar siatki 34 | Updates are checked every time you start the app. 35 | Ta wersja 36 | Nowa wersja 37 | Sprawdź aktualizacje 38 | Aktualizować 39 | Domyślny systemu 40 | Ciemny 41 | Jasny 42 | Przewodnik 43 | By default %s can\'t open external links. You will get redirected to the settings to make it work. 44 | Check the links so %s can open them correctly. 45 | Leave unchecked the ones that are from music services you use. (e.g. if you use Spotify leave it unchecked) 46 | Nie można pobrać informacji o aktualizacji 47 | Podany adres url jest nieprawidłowy 48 | Brak połączenia z Internetem 49 | Wystąpił błąd 50 | Błąd serwera 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rPT/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Converter música 4 | Compartilhar link universal 5 | Abrir 6 | Partilhar 7 | Capa de música 8 | Configurações 9 | Atrás 10 | Aplicações 11 | Selecione a ordem e quais aplicativos deseja visualizar 12 | Disposição 13 | Lista 14 | Lista %s 15 | Horizontal 16 | Vertical 17 | Forma do ícone 18 | Quadrado arredondado 19 | Círculo 20 | Quadrado 21 | Tema 22 | Confira o repositório em %s 23 | Atualizar 24 | Verificar atualizações disponíveis 25 | Versão 26 | Informação 27 | Abrindo 28 | Compartilhando 29 | Escolha a ordem e os apps que você quer ver quando converter uma música. 30 | Abrir um link mostrará os aplicativos selecionados. Selecionar apenas um aplicativo irá abri-lo diretamente. 31 | Abrir um link mostra os aplicativos selecionados. Selecionar apenas um aplicativo irá abri-lo diretamente. 32 | Escolha como você quer exibir os apps que você vê ao converter uma música. 33 | Tamanho da grelha 34 | As atualizações são verificadas sempre que você inicia o aplicativo. 35 | Esta versão 36 | Nova versão 37 | Verificar atualizações 38 | Atualizar 39 | Sistema padrão 40 | Escuro 41 | Claro 42 | Guia 43 | Por padrão, %s não pode abrir links externos. Você será redirecionado para as configurações para fazê-lo funcionar. 44 | Marque os links para que o %s possa abri-los corretamente. 45 | Deixe desmarcado os que são de serviços de música que você usa. (por exemplo, se você usar o Spotify o deixe desmarcado) 46 | Não foi possível carregar as informações de atualização 47 | O url informado está incorreto 48 | Sem ligação à Internet 49 | Surgiu um erro 50 | Erro de Servidor 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru-rRU/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Преобразовать песню 4 | Поделиться универсальной ссылкой 5 | Открыть 6 | Поделиться 7 | Обложка песни 8 | Настройки 9 | Назад 10 | Приложения 11 | Выберите порядок и приложения для просмотра 12 | Шаблон 13 | Список 14 | %s Список 15 | Горизонтально 16 | Вертикально 17 | Форма значка 18 | Скруглённый квадрат 19 | Круг 20 | Квадрат 21 | Тема 22 | Посмотрите репозиторий %s 23 | Обновление 24 | Проверить доступные обновления 25 | Версия 26 | Инфо 27 | Открытие 28 | Совместное использование 29 | Выберите порядок и приложения, которые вы хотите видеть при преобразовании песни. 30 | При открытии ссылки отображаются эти выбранные приложения. Выбрав только одно приложение, вы откроете его напрямую. 31 | При публикации с музыкальной платформы показаны выбранные приложения. При выборе только одного приложения действия будут открываться напрямую. 32 | Выберите способ отображения приложений, которые вы видите при преобразовании песни. 33 | Размер сетки 34 | Обновления проверяются каждый раз, когда вы запускаете приложение. 35 | Данная версия 36 | Новая версия 37 | Проверить обновления 38 | Обновить 39 | Системная 40 | Тёмная 41 | Светлая 42 | Руководство 43 | По умолчанию %s не может открывать внешние ссылки. Вы будете перенаправлены в настройки, чтобы заставить её работать. 44 | Проверьте ссылки, чтобы %s могла открывать их правильно. 45 | Снимите флажки с тех, которые относятся к используемым вами музыкальным сервисам. (например, если вы пользуетесь Spotify, снимите этот флажок) 46 | Не удалось загрузить информацию об обновлении 47 | Указанный url неверен 48 | Нет подключения к Интернету 49 | Произошла ошибка 50 | Ошибка сервера 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-sr-rSP/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Претвори песму 4 | Поделите универзалну линк 5 | Отвори 6 | Дели 7 | Обрада песме 8 | Подешавања 9 | Назад 10 | Апликације 11 | Select order and which apps to view 12 | Изглед 13 | Листа 14 | %s Листа 15 | Хоризонтална 16 | Вертикална 17 | Облик иконице 18 | Кружни квадрат 19 | Круг 20 | Квадрат 21 | Тема 22 | Check out the repository at %s 23 | Ажурирање 24 | Проверите доступна ажурирања 25 | Верзија 26 | Информације 27 | Opening 28 | Sharing 29 | Choose the order and which apps you want to see when converting a song. 30 | Opening a link shows these selected apps. Selecting only one app will open it directly. 31 | Sharing from a music platform shows these selected apps. Selecting only one app will open the actions directly. 32 | Choose the way you want to display the apps you see when converting a song. 33 | Величина мреже 34 | Updates are checked every time you start the app. 35 | Ова верзија 36 | Нова верзија 37 | Прегледај освежења 38 | Ажурирање 39 | Систем подразумевани 40 | Тамно 41 | Светло 42 | Водич 43 | By default %s can\'t open external links. You will get redirected to the settings to make it work. 44 | Check the links so %s can open them correctly. 45 | Leave unchecked the ones that are from music services you use. (e.g. if you use Spotify leave it unchecked) 46 | Нису добијени подаци о ажурирању 47 | The given url is incorrect 48 | Нема интернет везе 49 | Дошло је до грешке 50 | Серверска грешка 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-tr-rTR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Şarkıyı dönüştür 4 | Evrensel bağlantıyı paylaş 5 | 6 | Paylaş 7 | Şarkı kapağı 8 | Ayarlar 9 | Geri 10 | Uygulamalar 11 | Sırayı ve görüntülenecek uygulamaları seçin 12 | Düzen 13 | Liste 14 | %s Liste 15 | Yatay 16 | Dikey 17 | Simge şekli 18 | Oval kare 19 | Daire 20 | Kare 21 | Tema 22 | %s adresindeki depoyu inceleyin 23 | Güncellemeler 24 | Mevcut güncellemeleri kontrol edin 25 | Sürüm 26 | Bilgi 27 | Açılma 28 | Paylaşma 29 | Bir şarkıyı dönüştürürken sırayı ve hangi uygulamaları görmek istediğinizi seçin. 30 | Bir bağlantı açıldığında bu seçilen uygulamalar gösterilir. Yalnızca bir uygulama seçildiğinde doğrudan o uygulama açılır. 31 | Bir müzik platformundan paylaşımda bu seçilen uygulamalar gösteriliyor. Yalnızca bir uygulama seçildiğinde eylemler doğrudan açılır. 32 | Bir şarkıyı dönüştürürken gördüğünüz uygulamaları nasıl görüntülemek istediğinizi seçin. 33 | Izgara boyutu 34 | Uygulamayı her başlattığınızda güncellemeler kontrol edilir. 35 | Mevcut sürüm 36 | Yeni sürüm 37 | Güncellemeleri kontrol et 38 | Güncelle 39 | Sistem varsayılanı 40 | Karanlık 41 | Aydınlık 42 | Rehber 43 | Varsayılan olarak %s harici bağlantıları açamaz. Çalıştırmak için ayarlara yönlendirileceksiniz. 44 | %s\'nin doğru şekilde açabilmesi için bağlantıları kontrol edin. 45 | Kullandığınız müzik hizmetlerinden olanları işaretlemeyin. (örneğin Spotify kullanıyorsanız işareti kaldırın) 46 | Güncelleme bilgisi yüklenemedi 47 | Verilen url yanlış 48 | İnternet bağlantısı yok 49 | Bir hata oluştu 50 | Sunucu hatası 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-v29/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-v31/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @color/m3_ref_palette_dynamic_neutral_variant98 4 | @android:color/system_accent2_600 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-v34/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_surface_light 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 转换歌曲 4 | 共享通用链接 5 | 打开 6 | 分享 7 | 歌曲封面 8 | 设置 9 | 返回 10 | 应用 11 | Select order and which apps to view 12 | 界面 13 | 列表 14 | %s 列表 15 | 水平 16 | 垂直 17 | 图标形状 18 | 圆角矩形 19 | 圆圈 20 | 正方形 21 | 主题 22 | Check out the repository at %s 23 | 更新 24 | 检查可用更新 25 | 版本 26 | 信息 27 | Opening 28 | Sharing 29 | Choose the order and which apps you want to see when converting a song. 30 | Opening a link shows these selected apps. Selecting only one app will open it directly. 31 | Sharing from a music platform shows these selected apps. Selecting only one app will open the actions directly. 32 | Choose the way you want to display the apps you see when converting a song. 33 | 网格尺寸 34 | Updates are checked every time you start the app. 35 | 此版本 36 | 新版本 37 | 检查更新 38 | 更新 39 | 系统默认 40 | 深色 41 | 浅色 42 | 指导 43 | By default %s can\'t open external links. You will get redirected to the settings to make it work. 44 | Check the links so %s can open them correctly. 45 | Leave unchecked the ones that are from music services you use. (e.g. if you use Spotify leave it unchecked) 46 | 无法加载更新信息 47 | 给定的URL不正确 48 | 没有网络连接 49 | 发生错误 50 | 服务器错误 51 | Request timed out 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF4991DC 4 | #FFFFFF 5 | #FF4991DC 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Redomi 3 | Convert song 4 | Share universal link 5 | Open 6 | Share 7 | Song Cover 8 | Settings 9 | Back 10 | Apps 11 | Select order and which apps to view 12 | Layout 13 | List 14 | %s List 15 | Horizontal 16 | Vertical 17 | Icon shape 18 | Squircle 19 | Circle 20 | Square 21 | Theme 22 | Github 23 | acszo/Redomi 24 | Check out the repository at %s 25 | Update 26 | Check available updates 27 | Version 28 | Info 29 | Opening 30 | Sharing 31 | Choose the order and which apps you want to see when converting a song. 32 | Opening a link shows these selected apps. Selecting only one app will open it directly. 33 | Sharing from a music platform shows these selected apps. Selecting only one app will open the actions directly. 34 | Choose the way you want to display the apps you see when converting a song. 35 | Grid size 36 | Updates are checked every time you start the app. 37 | This version 38 | New version 39 | Check updates 40 | Update 41 | System default 42 | Dark 43 | Light 44 | Guide 45 | By default %s can\'t open external links. You will get redirected to the settings to make it work. 46 | Check the links so %s can open them correctly. 47 | Leave unchecked the ones that are from music services you use. (e.g. if you use Spotify leave it unchecked) 48 | Couldn\'t load update info 49 | The given url is incorrect 50 | No internet connection 51 | An error occurred 52 | Server Error 53 | Request timed out 54 | Retry 55 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 14 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/repo/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/test/java/com/acszo/redomi/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.redomi 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath "org.jetbrains.kotlin:kotlin-serialization:2.1.20" 4 | classpath "com.google.dagger:hilt-android-gradle-plugin:2.52" 5 | } 6 | } 7 | 8 | plugins { 9 | id "com.android.library" version "8.9.0" apply false 10 | id "com.android.application" version "8.9.0" apply false 11 | id "org.jetbrains.kotlin.android" version "2.1.20" apply false 12 | id "org.jetbrains.kotlin.plugin.compose" version "2.1.20" apply false 13 | } -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /app/src/main/res/values/strings.xml 3 | translation: /app/src/main/res/values-%android_code%/strings.xml 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/10305.txt: -------------------------------------------------------------------------------- 1 | # Improvements 2 | - Error handling when converting songs 3 | - New Czech, Portoguese and French translations 4 | - Correctly format search links 5 | 6 | # Added 7 | - Support for Yandex Music and Anghami 8 | - tidal.com links -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Unofficial song.link client 2 | 3 |
Features: 4 | 5 | - Open supported links from different music services in your favourite streaming app 6 | - Convert links and share songs with others on their preferred platforms 7 | - Customize Sharesheet in different ways: 8 | - Icon shapes 9 | - Choose which apps to display 10 | - Decide between a horizontal or vertical list, and its grid size 11 | - Material You 12 | - Light/Dark Theme -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Open songs from different platforms to your favourite one -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Redomi -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 2 | android.useAndroidX=true 3 | kotlin.code.style=official 4 | android.nonTransitiveRClass=true 5 | org.gradle.configuration-cache=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Feb 28 12:03:24 CET 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-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 | -------------------------------------------------------------------------------- /preview/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /preview/open.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/preview/open.gif -------------------------------------------------------------------------------- /preview/screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/preview/screenshot_6.png -------------------------------------------------------------------------------- /preview/share.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/preview/share.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { url 'https://jitpack.io' } 14 | } 15 | } 16 | rootProject.name = "Redomi" 17 | include ':app' 18 | include ':squircle' 19 | -------------------------------------------------------------------------------- /squircle/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /squircle/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.acszo.squircle' 8 | compileSdk 35 9 | 10 | defaultConfig { 11 | minSdk 28 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | compileOptions { 21 | sourceCompatibility JavaVersion.VERSION_11 22 | targetCompatibility JavaVersion.VERSION_11 23 | } 24 | kotlinOptions { 25 | jvmTarget = '11' 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation 'androidx.compose.foundation:foundation:1.7.4' 31 | } -------------------------------------------------------------------------------- /squircle/proguard-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acszo/Redomi/e4020bdc94aeb967cfbac0b91e279f3bf84ce7fb/squircle/proguard-rules.pro -------------------------------------------------------------------------------- /squircle/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /squircle/src/main/java/com/acszo/squircle/SquircleShape.kt: -------------------------------------------------------------------------------- 1 | package com.acszo.squircle 2 | 3 | import androidx.compose.ui.geometry.Size 4 | import androidx.compose.ui.graphics.Outline 5 | import androidx.compose.ui.graphics.Path 6 | import androidx.compose.ui.graphics.Shape 7 | import androidx.compose.ui.unit.Density 8 | import androidx.compose.ui.unit.LayoutDirection 9 | 10 | object SquircleShape: Shape { 11 | 12 | private const val CORNER_SMOOTHING = 0.72f 13 | 14 | override fun createOutline( 15 | size: Size, 16 | layoutDirection: LayoutDirection, 17 | density: Density 18 | ): Outline { 19 | return Outline.Generic( 20 | path = Path().apply { 21 | val width = size.width 22 | val height = size.height 23 | val corner = size.minDimension / 2 24 | val smoothingFactor = corner * (1 - CORNER_SMOOTHING) 25 | 26 | moveTo(x = corner, y = 0f) 27 | 28 | lineTo(x = width - corner, y = 0f) 29 | cubicTo(x1 = width - smoothingFactor, y1 = 0f, x2 = width, y2 = smoothingFactor, x3 = width, y3 = corner) 30 | 31 | lineTo(x = width, y = height - corner) 32 | cubicTo(x1 = width, y1 = height - smoothingFactor, x2 = width - smoothingFactor, y2 = height, x3 = width - corner, y3 = height) 33 | 34 | lineTo(x = corner, y = height) 35 | cubicTo(x1 = smoothingFactor, y1 = height, x2 = 0f, y2 = height - smoothingFactor, x3 = 0f, y3 = height - corner) 36 | 37 | lineTo(x = 0f, y = corner) 38 | cubicTo(x1 = 0f, y1 = smoothingFactor, x2 = smoothingFactor, y2 = 0f, x3 = corner, y3 = 0f) 39 | 40 | close() 41 | } 42 | ) 43 | } 44 | 45 | } --------------------------------------------------------------------------------