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 | Aç
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 |
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 | }
--------------------------------------------------------------------------------