From cbac36d98995dcece7bf14ee42b28522ee8b7454 Mon Sep 17 00:00:00 2001 From: Marco Gomiero Date: Tue, 24 Oct 2023 23:18:36 +0200 Subject: [PATCH] Fix WindowSize and JDK/proguard issue --- .../com/prof18/feedflow/MainActivity.kt | 17 +- .../feedflow/home/HomeScreen.android.kt | 6 +- desktopApp/build.gradle.kts | 1 - .../kotlin/com/prof18/feedflow/Main.kt | 4 +- .../feedflow/home/HomeScreen.desktop.kt | 10 +- .../ui/components/NewVersionBanner.kt | 11 +- .../prof18/feedflow/utils/WindowSizeClass.kt | 346 ++++++++++++++++++ gradle/libs.versions.toml | 4 +- 8 files changed, 379 insertions(+), 20 deletions(-) create mode 100644 desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/utils/WindowSizeClass.kt diff --git a/androidApp/src/main/kotlin/com/prof18/feedflow/MainActivity.kt b/androidApp/src/main/kotlin/com/prof18/feedflow/MainActivity.kt index ddd4da5c..7fd9f81c 100644 --- a/androidApp/src/main/kotlin/com/prof18/feedflow/MainActivity.kt +++ b/androidApp/src/main/kotlin/com/prof18/feedflow/MainActivity.kt @@ -11,6 +11,9 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier @@ -29,6 +32,7 @@ import com.prof18.feedflow.settings.about.LicensesScreen import com.prof18.feedflow.settings.importexport.ImportExportScreen class MainActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -44,6 +48,8 @@ class MainActivity : ComponentActivity() { onDispose {} } + val windowSize = calculateWindowSizeClass(this@MainActivity) + FeedFlowTheme { val navController = rememberNavController() @@ -51,7 +57,10 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, ) { - FeedFlowNavigation(navController) + FeedFlowNavigation( + windowSizeClass = windowSize, + navController = navController, + ) } } } @@ -59,7 +68,10 @@ class MainActivity : ComponentActivity() { @Suppress("LongMethod") @Composable - private fun FeedFlowNavigation(navController: NavHostController) { + private fun FeedFlowNavigation( + windowSizeClass: WindowSizeClass, + navController: NavHostController, + ) { NavHost( navController = navController, startDestination = Screen.Home.name, @@ -70,6 +82,7 @@ class MainActivity : ComponentActivity() { ) { composable(Screen.Home.name) { HomeScreen( + windowSizeClass = windowSizeClass, onSettingsButtonClicked = { navController.navigate(Screen.Settings.name) }, diff --git a/androidApp/src/main/kotlin/com/prof18/feedflow/home/HomeScreen.android.kt b/androidApp/src/main/kotlin/com/prof18/feedflow/home/HomeScreen.android.kt index cdd847a2..fa22ce00 100644 --- a/androidApp/src/main/kotlin/com/prof18/feedflow/home/HomeScreen.android.kt +++ b/androidApp/src/main/kotlin/com/prof18/feedflow/home/HomeScreen.android.kt @@ -30,8 +30,8 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -74,6 +74,7 @@ import org.koin.compose.koinInject @Suppress("LongMethod") @Composable internal fun HomeScreen( + windowSizeClass: WindowSizeClass, onSettingsButtonClicked: () -> Unit, ) { val homeViewModel = koinViewModel() @@ -103,14 +104,13 @@ internal fun HomeScreen( val scope = rememberCoroutineScope() - val windowSize = calculateWindowSizeClass() val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) var isDrawerMenuFullVisible by remember { mutableStateOf(true) } - when (windowSize.widthSizeClass) { + when (windowSizeClass.widthSizeClass) { WindowWidthSizeClass.Compact -> { ModalNavigationDrawer( drawerContent = { diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 67afca71..cb1692f0 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -36,7 +36,6 @@ kotlin { implementation(libs.bundles.about.libraries) implementation(libs.jsoup) implementation(libs.slf4j.nop) - implementation(libs.material.window.size) } } val jvmTest by getting diff --git a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/Main.kt b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/Main.kt index c469626a..13ecd919 100644 --- a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/Main.kt @@ -12,7 +12,6 @@ import androidx.compose.material.Surface import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState @@ -207,7 +206,6 @@ fun main() = application { } } -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable private fun MainContent( rootComponentContext: DefaultComponentContext, @@ -237,6 +235,7 @@ private fun MainContent( Column { if (newVersionState is NewVersionState.NewVersion) { NewVersionBanner( + window = window, onDownloadLinkClick = { openInBrowser(newVersionState.downloadLink) }, @@ -245,6 +244,7 @@ private fun MainContent( } HomeScreen( + window = window, paddingValues = paddingValues, homeViewModel = homeViewModel, snackbarHostState = snackbarHostState, diff --git a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/home/HomeScreen.desktop.kt b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/home/HomeScreen.desktop.kt index 6f922aa9..971a98ea 100644 --- a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/home/HomeScreen.desktop.kt +++ b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/home/HomeScreen.desktop.kt @@ -32,9 +32,6 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDrawerState -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -45,6 +42,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposeWindow import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.prof18.feedflow.MR @@ -62,12 +60,14 @@ import com.prof18.feedflow.ui.home.components.FeedItemView import com.prof18.feedflow.ui.home.components.FeedList import com.prof18.feedflow.ui.home.components.NoFeedsSourceView import com.prof18.feedflow.ui.style.Spacing +import com.prof18.feedflow.utils.WindowWidthSizeClass +import com.prof18.feedflow.utils.calculateWindowSizeClass import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable internal fun HomeScreen( + window: ComposeWindow, paddingValues: PaddingValues, homeViewModel: HomeViewModel, snackbarHostState: SnackbarHostState, @@ -88,7 +88,7 @@ internal fun HomeScreen( } } - val windowSize = calculateWindowSizeClass() + val windowSize = calculateWindowSizeClass(window) when (windowSize.widthSizeClass) { WindowWidthSizeClass.Compact -> { diff --git a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/ui/components/NewVersionBanner.kt b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/ui/components/NewVersionBanner.kt index cd0cf0fc..ee4d3326 100644 --- a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/ui/components/NewVersionBanner.kt +++ b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/ui/components/NewVersionBanner.kt @@ -13,12 +13,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposeWindow import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -27,15 +25,17 @@ import androidx.compose.ui.text.withStyle import com.prof18.feedflow.MR import com.prof18.feedflow.ui.style.FeedFlowTheme import com.prof18.feedflow.ui.style.Spacing +import com.prof18.feedflow.utils.WindowWidthSizeClass +import com.prof18.feedflow.utils.calculateWindowSizeClass import dev.icerock.moko.resources.compose.stringResource -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable internal fun NewVersionBanner( + window: ComposeWindow, onDownloadLinkClick: () -> Unit, onCloseClick: () -> Unit, ) { - val windowSize = calculateWindowSizeClass() + val windowSize = calculateWindowSizeClass(window) when (windowSize.widthSizeClass) { WindowWidthSizeClass.Compact -> { @@ -167,6 +167,7 @@ private fun AnnotatedClickableText( private fun NewVersionBannerPreview() { FeedFlowTheme { NewVersionBanner( + window = ComposeWindow(), onDownloadLinkClick = {}, onCloseClick = {}, ) diff --git a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/utils/WindowSizeClass.kt b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/utils/WindowSizeClass.kt new file mode 100644 index 00000000..02ceb21e --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/utils/WindowSizeClass.kt @@ -0,0 +1,346 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.prof18.feedflow.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.awt.ComposeWindow +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import java.awt.Component +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent + +/** + * Calculates the window's [WindowSizeClass]. + * + * A new [WindowSizeClass] will be returned whenever a change causes the width or + * height of the window to cross a breakpoint, such as when the device is rotated or the window + * is resized. + */ +@Composable +fun calculateWindowSizeClass(window: ComposeWindow): WindowSizeClass { + var windowSizeClass by remember(window) { + mutableStateOf(WindowSizeClass.calculateFromSize(window.getDpSize())) + } + + // Add a listener and listen for componentResized events + DisposableEffect(window) { + val listener = object : ComponentAdapter() { + override fun componentResized(event: ComponentEvent) { + windowSizeClass = WindowSizeClass.calculateFromSize(window.getDpSize()) + } + } + + window.addComponentListener(listener) + + onDispose { + window.removeComponentListener(listener) + } + } + + return windowSizeClass +} + +private fun Component.getDpSize(): DpSize = DpSize(width.dp, height.dp) + +/** + * Window size classes are a set of opinionated viewport breakpoints to design, develop, and test + * responsive application layouts against. + * + * WindowSizeClass contains a [WindowWidthSizeClass] and [WindowHeightSizeClass], representing the + * window size classes for this window's width and height respectively. + * + * See [calculateWindowSizeClass] to calculate the WindowSizeClass. + * + * @property widthSizeClass width-based window size class ([WindowWidthSizeClass]) + * @property heightSizeClass height-based window size class ([WindowHeightSizeClass]) + */ +@Immutable +class WindowSizeClass private constructor( + val widthSizeClass: WindowWidthSizeClass, + val heightSizeClass: WindowHeightSizeClass, +) { + companion object { + internal fun calculateFromSize(size: DpSize): WindowSizeClass { + val windowWidthSizeClass = WindowWidthSizeClass.fromWidth(size.width) + val windowHeightSizeClass = WindowHeightSizeClass.fromHeight(size.height) + return WindowSizeClass(windowWidthSizeClass, windowHeightSizeClass) + } + + /** + * Calculates the best matched [WindowSizeClass] for a given [size] and [Density] according + * to the provided [supportedWidthSizeClasses] and [supportedHeightSizeClasses]. + * + * @param size of the window + * @param density of the window + * @param supportedWidthSizeClasses the set of width size classes that are supported + * @param supportedHeightSizeClasses the set of height size classes that are supported + * @return [WindowSizeClass] corresponding to the given width and height + */ + fun calculateFromSize( + size: Size, + density: Density, + supportedWidthSizeClasses: Set = + WindowWidthSizeClass.DefaultSizeClasses, + supportedHeightSizeClasses: Set = + WindowHeightSizeClass.DefaultSizeClasses, + ): WindowSizeClass { + val windowWidthSizeClass = + WindowWidthSizeClass.fromWidth(size.width, density, supportedWidthSizeClasses) + val windowHeightSizeClass = + WindowHeightSizeClass.fromHeight(size.height, density, supportedHeightSizeClasses) + return WindowSizeClass(windowWidthSizeClass, windowHeightSizeClass) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as WindowSizeClass + + if (widthSizeClass != other.widthSizeClass) return false + if (heightSizeClass != other.heightSizeClass) return false + + return true + } + + override fun hashCode(): Int { + var result = widthSizeClass.hashCode() + result = 31 * result + heightSizeClass.hashCode() + return result + } + + override fun toString() = "WindowSizeClass($widthSizeClass, $heightSizeClass)" +} + +/** + * Width-based window size class. + * + * A window size class represents a breakpoint that can be used to build responsive layouts. Each + * window size class breakpoint represents a majority case for typical device scenarios so your + * layouts will work well on most devices and configurations. + * + */ +@Suppress("UnusedPrivateProperty") +@Immutable +@JvmInline +value class WindowWidthSizeClass private constructor(private val value: Int) : + Comparable { + + override operator fun compareTo(other: WindowWidthSizeClass) = + breakpoint().compareTo(other.breakpoint()) + + override fun toString(): String { + return "WindowWidthSizeClass." + when (this) { + Compact -> "Compact" + Medium -> "Medium" + Expanded -> "Expanded" + else -> "" + } + } + + companion object { + /** Represents the majority of phones in portrait. */ + val Compact = WindowWidthSizeClass(0) + + /** + * Represents the majority of tablets in portrait and large unfolded inner displays in + * portrait. + */ + val Medium = WindowWidthSizeClass(1) + + /** + * Represents the majority of tablets in landscape and large unfolded inner displays in + * landscape. + */ + val Expanded = WindowWidthSizeClass(2) + + /** + * The default set of size classes that includes [Compact], [Medium], and [Expanded] size + * classes. Should never expand to ensure behavioral consistency. + */ + val DefaultSizeClasses = setOf(Compact, Medium, Expanded) + + /** + * The standard set of size classes. It's supposed to include all size classes and will be + * expanded whenever a new size class is defined. By default + * [WindowSizeClass.calculateFromSize] will only return size classes in [DefaultSizeClasses] + * in order to avoid behavioral changes when new size classes are added. You can opt in to + * support all available size classes by doing: + * ``` + * WindowSizeClass.calculateFromSize( + * size = size, + * density = density, + * supportedWidthSizeClasses = WindowWidthSizeClass.StandardSizeClasses, + * supportedHeightSizeClasses = WindowHeightSizeClass.StandardSizeClasses + * ) + * ``` + */ + val StandardSizeClasses get() = DefaultSizeClasses + + private fun WindowWidthSizeClass.breakpoint(): Dp { + return when { + this == Expanded -> 840.dp + this == Medium -> 600.dp + else -> 0.dp + } + } + + /** Calculates the [WindowWidthSizeClass] for a given [width] */ + internal fun fromWidth(width: Dp): WindowWidthSizeClass { + return fromWidth( + with(defaultDensity) { width.toPx() }, + defaultDensity, + DefaultSizeClasses, + ) + } + + /** + * Calculates the best matched [WindowWidthSizeClass] for a given [width] in Pixels and + * a given [Density] from [supportedSizeClasses]. + */ + internal fun fromWidth( + width: Float, + density: Density, + supportedSizeClasses: Set, + ): WindowWidthSizeClass { + require(width >= 0) { "Width must not be negative" } + require(supportedSizeClasses.isNotEmpty()) { "Must support at least one size class" } + val sortedSizeClasses = supportedSizeClasses.sortedDescending() + // Find the largest supported size class that matches the width + sortedSizeClasses.forEach { + if (width >= with(density) { it.breakpoint().toPx() }) { + return it + } + } + // If none of the size classes matches, return the smallest one. + return sortedSizeClasses.last() + } + } +} + +/** + * Height-based window size class. + * + * A window size class represents a breakpoint that can be used to build responsive layouts. Each + * window size class breakpoint represents a majority case for typical device scenarios so your + * layouts will work well on most devices and configurations. + * + */ +@Suppress("UnusedPrivateProperty") +@Immutable +@JvmInline +value class WindowHeightSizeClass private constructor(private val value: Int) : + Comparable { + + override operator fun compareTo(other: WindowHeightSizeClass) = + breakpoint().compareTo(other.breakpoint()) + + override fun toString(): String { + return "WindowHeightSizeClass." + when (this) { + Compact -> "Compact" + Medium -> "Medium" + Expanded -> "Expanded" + else -> "" + } + } + + companion object { + /** Represents the majority of phones in landscape */ + val Compact = WindowHeightSizeClass(0) + + /** Represents the majority of tablets in landscape and majority of phones in portrait */ + val Medium = WindowHeightSizeClass(1) + + /** Represents the majority of tablets in portrait */ + val Expanded = WindowHeightSizeClass(2) + + /** + * The default set of size classes that includes [Compact], [Medium], and [Expanded] size + * classes. Should never expand to ensure behavioral consistency. + */ + val DefaultSizeClasses = setOf(Compact, Medium, Expanded) + + /** + * The standard set of size classes. It's supposed to include all size classes and will be + * expanded whenever a new size class is defined. By default + * [WindowSizeClass.calculateFromSize] will only return size classes in [DefaultSizeClasses] + * in order to avoid behavioral changes when new size classes are added. You can opt in to + * support all available size classes by doing: + * ``` + * WindowSizeClass.calculateFromSize( + * size = size, + * density = density, + * supportedWidthSizeClasses = WindowWidthSizeClass.StandardSizeClasses, + * supportedHeightSizeClasses = WindowHeightSizeClass.StandardSizeClasses + * ) + * ``` + */ + val StandardSizeClasses get() = DefaultSizeClasses + + private fun WindowHeightSizeClass.breakpoint(): Dp { + return when { + this == Expanded -> 900.dp + this == Medium -> 480.dp + else -> 0.dp + } + } + + /** Calculates the [WindowHeightSizeClass] for a given [height] */ + internal fun fromHeight(height: Dp): WindowHeightSizeClass { + return fromHeight( + with(defaultDensity) { height.toPx() }, + defaultDensity, + DefaultSizeClasses, + ) + } + + /** + * Calculates the best matched [WindowHeightSizeClass] for a given [height] in Pixels and + * a given [Density] from [supportedSizeClasses]. + */ + internal fun fromHeight( + height: Float, + density: Density, + supportedSizeClasses: Set, + ): WindowHeightSizeClass { + require(height >= 0) { "Width must not be negative" } + require(supportedSizeClasses.isNotEmpty()) { "Must support at least one size class" } + val sortedSizeClasses = supportedSizeClasses.sortedDescending() + // Find the largest supported size class that matches the width + sortedSizeClasses.forEach { + if (height >= with(density) { it.breakpoint().toPx() }) { + return it + } + } + // If none of the size classes matches, return the smallest one. + return sortedSizeClasses.last() + } + } +} + +private val defaultDensity = Density(1F, 1F) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92a4a55e..b2bc5a85 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,7 +47,7 @@ crashlytics-plugin = "2.9.7" google-services = "4.3.15" crashk-ios = "0.8.2" sentry = "6.25.2" -material-window-size = "0.3.0" +material-window-size = "1.1.2" [libraries] about-libraries-compose = { module = "com.mikepenz:aboutlibraries-compose", version.ref="about-libraries" } @@ -102,7 +102,7 @@ moko-resourcesCompose = { module = "dev.icerock.moko:resources-compose", version firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "crashlytics" } crashk-ios = { module = "co.touchlab.crashkios:crashlytics", version.ref = "crashk-ios" } sentry = { module = "io.sentry:sentry", version.ref = "sentry" } -material-window-size = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "material-window-size" } +material-window-size = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material-window-size" } [bundles] androidx-test = ["androidx-test-core", "androidx-test-ext-junit", "androidx-test-rules", "androidx-test-runner"]