diff --git a/app/src/main/java/dev/anilbeesetti/nextplayer/navigation/SettingsNavGraph.kt b/app/src/main/java/dev/anilbeesetti/nextplayer/navigation/SettingsNavGraph.kt index b6623f500..a75e1e103 100644 --- a/app/src/main/java/dev/anilbeesetti/nextplayer/navigation/SettingsNavGraph.kt +++ b/app/src/main/java/dev/anilbeesetti/nextplayer/navigation/SettingsNavGraph.kt @@ -9,12 +9,14 @@ import dev.anilbeesetti.nextplayer.settings.navigation.appearancePreferencesScre import dev.anilbeesetti.nextplayer.settings.navigation.audioPreferencesScreen import dev.anilbeesetti.nextplayer.settings.navigation.decoderPreferencesScreen import dev.anilbeesetti.nextplayer.settings.navigation.folderPreferencesScreen +import dev.anilbeesetti.nextplayer.settings.navigation.librariesScreen import dev.anilbeesetti.nextplayer.settings.navigation.mediaLibraryPreferencesScreen import dev.anilbeesetti.nextplayer.settings.navigation.navigateToAboutPreferences import dev.anilbeesetti.nextplayer.settings.navigation.navigateToAppearancePreferences import dev.anilbeesetti.nextplayer.settings.navigation.navigateToAudioPreferences import dev.anilbeesetti.nextplayer.settings.navigation.navigateToDecoderPreferences import dev.anilbeesetti.nextplayer.settings.navigation.navigateToFolderPreferencesScreen +import dev.anilbeesetti.nextplayer.settings.navigation.navigateToLibraries import dev.anilbeesetti.nextplayer.settings.navigation.navigateToMediaLibraryPreferencesScreen import dev.anilbeesetti.nextplayer.settings.navigation.navigateToPlayerPreferences import dev.anilbeesetti.nextplayer.settings.navigation.navigateToSubtitlePreferences @@ -69,6 +71,10 @@ fun NavGraphBuilder.settingsNavGraph( onNavigateUp = navController::navigateUp, ) aboutPreferencesScreen( + onLibrariesClick = navController::navigateToLibraries, + onNavigateUp = navController::navigateUp, + ) + librariesScreen( onNavigateUp = navController::navigateUp, ) } diff --git a/core/ui/src/main/res/drawable/ic_github.xml b/core/ui/src/main/res/drawable/ic_github.xml new file mode 100644 index 000000000..6a3cad3e8 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_github.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_kofi.xml b/core/ui/src/main/res/drawable/ic_kofi.xml new file mode 100644 index 000000000..4eeaedde5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_kofi.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_paypal.xml b/core/ui/src/main/res/drawable/ic_paypal.xml new file mode 100644 index 000000000..64e14e57d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_paypal.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_upi.xml b/core/ui/src/main/res/drawable/ic_upi.xml new file mode 100644 index 000000000..0a4eac690 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_upi.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index fc0733651..52e2b8b70 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -216,4 +216,14 @@ Loop: Off Loop: One Loop: All + anilbeesetti + by %1$s + Libraries + Github + Donate + No app found to handle this link + Support the developer on %1$s + Ko-fi + PayPal + UPI diff --git a/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/navigation/AboutPreferencesNavigation.kt b/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/navigation/AboutPreferencesNavigation.kt index fe5bdd670..fea1e555c 100644 --- a/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/navigation/AboutPreferencesNavigation.kt +++ b/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/navigation/AboutPreferencesNavigation.kt @@ -6,16 +6,36 @@ import androidx.navigation.NavOptions import androidx.navigation.navOptions import dev.anilbeesetti.nextplayer.core.ui.designsystem.animatedComposable import dev.anilbeesetti.nextplayer.settings.screens.about.AboutPreferencesScreen +import dev.anilbeesetti.nextplayer.settings.screens.about.LibrariesScreen const val aboutPreferencesNavigationRoute = "about_preferences_route" +const val librariesNavigationRoute = "libraries_route" fun NavController.navigateToAboutPreferences(navOptions: NavOptions? = navOptions { launchSingleTop = true }) { this.navigate(aboutPreferencesNavigationRoute, navOptions) } -fun NavGraphBuilder.aboutPreferencesScreen(onNavigateUp: () -> Unit) { +fun NavController.navigateToLibraries(navOptions: NavOptions? = navOptions { launchSingleTop = true }) { + this.navigate(librariesNavigationRoute, navOptions) +} + +fun NavGraphBuilder.aboutPreferencesScreen( + onLibrariesClick: () -> Unit, + onNavigateUp: () -> Unit, +) { animatedComposable(route = aboutPreferencesNavigationRoute) { AboutPreferencesScreen( + onLibrariesClick = onLibrariesClick, + onNavigateUp = onNavigateUp, + ) + } +} + +fun NavGraphBuilder.librariesScreen( + onNavigateUp: () -> Unit, +) { + animatedComposable(route = librariesNavigationRoute) { + LibrariesScreen( onNavigateUp = onNavigateUp, ) } diff --git a/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/screens/about/AboutPreferencesScreen.kt b/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/screens/about/AboutPreferencesScreen.kt index 72db63e81..6889688f6 100644 --- a/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/screens/about/AboutPreferencesScreen.kt +++ b/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/screens/about/AboutPreferencesScreen.kt @@ -1,36 +1,85 @@ package dev.anilbeesetti.nextplayer.settings.screens.about -import androidx.compose.foundation.layout.PaddingValues +import android.content.Context +import android.os.Build +import android.widget.Toast +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.dp -import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer -import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults +import androidx.compose.ui.unit.sp +import androidx.core.graphics.drawable.toBitmapOrNull import dev.anilbeesetti.nextplayer.core.ui.R +import dev.anilbeesetti.nextplayer.core.ui.components.ClickablePreferenceItem import dev.anilbeesetti.nextplayer.core.ui.components.NextTopAppBar import dev.anilbeesetti.nextplayer.core.ui.designsystem.NextIcons +import dev.anilbeesetti.nextplayer.settings.composables.PreferenceSubtitle + +private const val GITHUB_URL = "https://github.com/anilbeesetti/nextplayer" +private const val KOFI_URL = "https://ko-fi.com/anilbeesetti" +private const val PAYPAL_URL = "https://paypal.me/AnilBeesetti" +private const val UPI_URL = "upi://pay?pa=anilbeesetti811@ybl&pn=Next%20Player&cu=INR" @OptIn(ExperimentalMaterial3Api::class) @Composable fun AboutPreferencesScreen( + onLibrariesClick: () -> Unit, onNavigateUp: () -> Unit, ) { val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior() + val context = LocalContext.current val uriHandler = LocalUriHandler.current Scaffold( @@ -54,28 +103,220 @@ fun AboutPreferencesScreen( ) }, ) { innerPadding -> - LibrariesContainer( - contentPadding = innerPadding, + Column( modifier = Modifier .fillMaxSize() - .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), - colors = LibraryDefaults.libraryColors( - backgroundColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.onBackground, - badgeBackgroundColor = MaterialTheme.colorScheme.secondaryContainer, - badgeContentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ), - padding = LibraryDefaults.libraryPadding( - namePadding = PaddingValues(bottom = 4.dp), - badgeContentPadding = PaddingValues(4.dp), - ), - onLibraryClick = { library -> - library.website?.let { - if (it.isNotEmpty()) { - uriHandler.openUri(it) - } + .padding(innerPadding) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) + .verticalScroll(rememberScrollState()), + ) { + AboutApp( + onGithubClick = { + uriHandler.openUriOrShowToast( + uri = GITHUB_URL, + context = context, + ) + }, + onLibrariesClick = onLibrariesClick, + ) + PreferenceSubtitle(text = stringResource(id = R.string.donate)) + ClickablePreferenceItem( + title = stringResource(R.string.kofi), + description = stringResource(R.string.support_the_developer_on, stringResource(R.string.kofi)), + icon = ImageVector.vectorResource(R.drawable.ic_kofi), + onClick = { + uriHandler.openUriOrShowToast( + uri = KOFI_URL, + context = context, + ) + }, + ) + + ClickablePreferenceItem( + title = stringResource(R.string.paypal), + description = stringResource(R.string.support_the_developer_on, stringResource(R.string.paypal)), + icon = ImageVector.vectorResource(R.drawable.ic_paypal), + onClick = { + uriHandler.openUriOrShowToast( + uri = PAYPAL_URL, + context = context, + ) + }, + ) + + if (Locale.current.region == "IN") { + ClickablePreferenceItem( + title = stringResource(R.string.upi), + description = stringResource(R.string.support_the_developer_on, stringResource(R.string.upi)), + icon = ImageVector.vectorResource(R.drawable.ic_upi), + onClick = { + uriHandler.openUriOrShowToast( + uri = UPI_URL, + context = context, + ) + }, + ) + } + } + } +} + +@Composable +fun AboutApp( + modifier: Modifier = Modifier, + onGithubClick: () -> Unit, + onLibrariesClick: () -> Unit, +) { + val context = LocalContext.current + val appVersion = remember { context.appVersion() } + val appIcon = remember { context.appIcon() } + + val colorPrimary = MaterialTheme.colorScheme.primaryContainer + val colorTertiary = MaterialTheme.colorScheme.tertiaryContainer + + val transition = rememberInfiniteTransition() + val fraction by transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 5000), + repeatMode = RepeatMode.Reverse, + ), + ) + val cornerRadius = 24.dp + + Column( + modifier = modifier + .padding(all = 16.dp) + .drawWithCache { + val cx = size.width - size.width * fraction + val cy = size.height * fraction + + val gradient = Brush.radialGradient( + colors = listOf(colorPrimary, colorTertiary), + center = Offset(cx, cy), + radius = 800f, + ) + + onDrawBehind { + drawRoundRect( + brush = gradient, + cornerRadius = CornerRadius( + cornerRadius.toPx(), + cornerRadius.toPx(), + ), + ) } - }, - ) + } + .padding(all = 24.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + appIcon?.let { + Image( + bitmap = it, + contentDescription = "App Logo", + modifier = Modifier.size(48.dp).clip(CircleShape), + ) + } + Column { + Text( + text = stringResource(id = R.string.app_name), + fontSize = 22.sp, + color = MaterialTheme.colorScheme.onSurface, + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = appVersion, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 12.sp, + ) + Text( + text = stringResource(R.string.by, stringResource(R.string.app_developer)), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = onLibrariesClick, + colors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = .12f), + containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f), + disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = .12f), + ), + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .weight(1f), + ) { + Text(text = stringResource(R.string.libraries)) + } + Button( + onClick = onGithubClick, + colors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onTertiary, + disabledContentColor = MaterialTheme.colorScheme.onTertiary.copy(alpha = .12f), + containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.8f), + disabledContainerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = .12f), + ), + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(52.dp), + ) { + Icon( + painter = painterResource(R.drawable.ic_github), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = stringResource(R.string.github)) + } + } + } +} + +private fun Context.appVersion(): String { + val packageInfo = packageManager.getPackageInfo(packageName, 0) + + @Suppress("DEPRECATION") + val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode + } else { + packageInfo.versionCode + } + + return "${packageInfo.versionName} ($versionCode)" +} + +private fun Context.appIcon(): ImageBitmap? { + return packageManager.getApplicationInfo(packageName, 0).loadIcon(packageManager)?.toBitmapOrNull()?.asImageBitmap() +} + +internal fun UriHandler.openUriOrShowToast(uri: String, context: Context) { + try { + openUri(uri = uri) + } catch (e: Exception) { + Toast.makeText(context, context.getString(R.string.error_opening_link), Toast.LENGTH_SHORT).show() } } diff --git a/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/screens/about/LibrariesScreen.kt b/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/screens/about/LibrariesScreen.kt new file mode 100644 index 000000000..100fb0094 --- /dev/null +++ b/feature/settings/src/main/java/dev/anilbeesetti/nextplayer/settings/screens/about/LibrariesScreen.kt @@ -0,0 +1,83 @@ +package dev.anilbeesetti.nextplayer.settings.screens.about + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer +import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults +import dev.anilbeesetti.nextplayer.core.ui.R +import dev.anilbeesetti.nextplayer.core.ui.components.NextTopAppBar +import dev.anilbeesetti.nextplayer.core.ui.designsystem.NextIcons + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LibrariesScreen( + onNavigateUp: () -> Unit, +) { + val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior() + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + + Scaffold( + modifier = Modifier + .nestedScroll(scrollBehaviour.nestedScrollConnection), + topBar = { + NextTopAppBar( + title = stringResource(id = R.string.libraries), + scrollBehavior = scrollBehaviour, + navigationIcon = { + IconButton( + onClick = onNavigateUp, + modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Start)), + ) { + Icon( + imageVector = NextIcons.ArrowBack, + contentDescription = stringResource(id = R.string.navigate_up), + ) + } + }, + ) + }, + ) { innerPadding -> + LibrariesContainer( + contentPadding = innerPadding, + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), + colors = LibraryDefaults.libraryColors( + backgroundColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + badgeBackgroundColor = MaterialTheme.colorScheme.secondaryContainer, + badgeContentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ), + padding = LibraryDefaults.libraryPadding( + namePadding = PaddingValues(bottom = 4.dp), + badgeContentPadding = PaddingValues(4.dp), + ), + onLibraryClick = { library -> + library.website?.let { + if (it.isNotEmpty()) { + uriHandler.openUriOrShowToast(uri = it, context = context) + } + } + }, + ) + } +}