diff --git a/app/build.gradle b/app/build.gradle index 28a208195..5502025a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,6 +92,7 @@ android { buildFeatures { viewBinding true + compose true } packagingOptions { @@ -103,6 +104,10 @@ android { 'META-INF/COPYRIGHT'] } } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.3" + } } ext { @@ -284,6 +289,12 @@ dependencies { // Date and time formatting implementation "org.ocpsoft.prettytime:prettytime:5.0.7.Final" + // Jetpack Compose + implementation(platform('androidx.compose:compose-bom:2024.02.01')) + implementation 'androidx.compose.material3:material3' + implementation 'androidx.activity:activity-compose' + implementation 'androidx.compose.ui:ui-tooling-preview' + /** Debugging **/ // Memory leak detection debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" @@ -293,6 +304,9 @@ dependencies { debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" + // Jetpack Compose + debugImplementation 'androidx.compose.ui:ui-tooling' + /** Testing **/ testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:5.6.0' diff --git a/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt b/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt new file mode 100644 index 000000000..b788932a2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt @@ -0,0 +1,137 @@ +package org.schabi.newpipe.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import org.schabi.newpipe.R +import org.schabi.newpipe.ui.theme.AppTheme +import org.schabi.newpipe.ui.theme.SizeTokens + +@Composable +fun TextAction(text: String, modifier: Modifier = Modifier) { + Text(text = text, color = MaterialTheme.colorScheme.onSurface, modifier = modifier) +} + +@Composable +fun NavigationIcon() { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", + modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall) + ) +} + +@Composable +fun SearchSuggestionItem(text: String) { + // TODO: Add more components here to display all the required details of a search suggestion item. + Text(text = text) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Toolbar( + title: String, + modifier: Modifier = Modifier, + hasNavigationIcon: Boolean = true, + hasSearch: Boolean = false, + onSearchQueryChange: ((String) -> List)? = null, + actions: @Composable RowScope.() -> Unit = {} +) { + var isSearchActive by remember { mutableStateOf(false) } + var query by remember { mutableStateOf("") } + + Column { + TopAppBar( + title = { Text(text = title) }, + modifier = modifier, + navigationIcon = { if (hasNavigationIcon) NavigationIcon() }, + actions = { + actions() + if (hasSearch) { + IconButton(onClick = { isSearchActive = true }) { + Icon( + painterResource(id = R.drawable.ic_search), + contentDescription = stringResource(id = R.string.search), + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + } + ) + if (isSearchActive) { + SearchBar( + query = query, + onQueryChange = { query = it }, + onSearch = {}, + placeholder = { + Text(text = stringResource(id = R.string.search)) + }, + active = true, + onActiveChange = { + isSearchActive = it + }, + colors = SearchBarDefaults.colors( + containerColor = MaterialTheme.colorScheme.background, + inputFieldColors = SearchBarDefaults.inputFieldColors( + focusedTextColor = MaterialTheme.colorScheme.onBackground, + unfocusedTextColor = MaterialTheme.colorScheme.onBackground + ) + ) + ) { + onSearchQueryChange?.invoke(query)?.takeIf { it.isNotEmpty() } + ?.map { suggestionText -> SearchSuggestionItem(text = suggestionText) } + ?: run { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Column { + Text(text = "╰(°●°╰)") + Text(text = stringResource(id = R.string.search_no_results)) + } + } + } + } + } + } +} + +@Preview +@Composable +fun ToolbarPreview() { + AppTheme { + Toolbar( + title = "Title", + hasSearch = true, + onSearchQueryChange = { emptyList() }, + actions = { + TextAction(text = "Action1") + TextAction(text = "Action2") + } + ) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt b/app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt new file mode 100644 index 000000000..b61906ebe --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt @@ -0,0 +1,63 @@ +package org.schabi.newpipe.ui.theme + +import androidx.compose.ui.graphics.Color + +val md_theme_light_primary = Color(0xFFBB171C) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFFFDAD6) +val md_theme_light_onPrimaryContainer = Color(0xFF410002) +val md_theme_light_secondary = Color(0xFF984061) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFFFD9E2) +val md_theme_light_onSecondaryContainer = Color(0xFF3E001D) +val md_theme_light_tertiary = Color(0xFF006874) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFF97F0FF) +val md_theme_light_onTertiaryContainer = Color(0xFF001F24) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFEEEEEE) +val md_theme_light_onBackground = Color(0xFF1B1B1B) +val md_theme_light_surface = Color(0xFFE53835) +val md_theme_light_onSurface = Color(0xFFFFFFFF) +val md_theme_light_surfaceVariant = Color(0xFFF5DDDB) +val md_theme_light_onSurfaceVariant = Color(0xFF534341) +val md_theme_light_outline = Color(0xFF857371) +val md_theme_light_inverseOnSurface = Color(0xFFD6F6FF) +val md_theme_light_inverseSurface = Color(0xFF00363F) +val md_theme_light_inversePrimary = Color(0xFFFFB4AC) +val md_theme_light_surfaceTint = Color(0xFFBB171C) +val md_theme_light_outlineVariant = Color(0xFFD8C2BF) +val md_theme_light_scrim = Color(0xFF000000) + +val md_theme_dark_primary = Color(0xFFFFB4AC) +val md_theme_dark_onPrimary = Color(0xFF690006) +val md_theme_dark_primaryContainer = Color(0xFF93000D) +val md_theme_dark_onPrimaryContainer = Color(0xFFFFDAD6) +val md_theme_dark_secondary = Color(0xFFFFB1C8) +val md_theme_dark_onSecondary = Color(0xFF5E1133) +val md_theme_dark_secondaryContainer = Color(0xFF7B2949) +val md_theme_dark_onSecondaryContainer = Color(0xFFFFD9E2) +val md_theme_dark_tertiary = Color(0xFF4FD8EB) +val md_theme_dark_onTertiary = Color(0xFF00363D) +val md_theme_dark_tertiaryContainer = Color(0xFF004F58) +val md_theme_dark_onTertiaryContainer = Color(0xFF97F0FF) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF212121) +val md_theme_dark_onBackground = Color(0xFFFFFFFF) +val md_theme_dark_surface = Color(0xFF992521) +val md_theme_dark_onSurface = Color(0xFFFFFFFF) +val md_theme_dark_surfaceVariant = Color(0xFF534341) +val md_theme_dark_onSurfaceVariant = Color(0xFFD8C2BF) +val md_theme_dark_outline = Color(0xFFA08C8A) +val md_theme_dark_inverseOnSurface = Color(0xFF001F25) +val md_theme_dark_inverseSurface = Color(0xFFA6EEFF) +val md_theme_dark_inversePrimary = Color(0xFFBB171C) +val md_theme_dark_surfaceTint = Color(0xFFFFB4AC) +val md_theme_dark_outlineVariant = Color(0xFF534341) +val md_theme_dark_scrim = Color(0xFF000000) diff --git a/app/src/main/java/org/schabi/newpipe/ui/theme/SizeTokens.kt b/app/src/main/java/org/schabi/newpipe/ui/theme/SizeTokens.kt new file mode 100644 index 000000000..d8104d7ae --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/theme/SizeTokens.kt @@ -0,0 +1,13 @@ +package org.schabi.newpipe.ui.theme + +import androidx.compose.ui.unit.dp + +internal object SizeTokens { + val SpacingExtraSmall = 4.dp + val SpacingSmall = 8.dp + val SpacingMedium = 16.dp + val SpacingLarge = 24.dp + val SpacingExtraLarge = 32.dp + + val SpaceMinSize = 44.dp // Minimum tappable size required for accessibility +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt b/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt new file mode 100644 index 000000000..846794d72 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt @@ -0,0 +1,79 @@ +package org.schabi.newpipe.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, +) + +private val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, +) + +@Composable +fun AppTheme(useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = if (useDarkTheme) DarkColors else LightColors, + content = content + ) +}