diff --git a/.circleci/config.yml b/.circleci/config.yml index 67fabf9b..e8e37921 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,13 +3,13 @@ version: 2.1 orbs: - slack: circleci/slack@4.13.3 - gh: circleci/github-cli@2.3.0 + slack: circleci/slack@5.1.1 + gh: circleci/github-cli@2.5.0 executors: android: docker: - - image: cimg/android:2024.01 + - image: cimg/android:2024.11 commands: check_is_skipping_vrt: @@ -72,7 +72,7 @@ commands: command: | mkdir -p ./temp/zip if [ $IS_EXIST_SCREENSHOTS = "true" ]; then - zip -r ./temp/zip/screenshots.zip ./app/build/outputs/roborazzi + zip -r ./temp/zip/screenshots.zip ./screenshots fi - save_cache: paths: @@ -176,7 +176,7 @@ commands: git checkout --orphan screenshots_$CIRCLE_BRANCH git rm --cached -rf . - add_files=$(find . -type f -path "./app/build/outputs/roborazzi/*") + add_files=$(find . -type f -path "./screenshots/*") for file in $add_files; do git add -f $file done @@ -200,13 +200,13 @@ commands: if [ $IS_SKIPPING_VRT = "false" ]; then git push origin --delete compare_$CIRCLE_BRANCH || true - fileSize=$(echo $(find ./app/build/outputs/roborazzi -type f | grep -e '.*_compare.png' | wc -l | sed -e 's/ //g')) + fileSize=$(echo $(find ./screenshots/compare -type f | grep -e '.*_compare.webp' | wc -l | sed -e 's/ //g')) echo "fileSize: $fileSize" if [ $fileSize -ne 0 ]; then git checkout --orphan compare_$CIRCLE_BRANCH git rm --cached -rf . - add_files=$(find . -type f -path "./app/build/outputs/roborazzi/*" -name "*_compare.png") + add_files=$(find . -type f -path "./screenshots/compare/*" -name "*_compare.webp") for file in $add_files; do git add -f $file done @@ -229,7 +229,7 @@ commands: echo "| File name | Image |" >> comment echo "|-------|-------|" >> comment - files=$(find . -type f -path "./app/build/outputs/roborazzi/*" -name "*_compare.png") + files=$(find . -type f -path "./screenshots/compare/*" -name "*_compare.webp") for file in $files; do fileName=$(basename "$file" | sed -r 's/(.{20})/\1
/g') echo "| [$fileName](https://github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/blob/compare_$CIRCLE_BRANCH/$file) | ![](https://github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/blob/compare_$CIRCLE_BRANCH/$file?raw=true) |" >> comment diff --git a/.github/actions/incoming-webhook/action.yml b/.github/actions/incoming-webhook/action.yml index 13d0d5ae..64e3e1d6 100644 --- a/.github/actions/incoming-webhook/action.yml +++ b/.github/actions/incoming-webhook/action.yml @@ -15,7 +15,7 @@ runs: # https://github.com/slackapi/slack-github-action - name: Send GitHub Action trigger data to Slack workflow id: slack - uses: slackapi/slack-github-action@v1.26.0 + uses: slackapi/slack-github-action@v2.0.0 with: payload: | { diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 7a411be1..fe9df7dd 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -23,4 +23,4 @@ runs: cache: gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 \ No newline at end of file + uses: gradle/actions/setup-gradle@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 417ffb90..d8dc418a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ output.json *.jks *.keystore keystore.properties +/screenshots/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 4bec4ea8..7643783a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index a55e7a17..6e6eec11 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ + \ No newline at end of file diff --git a/Gemfile b/Gemfile index f99b8ef5..7782577e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" git_source(:github) { |repo_name| "https://github.com/kosenda/SimpleCompoundInterestCalculation" } -gem 'danger', '~> 9.4.0' +gem 'danger', '~> 9.5.0' gem "danger-checkstyle_format" gem "danger-jacoco" gem 'danger-android_lint' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 8079cf1a..79953da7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,10 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ansi (1.5.0) + ast (1.1.0) base64 (0.2.0) claide (1.1.0) claide-plugins (0.9.2) @@ -12,7 +14,8 @@ GEM colored2 (3.1.2) cork (0.3.0) colored2 (~> 3.1) - danger (9.4.3) + danger (9.5.1) + base64 (~> 0.2) claide (~> 1.0) claide-plugins (>= 0.9.2) colored2 (~> 3.1) @@ -22,9 +25,12 @@ GEM git (~> 1.13) kramdown (~> 2.3) kramdown-parser-gfm (~> 1.0) - no_proxy_fix octokit (>= 4.0) + pstore (~> 0.1) terminal-table (>= 1, < 4) + danger-android_lint (0.0.12) + danger-plugin-api (~> 1.0) + oga danger-checkstyle_format (0.1.1) danger-plugin-api (~> 1.0) ox (~> 2.0) @@ -33,11 +39,12 @@ GEM nokogiri-happymapper (~> 0.6) danger-plugin-api (1.0.0) danger (> 2.0) - faraday (2.9.0) - faraday-net_http (>= 2.0, < 3.2) + faraday (2.11.0) + faraday-net_http (>= 2.0, < 3.4) + logger faraday-http-cache (2.5.1) faraday (>= 0.8) - faraday-net_http (3.1.0) + faraday-net_http (3.3.0) net-http git (1.19.1) addressable (~> 2.8) @@ -46,10 +53,10 @@ GEM rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) + logger (1.6.1) nap (1.1.0) net-http (0.4.1) uri - no_proxy_fix (0.1.2) nokogiri (1.16.2-arm64-darwin) racc (~> 1.4) nokogiri (1.16.2-x86_64-linux) @@ -60,26 +67,34 @@ GEM base64 faraday (>= 1, < 3) sawyer (~> 0.9) + oga (0.3.4) + ast + ruby-ll (~> 2.1) open4 (1.3.4) ox (2.14.17) - public_suffix (5.0.4) + pstore (0.1.3) + public_suffix (5.1.1) racc (1.7.3) rchardet (1.8.0) - rexml (3.2.6) + rexml (3.3.9) + ruby-ll (2.1.3) + ansi + ast sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - unicode-display_width (2.5.0) - uri (0.13.0) + unicode-display_width (2.6.0) + uri (0.13.1) PLATFORMS arm64-darwin-22 x86_64-linux DEPENDENCIES - danger (~> 9.4.0) + danger (~> 9.5.0) + danger-android_lint danger-checkstyle_format danger-jacoco diff --git a/README.md b/README.md index e338a6b1..44e25b89 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ https://github.com/kosenda/hiragana-converter/blob/develop/REFERENCE.md |App Update|In App Update| |App Review|In App Review| |Coil|Image loading library| +|ComposablePreviewScanner|Help auto-generate screenshot tests| |Crashlytics|Firebase crashlytics| |Danger|Automatic review| |Hilt|Dependency Injection| @@ -55,7 +56,6 @@ https://github.com/kosenda/hiragana-converter/blob/develop/REFERENCE.md |Roborazzi|Make JVM Android Integration Test Visible| |Room|Database| |Secrets gradle plugin|Reading API keys from `local.properties`| -|Showkase|auto-generates a browser for Jetpack Compose UI| |Timber|Log output library| |Truth|Assertions used in testing| |Turbine|testing library for kotlinx.coroutines Flow| diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2291cb1b..7ccf8a41 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi import com.google.firebase.perf.plugin.FirebasePerfExtension -import ksnd.hiraganaconverter.kotlinOptions +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.io.FileInputStream import java.util.Properties @@ -22,21 +23,23 @@ plugins { android { namespace = "ksnd.hiraganaconverter" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "ksnd.hiraganaconverter" minSdk = 26 - targetSdk = 34 - versionCode = 44 - versionName = "1.33" + targetSdk = 35 + versionCode = 45 + versionName = "1.34" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } } androidResources { generateLocaleConfig = true @@ -91,9 +94,6 @@ android { buildConfig = true compose = true } - composeCompiler { - enableStrongSkippingMode = true - } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" @@ -102,6 +102,18 @@ android { testOptions { unitTests.isIncludeAndroidResources = true unitTests.isReturnDefaultValues = true + unitTests.all { + it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + } + } +} + +roborazzi { + @OptIn(ExperimentalRoborazziApi::class) + generateComposePreviewRobolectricTests { + enable = true + testerQualifiedClassName = "ksnd.hiraganaconverter.RoborazziComposePreviewTest" + packages = listOf("ksnd.hiraganaconverter") } } @@ -130,6 +142,10 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.lifecycle.runtime.compose) + // Supported: Workaround for AGP not merging test manifest + // ref: https://github.com/robolectric/robolectric/pull/4736 + debugImplementation(libs.androidx.compose.ui.test.manifest) + // Lottie implementation(libs.lottie) @@ -151,16 +167,18 @@ dependencies { // Navigation implementation(libs.androidx.navigation.compose) - // Showkase - debugImplementation(libs.showkase) - implementation(libs.showkase.annotation) - kspDebug(libs.showkase.processor) - // kotlinx serialization implementation(libs.kotlinx.serialization.json) // AboutLibraries implementation(libs.aboutLibraries) + + // Roborazzi (for ComposablePreviewScanner) + testImplementation(libs.roborazzi.compose.preview.scanner.support) + testImplementation(libs.junit) + testImplementation(libs.robolectric) + testImplementation(libs.composable.preview.scanner) + testImplementation(libs.webp.image.io) } tasks.withType().configureEach { diff --git a/app/src/main/java/ksnd/hiraganaconverter/ShowkaseRootModule.kt b/app/src/main/java/ksnd/hiraganaconverter/ShowkaseRootModule.kt deleted file mode 100644 index d994da28..00000000 --- a/app/src/main/java/ksnd/hiraganaconverter/ShowkaseRootModule.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ksnd.hiraganaconverter - -import com.airbnb.android.showkase.annotation.ShowkaseRoot -import com.airbnb.android.showkase.annotation.ShowkaseRootModule - -@ShowkaseRoot -class ShowkaseRootModule : ShowkaseRootModule diff --git a/app/src/main/java/ksnd/hiraganaconverter/view/RequestReviewDialog.kt b/app/src/main/java/ksnd/hiraganaconverter/view/RequestReviewDialog.kt index f64d7c80..a57ecdd6 100644 --- a/app/src/main/java/ksnd/hiraganaconverter/view/RequestReviewDialog.kt +++ b/app/src/main/java/ksnd/hiraganaconverter/view/RequestReviewDialog.kt @@ -7,6 +7,8 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import ksnd.hiraganaconverter.core.resource.R +import ksnd.hiraganaconverter.core.ui.preview.UiModePreview +import ksnd.hiraganaconverter.core.ui.theme.HiraganaConverterTheme @Composable fun RequestReviewDialog(onLater: () -> Unit, onOk: () -> Unit) { @@ -44,15 +46,13 @@ fun RequestReviewDialog(onLater: () -> Unit, onOk: () -> Unit) { ) } -// FIXME: Skip in ShowkaseComposable does not work. -// @UiModePreview -// @Composable -// @ShowkaseComposable(skip = true) -// fun PreviewRequestReviewDialog() { -// HiraganaConverterTheme { -// RequestReviewDialog( -// onLater = {}, -// onOk = {}, -// ) -// } -// } +@UiModePreview +@Composable +private fun PreviewRequestReviewDialog() { + HiraganaConverterTheme { + RequestReviewDialog( + onLater = {}, + onOk = {}, + ) + } +} diff --git a/app/src/test/java/ksnd/hiraganaconverter/PreviewTest.kt b/app/src/test/java/ksnd/hiraganaconverter/PreviewTest.kt deleted file mode 100644 index c50e7c09..00000000 --- a/app/src/test/java/ksnd/hiraganaconverter/PreviewTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package ksnd.hiraganaconverter - -import android.content.res.Configuration -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.LocalConfiguration -import com.airbnb.android.showkase.models.Showkase -import com.airbnb.android.showkase.models.ShowkaseBrowserComponent -import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH -import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers -import com.github.takahirom.roborazzi.captureRoboImage -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.ParameterizedRobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode - -// ref: https://github.com/DroidKaigi/conference-app-2023/pull/217 -@RunWith(ParameterizedRobolectricTestRunner::class) -@GraphicsMode(GraphicsMode.Mode.NATIVE) -@Config(qualifiers = RobolectricDeviceQualifiers.NexusOne) -class PreviewTest( - private val param: Pair, -) { - - @Test - fun previewScreenshot() { - val (showkaseBrowserComponent, count) = param - val componentName = showkaseBrowserComponent.componentName.replace(" ", "") - val filePath = DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + "/" + componentName + "_" + count + ".png" - captureRoboImage(filePath) { - val newConfiguration = Configuration().apply { - this.uiMode = if (componentName.contains(other = "dark", ignoreCase = true)) { - Configuration.UI_MODE_NIGHT_YES - } else { - Configuration.UI_MODE_NIGHT_NO - } - } - CompositionLocalProvider(LocalConfiguration provides newConfiguration) { - showkaseBrowserComponent.component() - } - } - } - - companion object { - @ParameterizedRobolectricTestRunner.Parameters - @JvmStatic - fun components(): Iterable> { - val countMap = mutableMapOf() - return Showkase.getMetadata().componentList.map { showkaseBrowserComponent -> - val componentName = showkaseBrowserComponent.componentName - val count = countMap.getOrDefault(key = componentName, defaultValue = 0) - countMap[componentName] = count + 1 - arrayOf(showkaseBrowserComponent to count) - } - } - } -} diff --git a/app/src/test/java/ksnd/hiraganaconverter/RoborazziComposePreviewTest.kt b/app/src/test/java/ksnd/hiraganaconverter/RoborazziComposePreviewTest.kt new file mode 100644 index 00000000..43f993c7 --- /dev/null +++ b/app/src/test/java/ksnd/hiraganaconverter/RoborazziComposePreviewTest.kt @@ -0,0 +1,95 @@ +package ksnd.hiraganaconverter + +import android.content.Context +import android.content.res.Configuration +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import androidx.test.core.app.ApplicationProvider +import com.github.takahirom.roborazzi.ComposePreviewTester +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi +import com.github.takahirom.roborazzi.LosslessWebPImageIoFormat +import com.github.takahirom.roborazzi.RoborazziOptions +import com.github.takahirom.roborazzi.captureRoboImage +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows +import org.robolectric.shadows.ShadowDisplay +import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner +import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo +import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder +import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview +import kotlin.math.roundToInt + +private const val SCREEN_SHOT_PATH = "../screenshots/" +private const val COMPARE_PATH = "../screenshots/compare/" + +/** + * ref: + * - https://github.com/takahirom/roborazzi + * - https://github.com/sergio-sastre/ComposablePreviewScanner + * - https://github.com/DeNA/android-modern-architecture-test-handson/blob/main/docs/handson/VisualRegressionTest_Preview_ComposablePreviewScanner.md + */ +@OptIn(ExperimentalRoborazziApi::class) +class RoborazziComposePreviewTest : ComposePreviewTester { + + private val composeTestRule = createAndroidComposeRule() + + override fun options(): ComposePreviewTester.Options { + // Use composeTestRule as a JUnit 4 Rule + val testLifecycleOptions = ComposePreviewTester.Options.JUnit4TestLifecycleOptions( + testRuleFactory = { composeTestRule }, + ) + return super.options().copy(testLifecycleOptions = testLifecycleOptions) + } + + override fun previews(): List> = + AndroidComposablePreviewScanner() + // Configure roborazzi's packages in :app/build.gradle.kts + .scanPackageTrees(*options().scanOptions.packages.toTypedArray()) + .getPreviews() + + override fun test(preview: ComposablePreview) { + val screenshotId = AndroidPreviewScreenshotIdBuilder(preview).build() + val filePath = "$SCREEN_SHOT_PATH$screenshotId.webp" + + preview.apply { + if (this.previewInfo.uiMode == Configuration.UI_MODE_NIGHT_YES) { + RuntimeEnvironment.setQualifiers("+night") + } + + setDisplaySize(this.previewInfo.widthDp, this.previewInfo.heightDp) + } + + // Change the environment and regenerate the activity + // Otherwise, environment will not be reflected + composeTestRule.activityRule.scenario.recreate() + + composeTestRule.apply { + setContent { + preview() + } + onRoot().captureRoboImage( + filePath = filePath, + roborazziOptions = RoborazziOptions( + compareOptions = RoborazziOptions.CompareOptions( + outputDirectoryPath = COMPARE_PATH, + ), + recordOptions = RoborazziOptions.RecordOptions( + imageIoFormat = LosslessWebPImageIoFormat(), + ), + ), + ) + } + } +} + +private fun setDisplaySize(widthDp: Int, heightDp: Int) { + val context = ApplicationProvider.getApplicationContext() + val display = ShadowDisplay.getDefaultDisplay() + val density = context.resources.displayMetrics.density + + Shadows.shadowOf(display).apply { + if (widthDp != -1) setWidth((widthDp * density).roundToInt()) + if (heightDp != -1) setHeight((heightDp * density).roundToInt()) + } +} diff --git a/app/src/test/java/ksnd/hiraganaconverter/view/screen/ConverterScreenTest.kt b/app/src/test/java/ksnd/hiraganaconverter/view/screen/ConverterScreenTest.kt deleted file mode 100644 index a2bdf03b..00000000 --- a/app/src/test/java/ksnd/hiraganaconverter/view/screen/ConverterScreenTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package ksnd.hiraganaconverter.view.screen - -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers -import com.github.takahirom.roborazzi.captureRoboImage -import ksnd.hiraganaconverter.core.ui.theme.HiraganaConverterTheme -import ksnd.hiraganaconverter.feature.converter.ConvertUiState -import ksnd.hiraganaconverter.feature.converter.ConverterScreenContent -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode - -@OptIn(ExperimentalMaterial3Api::class) -@RunWith(AndroidJUnit4::class) -@GraphicsMode(GraphicsMode.Mode.NATIVE) -@Config(qualifiers = RobolectricDeviceQualifiers.Pixel6Pro) -class ConverterScreenTest { - @Test - fun converterScreen_light() { - captureRoboImage { - HiraganaConverterTheme(isDarkTheme = false) { - ConverterScreenContent( - uiState = ConvertUiState(), - snackbarHostState = SnackbarHostState(), - scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()), - changeHiraKanaType = {}, - clearAllText = {}, - convert = {}, - updateInputText = {}, - updateOutputText = {}, - hideErrorCard = {}, - navigateScreen = {}, - ) - } - } - } - - @Test - fun converterScreen_dark() { - captureRoboImage { - HiraganaConverterTheme(isDarkTheme = true) { - ConverterScreenContent( - uiState = ConvertUiState(), - snackbarHostState = SnackbarHostState(), - scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()), - changeHiraKanaType = {}, - clearAllText = {}, - convert = {}, - updateInputText = {}, - updateOutputText = {}, - hideErrorCard = {}, - navigateScreen = {}, - ) - } - } - } -} diff --git a/app/src/test/resources/robolectric.properties b/app/src/test/resources/robolectric.properties new file mode 100644 index 00000000..4b38ad50 --- /dev/null +++ b/app/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=34 \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/AndroidLibraryComposePlugin.kt b/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/AndroidLibraryComposePlugin.kt index cf90659a..95123e54 100644 --- a/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/AndroidLibraryComposePlugin.kt +++ b/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/AndroidLibraryComposePlugin.kt @@ -29,9 +29,6 @@ class AndroidLibraryComposePlugin : Plugin { add("implementation", libs.findLibrary("androidx.compose.ui.tooling.preview").get()) add("testImplementation", libs.findLibrary("androidx.compose.ui.test.junit4").get()) add("implementation", libs.findLibrary("androidx.lifecycle.runtime.compose").get()) - add("debugImplementation", libs.findLibrary("showkase").get()) - add("implementation", libs.findLibrary("showkase.annotation").get()) - add("kspDebug", libs.findLibrary("showkase.processor").get()) } } } diff --git a/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/AndroidLibraryPlugin.kt b/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/AndroidLibraryPlugin.kt index 87a4a2aa..061cfe82 100644 --- a/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/AndroidLibraryPlugin.kt +++ b/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/AndroidLibraryPlugin.kt @@ -5,6 +5,9 @@ import org.gradle.api.JavaVersion import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile class AndroidLibraryPlugin : Plugin { override fun apply(target: Project) { @@ -14,7 +17,7 @@ class AndroidLibraryPlugin : Plugin { apply("org.jetbrains.kotlin.android") } extensions.configure { - compileSdk = 34 + compileSdk = 35 defaultConfig { minSdk = 26 } @@ -22,8 +25,10 @@ class AndroidLibraryPlugin : Plugin { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } } } } diff --git a/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/Extensions.kt b/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/Extensions.kt index a3abad7d..4cb3e322 100644 --- a/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/Extensions.kt +++ b/build-logic/convention/src/main/kotlin/ksnd/hiraganaconverter/Extensions.kt @@ -1,23 +1,13 @@ package ksnd.hiraganaconverter -import com.android.build.api.dsl.CommonExtension import org.gradle.api.Project import org.gradle.api.artifacts.VersionCatalogsExtension -import org.gradle.api.plugins.ExtensionAware import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.withType import org.gradle.testing.jacoco.plugins.JacocoPluginExtension import org.gradle.testing.jacoco.plugins.JacocoTaskExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions - -/* - * ref: https://github.com/android/nowinandroid/blob/main/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt - */ -fun CommonExtension<*, *, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) { - (this as ExtensionAware).extensions.configure("kotlinOptions", block) -} /* * ref: https://github.com/android/nowinandroid/blob/main/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt diff --git a/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/TopBar.kt b/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/TopBar.kt index b60c7847..d19876f1 100644 --- a/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/TopBar.kt +++ b/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/TopBar.kt @@ -32,6 +32,7 @@ import ksnd.hiraganaconverter.core.ui.isTest import ksnd.hiraganaconverter.core.ui.navigation.Nav import ksnd.hiraganaconverter.core.ui.parts.button.CustomIconButton import ksnd.hiraganaconverter.core.ui.preview.UiModePreview +import ksnd.hiraganaconverter.core.ui.theme.HiraganaConverterTheme @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -98,8 +99,10 @@ fun TopBar( @UiModePreview @Composable fun PreviewTopBar() { - TopBar( - scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()), - navigateScreen = {}, - ) + HiraganaConverterTheme { + TopBar( + scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()), + navigateScreen = {}, + ) + } } diff --git a/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/button/CustomButtonWithBackground.kt b/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/button/CustomButtonWithBackground.kt index 0de3767f..632cf154 100644 --- a/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/button/CustomButtonWithBackground.kt +++ b/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/button/CustomButtonWithBackground.kt @@ -5,11 +5,10 @@ import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.ripple.LocalRippleTheme -import androidx.compose.material.ripple.RippleAlpha -import androidx.compose.material.ripple.RippleTheme +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -27,6 +26,7 @@ import ksnd.hiraganaconverter.core.ui.preview.UiModePreview import ksnd.hiraganaconverter.core.ui.rememberButtonScaleState import ksnd.hiraganaconverter.core.ui.theme.HiraganaConverterTheme +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomButtonWithBackground( modifier: Modifier = Modifier, @@ -39,15 +39,7 @@ fun CustomButtonWithBackground( val buttonScaleState = rememberButtonScaleState() val localView = LocalView.current - CompositionLocalProvider( - LocalRippleTheme provides object : RippleTheme { - @Composable - override fun defaultColor() = Color.Transparent - - @Composable - override fun rippleAlpha() = RippleAlpha(0f, 0f, 0f, 0f) - }, - ) { + CompositionLocalProvider(LocalRippleConfiguration provides null) { IconButton( modifier = modifier .padding(all = 8.dp) diff --git a/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/dialog/MovesToSiteDialog.kt b/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/dialog/MovesToSiteDialog.kt index f996ead5..e33af99f 100644 --- a/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/dialog/MovesToSiteDialog.kt +++ b/core/ui/src/main/java/ksnd/hiraganaconverter/core/ui/parts/dialog/MovesToSiteDialog.kt @@ -4,20 +4,31 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import com.airbnb.android.showkase.annotation.ShowkaseComposable import ksnd.hiraganaconverter.core.resource.R import ksnd.hiraganaconverter.core.ui.preview.UiModePreview import ksnd.hiraganaconverter.core.ui.theme.HiraganaConverterTheme @Composable -fun MovesToSiteDialog(onDismissRequest: () -> Unit, onClick: () -> Unit, url: String) { +fun MoveToBrowserDialog( + url: String, + onMoveToBrowser: () -> Unit, + onDismissRequest: () -> Unit, +) { + val urlHandler = LocalUriHandler.current + AlertDialog( onDismissRequest = onDismissRequest, title = { Text(text = stringResource(id = R.string.move_to_browser)) }, text = { Text(text = url) }, confirmButton = { - TextButton(onClick = onClick) { + TextButton( + onClick = { + urlHandler.openUri(url) + onMoveToBrowser() + }, + ) { Text(text = stringResource(id = R.string.ok)) } }, @@ -31,13 +42,12 @@ fun MovesToSiteDialog(onDismissRequest: () -> Unit, onClick: () -> Unit, url: St @UiModePreview @Composable -@ShowkaseComposable(skip = true) -fun PreviewMovesToSiteDialog() { +private fun PreviewMoveToBrowserDialog() { HiraganaConverterTheme { - MovesToSiteDialog( + MoveToBrowserDialog( onDismissRequest = {}, - onClick = {}, - url = "架空のURL", + onMoveToBrowser = {}, + url = "https://example.com", ) } } diff --git a/feature/info/src/main/java/ksnd/hiraganaconverter/feature/info/InfoScreen.kt b/feature/info/src/main/java/ksnd/hiraganaconverter/feature/info/InfoScreen.kt index ebfa13d7..1f32a282 100644 --- a/feature/info/src/main/java/ksnd/hiraganaconverter/feature/info/InfoScreen.kt +++ b/feature/info/src/main/java/ksnd/hiraganaconverter/feature/info/InfoScreen.kt @@ -1,5 +1,6 @@ package ksnd.hiraganaconverter.feature.info +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -47,7 +48,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalUriHandler @@ -56,6 +56,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import ksnd.hiraganaconverter.core.analytics.LocalAnalytics @@ -68,7 +69,7 @@ import ksnd.hiraganaconverter.core.ui.parts.GooCreditImage import ksnd.hiraganaconverter.core.ui.parts.button.CustomIconButton import ksnd.hiraganaconverter.core.ui.parts.button.TransitionButton import ksnd.hiraganaconverter.core.ui.parts.card.TitleCard -import ksnd.hiraganaconverter.core.ui.parts.dialog.MovesToSiteDialog +import ksnd.hiraganaconverter.core.ui.parts.dialog.MoveToBrowserDialog import ksnd.hiraganaconverter.core.ui.preview.UiModePreview import ksnd.hiraganaconverter.core.ui.theme.HiraganaConverterTheme import ksnd.hiraganaconverter.core.ui.theme.urlColor @@ -99,8 +100,6 @@ private fun InfoScreenContent( onBackPressed: () -> Unit, onClickLicense: () -> Unit, ) { - val urlHandler = LocalUriHandler.current - val context = LocalContext.current val layoutDirection = LocalLayoutDirection.current val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) val coroutineScope = rememberCoroutineScope() @@ -156,26 +155,24 @@ private fun InfoScreenContent( } if (isShowMovesToAppSiteDialog) { - MovesToSiteDialog( + MoveToBrowserDialog( onDismissRequest = { isShowMovesToAppSiteDialog = false }, - onClick = { + onMoveToBrowser = { isShowMovesToAppSiteDialog = false - urlHandler.openUri(uri = context.getString(R.string.review_url)) }, url = stringResource(id = R.string.review_url), ) } if (isShowMovesToApiSiteDialog) { - MovesToSiteDialog( + MoveToBrowserDialog( onDismissRequest = { isShowMovesToApiSiteDialog = false }, - onClick = { + onMoveToBrowser = { isShowMovesToApiSiteDialog = false - urlHandler.openUri(uri = context.getString(R.string.goo_url)) }, url = stringResource(id = R.string.goo_url), ) @@ -384,7 +381,8 @@ private fun UrlText(url: String, onURLClick: () -> Unit) { ) } -@UiModePreview +@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, heightDp = 1100) +@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO, heightDp = 1100) @Composable fun PreviewInfoScreenContent() { HiraganaConverterTheme { diff --git a/feature/setting/src/main/java/ksnd/hiraganaconverter/feature/setting/SettingScreen.kt b/feature/setting/src/main/java/ksnd/hiraganaconverter/feature/setting/SettingScreen.kt index 65d99de1..1623b286 100644 --- a/feature/setting/src/main/java/ksnd/hiraganaconverter/feature/setting/SettingScreen.kt +++ b/feature/setting/src/main/java/ksnd/hiraganaconverter/feature/setting/SettingScreen.kt @@ -1,5 +1,6 @@ package ksnd.hiraganaconverter.feature.setting +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -36,6 +37,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch @@ -155,7 +157,8 @@ private fun SettingScreenContent( } } -@UiModePreview +@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, heightDp = 1300) +@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO, heightDp = 1300) @Composable fun PreviewSettingScreenContent() { HiraganaConverterTheme { diff --git a/gradle.properties b/gradle.properties index 2abf4c0e..062af591 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,3 +23,5 @@ kotlin.code.style=official android.nonTransitiveRClass=true org.gradle.caching=true + +roborazzi.record.image.extension=webp \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 91253de2..69ccae25 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,50 +1,51 @@ [versions] -accompanist = "0.34.0" -activity = "1.9.0" -androidGradlePlugin = "8.5.1" +accompanist = "0.36.0" +activity = "1.9.3" +androidGradlePlugin = "8.7.2" androidxAppCompat = "1.7.0" -androidxCompose = "1.6.8" -androidxComposeMaterial3 = "1.2.1" +androidxCompose = "1.7.5" +androidxComposeMaterial3 = "1.3.1" androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.1.1" androidxHilt = "1.2.0" -androidxLifecycle = "2.8.2" -androidxNavigation = "2.8.0-beta05" +androidxLifecycle = "2.8.7" +androidxNavigation = "2.8.4" androidxTestCore = "1.6.1" appUpdate = "2.1.0" coil = "2.7.0" -coroutines = "1.8.1" -coroutineTest = "1.8.1" +coroutines = "1.9.0" +coroutineTest = "1.9.0" dokka = "1.9.20" gmsPlugin = "4.4.2" -firebaseBom = "33.1.2" +firebaseBom = "33.6.0" firebaseCrashlyticsPlugin = "3.0.2" firebasePerfPlugin = "1.4.2" firebaseAppdistributionPlugin = "5.0.0" -hilt = "2.51.1" +hilt = "2.52" junit4 = "4.13.2" -kotlin = "2.0.0" -kotlinxSerializationJson = "1.7.1" -kotlinxDatetime = "0.6.0" -ksp = "2.0.0-1.0.22" -ktlint = "1.3.1" -lazyColumnScrollbar = "2.1.0" -lottie = "6.4.1" +kotlin = "2.0.21" +kotlinxSerializationJson = "1.7.3" +kotlinxDatetime = "0.6.1" +ksp = "2.0.21-1.0.28" +ktlint = "1.4.1" +lazyColumnScrollbar = "2.2.0" +lottie = "6.6.0" okhttp3 = "4.12.0" playReview = "2.0.1" retrofit = "2.11.0" retrofitSerialization = "1.0.0" -robolectric = "4.13" +robolectric = "4.14" room = "2.6.1" secrets = "2.0.1" timber = "5.0.1" truth = "1.4.4" -turbine = "1.1.0" -mockk = "1.13.12" -roborazzi = "1.23.0" -showkase = "1.0.3" -aboutLibraries = "11.2.2" -konsist = "0.15.1" +turbine = "1.2.0" +mockk = "1.13.13" +roborazzi = "1.32.2" +aboutLibraries = "11.2.3" +konsist = "0.16.1" +composablePreviewScanner = "0.4.0" +webpImageIO = "0.3.3" # ** I'm using it, so no deletions allowed. ** jacoco = "0.8.10" @@ -61,6 +62,7 @@ androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxCompose" } androidx-compose-ui-google-fonts = { group = "androidx.compose.ui", name = "ui-text-google-fonts", version.ref = "androidxCompose" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "androidxCompose" } +androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidxCompose" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidxCompose" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidxCompose" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" } @@ -120,6 +122,10 @@ mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" } roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" } roborazzi-junit4-rule = { group = "io.github.takahirom.roborazzi", name = "roborazzi-junit-rule", version.ref = "roborazzi" } +roborazzi-compose-preview-scanner-support = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose-preview-scanner-support", version.ref = "roborazzi" } + +# ComposablePreviewScanner +composable-preview-scanner = { group = "io.github.sergio-sastre.ComposablePreviewScanner", name = "android", version.ref = "composablePreviewScanner" } # Firebase firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref="firebaseBom"} @@ -149,17 +155,15 @@ kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-pl # LazyColumnScrollbar lazyColumnScrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazyColumnScrollbar" } -# Showkase -showkase = { group = "com.airbnb.android", name = "showkase", version.ref = "showkase" } -showkase-annotation = { group = "com.airbnb.android", name = "showkase-annotation", version.ref = "showkase" } -showkase-processor = { group = "com.airbnb.android", name = "showkase-processor", version.ref = "showkase" } - # AboutLibraries aboutLibraries = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "aboutLibraries" } # Konsist konsist = { group = "com.lemonappdev", name = "konsist", version.ref = "konsist" } +# WebP ImageIO +webp-image-io = { group = "io.github.darkxanter", name = "webp-imageio", version.ref = "webpImageIO" } + [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c352119..a4b76b95 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e..94113f20 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME