diff --git a/.editorconfig b/.editorconfig
index a1c8aaad..5ce188c5 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -20,3 +20,6 @@ ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
# @see https://youtrack.jetbrains.com/issue/KTIJ-21944
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_line_break_after_multiline_when_entry = false
+
+[*.yml]
+indent_size = 2
diff --git a/.gitattributes b/.gitattributes
index 17dde093..dd66c79c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,2 @@
**/baseline-prof.txt linguist-generated=true
-**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
**/yarn.lock -diff
diff --git a/.github/workflows/Screenshots.yml b/.github/workflows/CompareScreenshot.yml
similarity index 63%
rename from .github/workflows/Screenshots.yml
rename to .github/workflows/CompareScreenshot.yml
index a4e7ffd5..52e0949b 100644
--- a/.github/workflows/Screenshots.yml
+++ b/.github/workflows/CompareScreenshot.yml
@@ -2,28 +2,37 @@ name: CompareScreenshot
on:
push:
+ branches:
+ - master
+ pull_request:
paths:
- - 'image-loader/**'
+ - 'image-loader/src/**'
-env:
- GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx6g -Dorg.gradle.daemon=false -Dkotlin.incremental=false"
+permissions: { }
jobs:
- test:
- runs-on: macos-latest
+ compare-screenshot-test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ permissions:
+ contents: read # for clone
+ actions: write # for upload-artifact
steps:
- - uses: actions/checkout@v4
- with:
- lfs: true
+ - name: Checkout
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- - uses: actions/setup-java@v3.13.0
+ - name: Set up JDK 17
+ uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
with:
- distribution: 'zulu'
- java-version: 19
+ distribution: temurin
+ java-version: 17
- - name: Gradle cache
- uses: gradle/gradle-build-action@v2
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
+ with:
+ gradle-version: wrapper
# Download screenshots from main branch
- uses: dawidd6/action-download-artifact@v2
@@ -35,7 +44,7 @@ jobs:
- name: compare screenshot test
id: compare-screenshot-test
- run: ./gradlew compareRoborazziDebug --stacktrace --info
+ run: ./gradlew compareRoborazzi --stacktrace --info
- uses: actions/upload-artifact@v3
if: ${{ always() }}
diff --git a/.github/workflows/CompareScreenshotComment.yml b/.github/workflows/CompareScreenshotComment.yml
new file mode 100644
index 00000000..760e431f
--- /dev/null
+++ b/.github/workflows/CompareScreenshotComment.yml
@@ -0,0 +1,150 @@
+name: Screenshot compare comment
+
+on:
+ workflow_run:
+ workflows:
+ - CompareScreenshot
+ types:
+ - completed
+
+permissions: { }
+
+jobs:
+ comment-compare-screenshot-test:
+ if: >
+ github.event.workflow_run.event == 'pull_request' &&
+ github.event.workflow_run.conclusion == 'success'
+
+ timeout-minutes: 2
+
+ permissions:
+ actions: read # for downloading artifacts
+ contents: write # for pushing screenshot-diff to companion branch
+ pull-requests: write # for creating a comment on pull requests
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
+ with:
+ name: pr
+ run_id: ${{ github.event.workflow_run.id }}
+ - id: get-pull-request-number
+ name: Get pull request number
+ shell: bash
+ run: |
+ echo "pull_request_number=$(cat NR)" >> "$GITHUB_OUTPUT"
+ - name: master checkout
+ id: checkout-master
+ uses: actions/checkout@v3
+ with:
+ ref: master
+ - id: switch-companion-branch
+ env:
+ BRANCH_NAME: companion_${{ github.event.workflow_run.head_branch }}
+ run: |
+ # orphan means it will create no history branch
+ git branch -D "$BRANCH_NAME" || true
+ git checkout --orphan "$BRANCH_NAME"
+ git rm -rf .
+ - uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
+ with:
+ run_id: ${{ github.event.workflow_run.id }}
+ name: screenshot-diff
+ path: screenshot-diff
+ - id: check-if-there-are-valid-files
+ name: Check if there are valid files
+ shell: bash
+ run: |
+ # Find all the files ending with _compare.png
+ mapfile -t files_to_add < <(find . -type f -name "*_compare.png")
+
+ # Check for invalid file names and add only valid ones
+ exist_valid_files="false"
+ for file in "${files_to_add[@]}"; do
+ if [[ $file =~ ^[a-zA-Z0-9_./-]+$ ]]; then
+ exist_valid_files="true"
+ break
+ fi
+ done
+ echo "exist_valid_files=$exist_valid_files" >> "$GITHUB_OUTPUT"
+ - id: push-screenshot-diff
+ shell: bash
+ if: steps.check-if-there-are-valid-files.outputs.exist_valid_files == 'true'
+ env:
+ BRANCH_NAME: companion_${{ github.event.workflow_run.head_branch }}
+ run: |
+ # Find all the files ending with _compare.png
+ files_to_add=$(find . -type f -name "*_compare.png")
+
+ # Check for invalid file names and add only valid ones
+ for file in $files_to_add; do
+ if [[ "$file" =~ ^[a-zA-Z0-9_./-]+$ ]]; then
+ git add "$file"
+ fi
+ done
+ git config --global user.name ScreenshotBot
+ git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
+ git commit -m "Add screenshot diff"
+ git push origin HEAD:"$BRANCH_NAME" -f
+ - id: generate-diff-reports
+ name: Generate diff reports
+ if: steps.check-if-there-are-valid-files.outputs.exist_valid_files == 'true'
+ env:
+ BRANCH_NAME: companion_${{ github.event.workflow_run.head_branch }}
+ shell: bash
+ run: |
+ # Find all the files ending with _compare.png in roborazzi folder
+ files=$(find . -type f -name "*_compare.png" | grep "roborazzi/" | grep -E "^[a-zA-Z0-9_./-]+$")
+ delimiter="$(openssl rand -hex 8)"
+ {
+ echo "reports<<${delimiter}"
+
+ # Create markdown table header
+ echo "Snapshot diff report"
+ echo "| File name | Image |"
+ echo "|-------|-------|"
+ } >> "$GITHUB_OUTPUT"
+
+ # Iterate over the files and create table rows
+ for file in $files; do
+ # Get the file name and insert newlines every 20 characters
+ fileName=$(basename "$file" | sed -r 's/(.{20})/\1
/g')
+ echo "| [$fileName](https://github.com/${{ github.repository }}/blob/$BRANCH_NAME/$file) | ![](https://github.com/${{ github.repository }}/blob/$BRANCH_NAME/$file?raw=true) |" >> "$GITHUB_OUTPUT"
+ done
+ echo "${delimiter}" >> "$GITHUB_OUTPUT"
+ - name: Find Comment
+ uses: peter-evans/find-comment@v2
+ id: fc
+ if: steps.generate-diff-reports.outputs.reports != ''
+ with:
+ issue-number: ${{ steps.get-pull-request-number.outputs.pull_request_number }}
+ comment-author: 'github-actions[bot]'
+ body-includes: Snapshot diff report
+
+ - name: Add or update comment on PR
+ uses: peter-evans/create-or-update-comment@v3
+ if: steps.generate-diff-reports.outputs.reports != ''
+ with:
+ comment-id: ${{ steps.fc.outputs.comment-id }}
+ issue-number: ${{ steps.get-pull-request-number.outputs.pull_request_number }}
+ body: ${{ steps.generate-diff-reports.outputs.reports }}
+ edit-mode: replace
+
+ - name: Cleanup outdated companion branches
+ run: |
+ # Find outdated companion branches with last commit date
+ git branch -r --format="%(refname:lstrip=3)" | grep companion_ | while read -r branch; do
+ last_commit_date_timestamp=$(git log -1 --format=%ct "origin/$branch")
+ now_timestamp=$(date +%s)
+ # Delete branch if it's older than 1 month
+ # if [ $((now_timestamp - last_commit_date_timestamp)) -gt 2592000 ]; then
+ # For testing purpose, delete branch if it's older than 1 second
+ echo "branch: $branch now_timestamp: $now_timestamp last_commit_date_timestamp: $last_commit_date_timestamp"
+ if [ $((now_timestamp - last_commit_date_timestamp)) -gt 1 ]; then
+ # Comment out for demonstration purpose
+ echo "Deleting $branch"
+
+ # git push origin --delete "$branch"
+ fi
+ done
diff --git a/.github/workflows/DocsDeploy.yml b/.github/workflows/DocsDeploy.yml
index b5a66cb2..86a96b16 100644
--- a/.github/workflows/DocsDeploy.yml
+++ b/.github/workflows/DocsDeploy.yml
@@ -34,7 +34,7 @@ jobs:
run: ./gradlew dokkaHtmlMultiModule --stacktrace
- name: Setup Node
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: 18
cache: yarn
diff --git a/.github/workflows/DocsTest.yml b/.github/workflows/DocsTest.yml
index bfd3cd22..8a2f518a 100644
--- a/.github/workflows/DocsTest.yml
+++ b/.github/workflows/DocsTest.yml
@@ -34,7 +34,7 @@ jobs:
run: ./gradlew dokkaHtmlMultiModule --stacktrace
- name: Setup Node
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: 18
cache: yarn
diff --git a/.github/workflows/StoreScreenshot.yml b/.github/workflows/StoreScreenshot.yml
index 6b25faa8..88ca8f15 100644
--- a/.github/workflows/StoreScreenshot.yml
+++ b/.github/workflows/StoreScreenshot.yml
@@ -3,8 +3,12 @@ name: StoreScreenshot
on:
push:
branches:
- - main
+ - master
pull_request:
+ paths:
+ - 'image-loader/src/**'
+
+run-name: "StoreScreenshot by ${{ github.actor }}"
permissions: { }
@@ -19,7 +23,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up JDK 17
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
@@ -35,7 +39,7 @@ jobs:
- name: record screenshot
id: record-test
- run: ./gradlew recordRoborazziDebug --stacktrace
+ run: ./gradlew recordRoborazzi --stacktrace
- uses: actions/upload-artifact@v3
if: ${{ always() }}
@@ -59,4 +63,4 @@ jobs:
name: screenshot-test-results
path: |
**/build/test-results
- retention-days: 30
\ No newline at end of file
+ retention-days: 30
diff --git a/README.md b/README.md
index a72dfc00..bd616703 100644
--- a/README.md
+++ b/README.md
@@ -17,17 +17,17 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
-+ api("io.github.qdsfdhvh:image-loader:1.6.8")
++ api("io.github.qdsfdhvh:image-loader:1.7.0")
// optional - Moko Resources Decoder
-+ api("io.github.qdsfdhvh:image-loader-extension-moko-resources:1.6.8")
++ api("io.github.qdsfdhvh:image-loader-extension-moko-resources:1.7.0")
// optional - Blur Interceptor (only support bitmap)
-+ api("io.github.qdsfdhvh:image-loader-extension-blur:1.6.8")
++ api("io.github.qdsfdhvh:image-loader-extension-blur:1.7.0")
}
}
val jvmMain by getting {
dependencies {
// optional - ImageIO Decoder
-+ api("io.github.qdsfdhvh:image-loader-extension-imageio:1.6.8")
++ api("io.github.qdsfdhvh:image-loader-extension-imageio:1.7.0")
}
}
}
@@ -58,18 +58,40 @@ fun Content() {
CompositionLocalProvider(
LocalImageLoader provides remember { generateImageLoader() },
) {
- val painter = rememberImagePainter("https://..")
+ // Option 1 on 1.7.0+
+ AutoSizeImage(
+ "https://...",
+ contentDescription = "image",
+ )
+ // Option 2 on 1.7.0+
+ AutoSizeBox("https://...") { action ->
+ when (action) {
+ is ImageAction.Success -> {
+ Image(
+ rememberImageSuccessPainter(action),
+ contentDescription = "image",
+ )
+ }
+ is ImageAction.Loading -> {}
+ is ImageAction.Failure -> {}
+ }
+ }
+ // Option 3
Image(
- painter = painter,
+ painter = rememberImagePainter("https://.."),
contentDescription = "image",
)
}
}
```
+Use priority: `AutoSizeImage` -> `AutoSizeBox` -> `rememberImagePainter`.
+
+`AutoSizeBox` & `AutoSizeImage` are based on **Modifier.Node**, `AutoSizeImage` ≈ `AutoSizeBox` + `Painter`.
+
#### in Android
-```kotlin title="MainActivity.kt"
+```kotlin
fun generateImageLoader(): ImageLoader {
return ImageLoader {
options {
@@ -96,7 +118,7 @@ fun generateImageLoader(): ImageLoader {
#### in Jvm
-```kotlin title="Main.kt"
+```kotlin
fun generateImageLoader(): ImageLoader {
return ImageLoader {
components {
diff --git a/app/android/benchmark/build.gradle.kts b/app/android/benchmark/build.gradle.kts
index 1aa7bc09..cdd23fc4 100644
--- a/app/android/benchmark/build.gradle.kts
+++ b/app/android/benchmark/build.gradle.kts
@@ -9,6 +9,7 @@ plugins {
android {
namespace = "com.seiko.imageloader.demo.benchmark"
defaultConfig {
+ minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// TODO temporary until AGP 8.2, which no longer requires this.
diff --git a/app/android/src/main/java/com/seiko/imageloader/demo/MainActivity.kt b/app/android/src/main/java/com/seiko/imageloader/demo/MainActivity.kt
index 0a9bf110..a30ec844 100644
--- a/app/android/src/main/java/com/seiko/imageloader/demo/MainActivity.kt
+++ b/app/android/src/main/java/com/seiko/imageloader/demo/MainActivity.kt
@@ -5,9 +5,9 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
-import com.seiko.imageloader.DefaultAndroid
import com.seiko.imageloader.ImageLoader
import com.seiko.imageloader.LocalImageLoader
+import com.seiko.imageloader.createDefaultAndroid
import com.seiko.imageloader.demo.util.commonConfig
class MainActivity : ComponentActivity() {
@@ -24,7 +24,7 @@ class MainActivity : ComponentActivity() {
private fun generateImageLoader(): ImageLoader {
return ImageLoader {
- takeFrom(ImageLoader.DefaultAndroid(applicationContext))
+ takeFrom(ImageLoader.createDefaultAndroid(applicationContext))
commonConfig()
}
// return ImageLoader {
diff --git a/app/common/build.gradle.kts b/app/common/build.gradle.kts
index 5f86fd30..7a4ca1e8 100644
--- a/app/common/build.gradle.kts
+++ b/app/common/build.gradle.kts
@@ -7,9 +7,8 @@ plugins {
}
kotlin {
- @Suppress("UNUSED_VARIABLE")
sourceSets {
- val commonMain by getting {
+ commonMain {
dependencies {
api(compose.runtime)
api(compose.foundation)
@@ -27,24 +26,24 @@ kotlin {
implementation(libs.kermit)
}
}
- val androidMain by getting {
+ androidMain {
// https://github.com/icerockdev/moko-resources/issues/531
- dependsOn(commonMain)
+ dependsOn(commonMain.get())
dependencies {
implementation(libs.ktor.client.cio)
}
}
- val desktopMain by getting {
+ desktopMain {
dependencies {
implementation(libs.ktor.client.cio)
}
}
- val appleMain by getting {
+ appleMain {
dependencies {
implementation(libs.ktor.client.darwin)
}
}
- val jsMain by getting {
+ jsMain {
dependencies {
implementation(libs.ktor.client.js)
}
diff --git a/app/common/src/commonMain/kotlin/com/seiko/imageloader/demo/scene/Common.kt b/app/common/src/commonMain/kotlin/com/seiko/imageloader/demo/scene/Common.kt
index 03eae010..74591bbe 100644
--- a/app/common/src/commonMain/kotlin/com/seiko/imageloader/demo/scene/Common.kt
+++ b/app/common/src/commonMain/kotlin/com/seiko/imageloader/demo/scene/Common.kt
@@ -4,7 +4,6 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@@ -28,12 +27,11 @@ import androidx.compose.ui.layout.ContentScale
import com.seiko.imageloader.demo.model.Image
import com.seiko.imageloader.demo.util.NullDataInterceptor
import com.seiko.imageloader.demo.util.decodeJson
-import com.seiko.imageloader.model.ImageEvent
+import com.seiko.imageloader.model.ImageAction
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.ImageRequestBuilder
-import com.seiko.imageloader.model.ImageResult
-import com.seiko.imageloader.rememberImageAction
-import com.seiko.imageloader.rememberImageActionPainter
+import com.seiko.imageloader.rememberImageSuccessPainter
+import com.seiko.imageloader.ui.AutoSizeBox
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -77,7 +75,7 @@ fun ImageItem(
Box(modifier, Alignment.Center) {
val dataState by rememberUpdatedState(data)
val blockState by rememberUpdatedState(block)
- val requestState = remember {
+ val request by remember {
derivedStateOf {
ImageRequest {
data(dataState)
@@ -92,27 +90,27 @@ fun ImageItem(
}
}
}
- val action by rememberImageAction(requestState)
- val painter = rememberImageActionPainter(action)
- Image(
- painter = painter,
- contentDescription = null,
- contentScale = contentScale,
- modifier = Modifier.fillMaxSize(),
- )
- when (val current = action) {
- is ImageEvent.StartWithDisk,
- is ImageEvent.StartWithFetch,
- -> {
- CircularProgressIndicator()
- }
- is ImageResult.Source -> {
- Text("image result is source")
- }
- is ImageResult.Error -> {
- Text(current.error.message ?: "Error")
+
+ AutoSizeBox(
+ request,
+ Modifier.matchParentSize(),
+ ) { action ->
+ when (action) {
+ is ImageAction.Loading -> {
+ CircularProgressIndicator()
+ }
+ is ImageAction.Success -> {
+ Image(
+ rememberImageSuccessPainter(action),
+ contentDescription = "image",
+ contentScale = contentScale,
+ modifier = Modifier.matchParentSize(),
+ )
+ }
+ is ImageAction.Failure -> {
+ Text(action.error.message ?: "Error")
+ }
}
- else -> Unit
}
}
}
diff --git a/app/common/src/commonMain/kotlin/com/seiko/imageloader/demo/util/ImageLoader.kt b/app/common/src/commonMain/kotlin/com/seiko/imageloader/demo/util/ImageLoader.kt
index aa4a12b7..a464b939 100644
--- a/app/common/src/commonMain/kotlin/com/seiko/imageloader/demo/util/ImageLoader.kt
+++ b/app/common/src/commonMain/kotlin/com/seiko/imageloader/demo/util/ImageLoader.kt
@@ -66,7 +66,7 @@ object NullDataInterceptor : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val data = chain.request.data
if (data === NullRequestData || data is String && data.isEmpty()) {
- return ImageResult.Painter(
+ return ImageResult.OfPainter(
painter = EmptyPainter,
)
}
diff --git a/app/intellij-plugin/build.gradle.kts b/app/intellij-plugin/build.gradle.kts
index 94387650..591b17c3 100644
--- a/app/intellij-plugin/build.gradle.kts
+++ b/app/intellij-plugin/build.gradle.kts
@@ -1,5 +1,5 @@
plugins {
- id("org.jetbrains.intellij") version "1.15.0"
+ id("org.jetbrains.intellij") version "1.16.0"
java
id("app.kotlin.jvm")
id("app.compose.multiplatform")
diff --git a/app/ios-combine/build.gradle.kts b/app/ios-combine/build.gradle.kts
index edbf3cfc..b8f15588 100644
--- a/app/ios-combine/build.gradle.kts
+++ b/app/ios-combine/build.gradle.kts
@@ -21,7 +21,7 @@ kotlin {
version = "1.0.0"
summary = "Shared code for the sample"
homepage = "https://github.com/qdsfdhvh/compose-imageloader"
- ios.deploymentTarget = "16.0"
+ ios.deploymentTarget = "14.0"
podfile = project.file("../ios/Podfile")
framework {
baseName = "combine"
diff --git a/app/ios-combine/src/commonMain/kotlin/com/seiko/imageloader/demo/Main.kt b/app/ios-combine/src/commonMain/kotlin/com/seiko/imageloader/demo/Main.kt
index dcf4ff98..b34c8cf7 100644
--- a/app/ios-combine/src/commonMain/kotlin/com/seiko/imageloader/demo/Main.kt
+++ b/app/ios-combine/src/commonMain/kotlin/com/seiko/imageloader/demo/Main.kt
@@ -3,9 +3,9 @@ package com.seiko.imageloader.demo
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.window.ComposeUIViewController
-import com.seiko.imageloader.DefaultIOS
import com.seiko.imageloader.ImageLoader
import com.seiko.imageloader.LocalImageLoader
+import com.seiko.imageloader.createDefaultIOS
import com.seiko.imageloader.demo.util.commonConfig
import platform.UIKit.UIViewController
@@ -20,7 +20,7 @@ fun MainViewController(): UIViewController = ComposeUIViewController {
private fun generateImageLoader(): ImageLoader {
return ImageLoader {
- takeFrom(ImageLoader.DefaultIOS)
+ takeFrom(ImageLoader.createDefaultIOS())
commonConfig()
}
// return ImageLoader {
diff --git a/app/macos/build.gradle.kts b/app/macos/build.gradle.kts
index 12078f61..926b8c7b 100644
--- a/app/macos/build.gradle.kts
+++ b/app/macos/build.gradle.kts
@@ -30,19 +30,12 @@ kotlin {
}
}
}
- @Suppress("UNUSED_VARIABLE")
sourceSets {
- val macosMain by creating {
+ commonMain {
dependencies {
implementation(projects.app.common)
}
}
- val macosX64Main by getting {
- dependsOn(macosMain)
- }
- val macosArm64Main by getting {
- dependsOn(macosMain)
- }
}
}
diff --git a/app/web/build.gradle.kts b/app/web/build.gradle.kts
index 2279788f..abc74569 100644
--- a/app/web/build.gradle.kts
+++ b/app/web/build.gradle.kts
@@ -9,9 +9,8 @@ kotlin {
browser()
binaries.executable()
}
- @Suppress("UNUSED_VARIABLE")
sourceSets {
- val commonMain by getting {
+ commonMain {
dependencies {
implementation(projects.app.common)
implementation(compose.runtime)
@@ -19,9 +18,9 @@ kotlin {
implementation(libs.moko.resources)
}
}
- val jsMain by getting {
+ jsMain {
// https://github.com/icerockdev/moko-resources/issues/531
- dependsOn(commonMain)
+ dependsOn(commonMain.get())
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/KotlinMultiplatformConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KotlinMultiplatformConventionPlugin.kt
index 1e005fc0..91d302c2 100644
--- a/build-logic/convention/src/main/kotlin/KotlinMultiplatformConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/KotlinMultiplatformConventionPlugin.kt
@@ -24,7 +24,7 @@ class KotlinMultiplatformConventionPlugin : Plugin {
nodejs()
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)
- targetHierarchy.custom {
+ applyHierarchyTemplate {
common {
group("jvm") {
withAndroidTarget()
@@ -49,6 +49,14 @@ class KotlinMultiplatformConventionPlugin : Plugin {
}
}
}
+ targets.configureEach {
+ compilations.configureEach {
+ compilerOptions.configure {
+ // https://youtrack.jetbrains.com/issue/KT-61573
+ freeCompilerArgs.add("-Xexpect-actual-classes")
+ }
+ }
+ }
}
configKotlin()
}
diff --git a/build.gradle.kts b/build.gradle.kts
index f84e74bf..54bef2f7 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -87,10 +87,10 @@ object ProjectVersion {
private const val major = "1"
// functionality in a backwards compatible manner
- private const val monir = "6"
+ private const val monir = "7"
// backwards compatible bug fixes
- private const val path = "8"
+ private const val path = "0"
const val version = "$major.$monir.$path"
}
diff --git a/docs/docs/core/basic.md b/docs/docs/core/basic.md
index 65b09eff..1d1684e5 100644
--- a/docs/docs/core/basic.md
+++ b/docs/docs/core/basic.md
@@ -5,13 +5,35 @@
Just use it like this for display image:
```kotlin
-val painter = rememberImagePainter("https://..")
+// Option 1 on 1.7.0+
+AutoSizeImage(
+ "https://...",
+ contentDescription = "image",
+)
+// Option 2 on 1.7.0+
+AutoSizeBox("https://...") { action ->
+ when (action) {
+ is ImageAction.Success -> {
+ Image(
+ rememberImageSuccessPainter(action),
+ contentDescription = "image",
+ )
+ }
+ is ImageAction.Loading -> {}
+ is ImageAction.Failure -> {}
+ }
+}
+// Option 3
Image(
- painter = painter,
+ painter = rememberImagePainter("https://.."),
contentDescription = "image",
)
```
+Use priority: `AutoSizeImage` -> `AutoSizeBox` -> `rememberImagePainter`.
+
+`AutoSizeBox` & `AutoSizeImage` are based on **Modifier.Node**, `AutoSizeImage` ≈ `AutoSizeBox` + `Painter`.
+
PS: default `Imageloader` will reload when it's displayed, is not friendly for `https` link, so it is recommended to custom `ImageLoader` and configure the cache.
## Custom ImageLoader
@@ -24,11 +46,7 @@ fun Content() {
CompositionLocalProvider(
LocalImageLoader provides remember { generateImageLoader() },
) {
- val painter = rememberImagePainter("https://..")
- Image(
- painter = painter,
- contentDescription = "image",
- )
+ // App
}
}
```
@@ -45,6 +63,8 @@ fun generateImageLoader(): ImageLoader {
setupDefaultComponents()
}
interceptor {
+ // cache 100 success image result, without bitmap
+ defaultImageResultMemoryCache()
memoryCacheConfig {
// Set the max size to 25% of the app's available memory.
maxSizePercent(context, 0.25)
@@ -67,6 +87,8 @@ fun generateImageLoader(): ImageLoader {
setupDefaultComponents()
}
interceptor {
+ // cache 100 success image result, without bitmap
+ defaultImageResultMemoryCache()
memoryCacheConfig {
maxSizeBytes(32 * 1024 * 1024) // 32MB
}
@@ -95,7 +117,9 @@ fun generateImageLoader(): ImageLoader {
components {
setupDefaultComponents()
}
- interceptor {
+ interceptor {
+ // cache 100 success image result, without bitmap
+ defaultImageResultMemoryCache()
memoryCacheConfig {
maxSizeBytes(32 * 1024 * 1024) // 32MB
}
diff --git a/docs/docs/core/imageloader.md b/docs/docs/core/imageloader.md
index 45729a31..aeacc8b2 100644
--- a/docs/docs/core/imageloader.md
+++ b/docs/docs/core/imageloader.md
@@ -11,22 +11,30 @@ interface ImageLoader {
`ImageAction` structure is as follows:
```kotlin
-sealed interface ImageAction
-
-sealed interface ImageEvent : ImageAction {
- object Start : ImageEvent
- object StartWithMemory : ImageEvent
- object StartWithDisk : ImageEvent
- object StartWithFetch : ImageEvent
- data class Progress(val progress: Float) : ImageEvent
+sealed interface ImageAction {
+ sealed interface Loading : ImageAction
+ sealed interface Success : ImageAction
+ sealed interface Failure : ImageAction {
+ val error: Throwable
+ }
+}
+
+sealed interface ImageEvent : ImageAction.Loading {
+ data object Start : ImageEvent
+ data object StartWithMemory : ImageEvent
+ data object StartWithDisk : ImageEvent
+ data object StartWithFetch : ImageEvent
}
sealed interface ImageResult : ImageAction {
- data class Source() : ImageResult
- data class Bitmap() : ImageResult
- data class Image() : ImageResult
- data class Painter() : ImageResult
- data class Error() : ImageResult
+ data class OfBitmap() : ImageResult, ImageAction.Success
+ data class OfImage() : ImageResult, ImageAction.Success
+ data class OfPainter() :ImageResult, ImageAction.Success
+ data class OfError(override val error: Throwable) : ImageResult, ImageAction.Failure
+ data class OfSource() : ImageResult, ImageAction.Failure {
+ override val error: Throwable
+ get() = IllegalStateException("failure to decode image source")
+ }
}
```
@@ -35,7 +43,7 @@ sealed interface ImageResult : ImageAction {
This is the most center feature of `ImageLoader`, The loading of the entire image is implemented by the default 3 + 2 interceptors:
- **MappedInterceptor**
-- MemoryCacheInterceptor
+- MemoryCacheInterceptors
- **DecodeInterceptor**
- DiskCacheInterceptor
- **FetchInterceptor**
@@ -95,7 +103,6 @@ ImageLoader {
retryIfDiskDecodeError = true
imageConfig = Options.ImageConfig.ARGB_8888
scale = Scale.AUTO
- sizeResolver = SizeResolver.Unspecified
memoryCachePolicy = CachePolicy.ENABLED
diskCachePolicy = CachePolicy.ENABLED
playAnimate = true
diff --git a/docs/docs/setup.mdx b/docs/docs/setup.mdx
index 1aaa4f2a..5edce839 100644
--- a/docs/docs/setup.mdx
+++ b/docs/docs/setup.mdx
@@ -15,17 +15,17 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
-+ api("io.github.qdsfdhvh:image-loader:1.6.8")
++ api("io.github.qdsfdhvh:image-loader:1.7.0")
// optional - Moko Resources Decoder
-+ api("io.github.qdsfdhvh:image-loader-extension-moko-resources:1.6.8")
++ api("io.github.qdsfdhvh:image-loader-extension-moko-resources:1.7.0")
// optional - Blur Interceptor (only support bitmap)
-+ api("io.github.qdsfdhvh:image-loader-extension-blur:1.6.8")
++ api("io.github.qdsfdhvh:image-loader-extension-blur:1.7.0")
}
}
val jvmMain by getting {
dependencies {
// optional - ImageIO Decoder
-+ api("io.github.qdsfdhvh:image-loader-extension-imageio:1.6.8")
++ api("io.github.qdsfdhvh:image-loader-extension-imageio:1.7.0")
}
}
}
@@ -38,7 +38,7 @@ Copy the following snippets if you are using [gradle version catalog](https://do
```xml title="libs.versions.toml"
[versions]
-image-loader = "1.6.1"
+image-loader = "1.7.0"
[libraries]
image-loader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "image-loader" }
diff --git a/docs/src/pages/index.mdx b/docs/src/pages/index.mdx
index b67889ac..66212ae8 100644
--- a/docs/src/pages/index.mdx
+++ b/docs/src/pages/index.mdx
@@ -11,9 +11,27 @@
```kotlin
@Composeable
fun Content() {
- val painter = rememberImagePainter("https://...")
+ // Option 1 on 1.7.0+
+ AutoSizeImage(
+ "https://...",
+ contentDescription = "image",
+ )
+ // Option 2 on 1.7.0+
+ AutoSizeBox("https://...") { action ->
+ when (action) {
+ is ImageAction.Success -> {
+ Image(
+ rememberImageSuccessPainter(action),
+ contentDescription = "image",
+ )
+ }
+ is ImageAction.Loading -> {}
+ is ImageAction.Failure -> {}
+ }
+ }
+ // Option 3
Image(
- painter = painter,
+ painter = rememberImagePainter("https://.."),
contentDescription = "image",
)
}
diff --git a/extension/blur/build.gradle.kts b/extension/blur/build.gradle.kts
index 93b64e36..fcd92450 100644
--- a/extension/blur/build.gradle.kts
+++ b/extension/blur/build.gradle.kts
@@ -12,8 +12,7 @@ kotlin {
implementation(projects.imageLoader)
}
}
- @Suppress("UNUSED_VARIABLE")
- val androidMain by getting {
+ androidMain {
dependencies {
implementation("com.github.android:renderscript-intrinsics-replacement-toolkit:9a70eae6f1")
}
diff --git a/extension/blur/src/commonMain/kotlin/com/seiko/imageloader/intercept/BlurInterceptor.kt b/extension/blur/src/commonMain/kotlin/com/seiko/imageloader/intercept/BlurInterceptor.kt
index 586a5587..aa433f8e 100644
--- a/extension/blur/src/commonMain/kotlin/com/seiko/imageloader/intercept/BlurInterceptor.kt
+++ b/extension/blur/src/commonMain/kotlin/com/seiko/imageloader/intercept/BlurInterceptor.kt
@@ -9,9 +9,9 @@ class BlurInterceptor : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = chain.request
val result = chain.proceed(request)
- if (result is ImageResult.Bitmap) {
+ if (result is ImageResult.OfBitmap) {
val blurEffects = request.blurEffects ?: return result
- return ImageResult.Bitmap(
+ return ImageResult.OfBitmap(
bitmap = blur(result.bitmap, blurEffects.radius),
)
}
diff --git a/extension/imageio/build.gradle.kts b/extension/imageio/build.gradle.kts
index 499ac873..77b15943 100644
--- a/extension/imageio/build.gradle.kts
+++ b/extension/imageio/build.gradle.kts
@@ -11,7 +11,7 @@ kotlin {
dependencies {
implementation(projects.imageLoader)
// svg
- implementation("com.twelvemonkeys.imageio:imageio-batik:3.9.4")
+ implementation("com.twelvemonkeys.imageio:imageio-batik:3.10.0")
implementation("org.apache.xmlgraphics:batik-transcoder:1.17")
}
}
diff --git a/extension/imageio/src/jvmMain/kotlin/com/seiko/imageloader/component/decoder/ImageIODecoder.kt b/extension/imageio/src/jvmMain/kotlin/com/seiko/imageloader/component/decoder/ImageIODecoder.kt
index 85884517..e709ea08 100644
--- a/extension/imageio/src/jvmMain/kotlin/com/seiko/imageloader/component/decoder/ImageIODecoder.kt
+++ b/extension/imageio/src/jvmMain/kotlin/com/seiko/imageloader/component/decoder/ImageIODecoder.kt
@@ -15,13 +15,13 @@ class ImageIODecoder(
val image = runInterruptible {
ImageIO.read(source.inputStream())
}
- return DecodeResult.Painter(
+ return DecodeResult.OfPainter(
painter = image.toPainter(),
)
}
class Factory : Decoder.Factory {
- override suspend fun create(source: DecodeSource, options: Options): Decoder? {
+ override fun create(source: DecodeSource, options: Options): Decoder? {
if (isGif(source.source)) return null
return ImageIODecoder(source.source)
}
diff --git a/extension/moko-resources/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.android.kt b/extension/moko-resources/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.android.kt
index f5ff9633..caa0fcd1 100644
--- a/extension/moko-resources/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.android.kt
+++ b/extension/moko-resources/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.android.kt
@@ -13,26 +13,26 @@ import okio.buffer
import okio.source
internal actual suspend fun AssetResource.toFetchResult(options: Options): FetchResult? {
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = getInputStream(options.androidContext).source().buffer(),
)
}
internal actual suspend fun ColorResource.toFetchResult(options: Options): FetchResult? {
- return FetchResult.Painter(
+ return FetchResult.OfPainter(
painter = ColorPainter(Color(getColor(options.androidContext))),
)
}
internal actual suspend fun FileResource.toFetchResult(options: Options): FetchResult? {
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = options.androidContext.resources.openRawResource(rawResId).source().buffer(),
)
}
internal actual suspend fun ImageResource.toFetchResult(options: Options): FetchResult? {
val drawable = requireNotNull(getDrawable(options.androidContext))
- return FetchResult.Image(
+ return FetchResult.OfImage(
image = drawable.toImage(),
)
}
diff --git a/extension/moko-resources/src/desktopMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.desktop.kt b/extension/moko-resources/src/desktopMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.desktop.kt
index e9309b73..8ec50ec0 100644
--- a/extension/moko-resources/src/desktopMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.desktop.kt
+++ b/extension/moko-resources/src/desktopMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.desktop.kt
@@ -16,7 +16,7 @@ internal actual suspend fun AssetResource.toFetchResult(options: Options): Fetch
}
internal actual suspend fun ColorResource.toFetchResult(options: Options): FetchResult? {
- return FetchResult.Painter(
+ return FetchResult.OfPainter(
painter = ColorPainter(
Color(
red = lightColor.red,
@@ -31,7 +31,7 @@ internal actual suspend fun ColorResource.toFetchResult(options: Options): Fetch
internal actual suspend fun FileResource.toFetchResult(options: Options): FetchResult? {
val stream = resourcesClassLoader.getResourceAsStream(filePath)
?: throw FileNotFoundException("Couldn't open resource as stream at: $filePath")
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = stream.source().buffer(),
)
}
@@ -39,7 +39,7 @@ internal actual suspend fun FileResource.toFetchResult(options: Options): FetchR
internal actual suspend fun ImageResource.toFetchResult(options: Options): FetchResult? {
val stream = resourcesClassLoader.getResourceAsStream(filePath)
?: throw FileNotFoundException("Couldn't open resource as stream at: $filePath")
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = stream.source().buffer(),
)
}
diff --git a/extension/moko-resources/src/iosMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.ios.kt b/extension/moko-resources/src/iosMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.ios.kt
index 82b10621..aea29635 100644
--- a/extension/moko-resources/src/iosMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.ios.kt
+++ b/extension/moko-resources/src/iosMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.ios.kt
@@ -46,7 +46,7 @@ internal actual suspend fun ColorResource.toFetchResult(options: Options): Fetch
alpha = alpha.value.toFloat(),
)
}
- return FetchResult.Painter(
+ return FetchResult.OfPainter(
painter = ColorPainter(color),
)
}
@@ -57,7 +57,7 @@ internal actual suspend fun FileResource.toFetchResult(options: Options): FetchR
ofType = extension,
inDirectory = "files",
)!!.toPath()
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = FileSystem.SYSTEM.source(path).buffer(),
)
}
@@ -68,7 +68,7 @@ internal actual suspend fun ImageResource.toFetchResult(options: Options): Fetch
?: throw IllegalArgumentException("can't read UIImage of $this")
val cgImage: CGImageRef = uiImage.CGImage()
?: throw IllegalArgumentException("can't read CGImage of $this")
- return FetchResult.Image(
+ return FetchResult.OfImage(
image = cgImage.toSkiaImage(),
)
}
diff --git a/extension/moko-resources/src/jsMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.js.kt b/extension/moko-resources/src/jsMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.js.kt
index 49bf0409..879d864a 100644
--- a/extension/moko-resources/src/jsMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.js.kt
+++ b/extension/moko-resources/src/jsMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.js.kt
@@ -14,25 +14,25 @@ import okio.BufferedSource
import org.khronos.webgl.Int8Array
internal actual suspend fun AssetResource.toFetchResult(options: Options): FetchResult? {
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = fetchToSource(originalPath),
)
}
internal actual suspend fun ColorResource.toFetchResult(options: Options): FetchResult? {
- return FetchResult.Painter(
+ return FetchResult.OfPainter(
painter = ColorPainter(Color(lightColor.argb)),
)
}
internal actual suspend fun FileResource.toFetchResult(options: Options): FetchResult? {
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = fetchToSource(fileUrl),
)
}
internal actual suspend fun ImageResource.toFetchResult(options: Options): FetchResult? {
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = fetchToSource(fileUrl),
)
}
diff --git a/extension/moko-resources/src/macosMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.macos.kt b/extension/moko-resources/src/macosMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.macos.kt
index e8fc390e..bd96e573 100644
--- a/extension/moko-resources/src/macosMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.macos.kt
+++ b/extension/moko-resources/src/macosMain/kotlin/com/seiko/imageloader/component/fetcher/MokoResourceFetcher.macos.kt
@@ -24,7 +24,7 @@ internal actual suspend fun ColorResource.toFetchResult(options: Options): Fetch
val nsColor = getNSColor()
val deviceColor = nsColor.colorUsingColorSpace(deviceRGBColorSpace)
?: error("can't convert $nsColor to deviceRGBColorSpace")
- return FetchResult.Painter(
+ return FetchResult.OfPainter(
painter = ColorPainter(
Color(
red = deviceColor.redComponent.toFloat(),
@@ -42,7 +42,7 @@ internal actual suspend fun FileResource.toFetchResult(options: Options): FetchR
ofType = extension,
inDirectory = "files",
)!!.toPath()
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = FileSystem.SYSTEM.source(path).buffer(),
)
}
@@ -56,7 +56,7 @@ internal actual suspend fun ImageResource.toFetchResult(options: Options): Fetch
context = null,
hints = null,
) ?: throw IllegalArgumentException("can't read CGImage of $this")
- return FetchResult.Image(
+ return FetchResult.OfImage(
image = cgImage.toSkiaImage(),
)
}
diff --git a/extension/nine-patch/src/commonMain/kotlin/com/seiko/imageloader/intercept/NinePatchInterceptor.kt b/extension/nine-patch/src/commonMain/kotlin/com/seiko/imageloader/intercept/NinePatchInterceptor.kt
index 94c19ea1..cea77289 100644
--- a/extension/nine-patch/src/commonMain/kotlin/com/seiko/imageloader/intercept/NinePatchInterceptor.kt
+++ b/extension/nine-patch/src/commonMain/kotlin/com/seiko/imageloader/intercept/NinePatchInterceptor.kt
@@ -10,9 +10,9 @@ class NinePatchInterceptor : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = chain.request
val result = chain.proceed(request)
- if (result is ImageResult.Bitmap) {
+ if (result is ImageResult.OfBitmap) {
val centerSlice = request.ninePatchData ?: return result
- return ImageResult.Painter(
+ return ImageResult.OfPainter(
painter = NinePatchPainter(
image = result.bitmap.asImageBitmap(),
ninePatchData = centerSlice,
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c2f8986e..e1216a3a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,7 +1,7 @@
[versions]
agp = "8.1.2"
-compose-multiplatform = "1.5.3"
-kotlin = "1.9.10"
+compose-multiplatform = "1.5.10"
+kotlin = "1.9.20"
kotlinx-coroutines = "1.7.3"
kotlinx-serialization = "1.6.0"
androidx-core-ktx = "1.12.0"
@@ -13,21 +13,21 @@ androidx-lifecycle-runtime-ktx = "2.6.2"
spotless = "6.22.0"
ktlint = "0.50.0"
publish = "0.25.3"
-dokka = "1.9.0"
+dokka = "1.9.10"
moko-resources = "0.23.0"
ktor = "2.3.5"
okio = "3.6.0"
uri-kmp = "0.0.15"
kermit = "2.0.1"
androidsvg = "1.4"
-benchmark = "1.2.0-rc02"
+benchmark = "1.2.0"
junit = "4.13.2"
androidx-test-junit = "1.1.5"
androidx-test-espresso = "3.5.1"
androidx-test-uiautomator = "2.2.0"
profileinstaller = "1.3.1"
-roborazzi = "1.6.0-rc-1"
-robolectric = "4.10.3"
+roborazzi = "1.8.0-alpha-4"
+robolectric = "4.11.1"
poko = "0.15.0"
turbine = "1.0.0"
diff --git a/image-loader/build.gradle.kts b/image-loader/build.gradle.kts
index 96d2ab20..5d95c469 100644
--- a/image-loader/build.gradle.kts
+++ b/image-loader/build.gradle.kts
@@ -12,10 +12,15 @@ plugins {
}
kotlin {
- @Suppress("UNUSED_VARIABLE")
sourceSets {
- val commonMain by getting {
+ all {
+ languageSettings {
+ optIn("kotlin.experimental.ExperimentalNativeApi")
+ }
+ }
+ commonMain {
dependencies {
+ api(compose.foundation)
api(compose.ui)
api(libs.kotlinx.coroutines.core)
api(libs.okio)
@@ -23,7 +28,7 @@ kotlin {
api(libs.uri.kmp)
}
}
- val commonTest by getting {
+ commonTest {
dependencies {
implementation(kotlin("test"))
implementation(libs.bundles.test.common)
@@ -31,12 +36,7 @@ kotlin {
implementation(compose.ui)
}
}
- val jvmMain by getting {
- dependencies {
- implementation(libs.ktor.client.okhttp)
- }
- }
- val androidMain by getting {
+ androidMain {
dependencies {
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.core.ktx)
@@ -45,18 +45,18 @@ kotlin {
implementation(libs.androidsvg)
}
}
- val androidUnitTest by getting {
+ androidUnitTest {
dependencies {
implementation(compose.desktop.uiTestJUnit4)
implementation(libs.bundles.test.android)
}
}
- val desktopMain by getting {
+ desktopMain {
dependencies {
implementation(libs.kotlinx.coroutines.swing)
}
}
- val desktopTest by getting {
+ desktopTest {
languageSettings {
enableLanguageFeature(LanguageFeature.ContextReceivers.name)
}
@@ -69,29 +69,19 @@ kotlin {
}
}
}
- val appleMain by getting {
- dependencies {
- implementation(libs.ktor.client.darwin)
- }
- }
- val jsMain by getting {
- dependencies {
- implementation(libs.ktor.client.js)
- }
- }
val noJsMain by creating {
- dependsOn(commonMain)
- jvmMain.dependsOn(this)
- appleMain.dependsOn(this)
+ dependsOn(commonMain.get())
+ jvmMain.get().dependsOn(this)
+ appleMain.get().dependsOn(this)
dependencies {
implementation(libs.androidx.collection)
}
}
val noAndroidMain by creating {
- dependsOn(commonMain)
- desktopMain.dependsOn(this)
- appleMain.dependsOn(this)
- jsMain.dependsOn(this)
+ dependsOn(commonMain.get())
+ desktopMain.get().dependsOn(this)
+ appleMain.get().dependsOn(this)
+ jsMain.get().dependsOn(this)
}
}
sourceSets.all {
diff --git a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/BitmapFactoryDecoder.kt b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/BitmapFactoryDecoder.kt
index 4dddf97c..f482fea6 100644
--- a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/BitmapFactoryDecoder.kt
+++ b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/BitmapFactoryDecoder.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build.VERSION.SDK_INT
+import androidx.compose.ui.geometry.isSpecified
import com.seiko.imageloader.option.Options
import com.seiko.imageloader.option.androidContext
import com.seiko.imageloader.util.DEFAULT_MAX_PARALLELISM
@@ -14,7 +15,7 @@ import com.seiko.imageloader.util.calculateInSampleSize
import com.seiko.imageloader.util.computeSizeMultiplier
import com.seiko.imageloader.util.isRotated
import com.seiko.imageloader.util.isSwapped
-import com.seiko.imageloader.util.toBitmapConfig
+import com.seiko.imageloader.util.toAndroidConfig
import com.seiko.imageloader.util.toSoftware
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore
@@ -78,14 +79,14 @@ class BitmapFactoryDecoder private constructor(
// Reverse the EXIF transformations to get the original image.
val bitmap = ExifUtils.reverseTransformations(outBitmap, exifData)
- return DecodeResult.Bitmap(
+ return DecodeResult.OfBitmap(
bitmap = bitmap,
)
}
/** Compute and set [BitmapFactory.Options.inPreferredConfig]. */
private fun BitmapFactory.Options.configureConfig(exifData: ExifData) {
- var config = options.imageConfig.toBitmapConfig()
+ var config = options.bitmapConfig.toAndroidConfig()
// Disable hardware bitmaps if we need to perform EXIF transformations.
if (exifData.isFlipped || exifData.isRotated) {
@@ -118,7 +119,15 @@ class BitmapFactoryDecoder private constructor(
// EXIF transformations (but before sampling).
val srcWidth = if (exifData.isSwapped) outHeight else outWidth
val srcHeight = if (exifData.isSwapped) outWidth else outHeight
- val (dstWidth, dstHeight) = calculateDstSize(srcWidth, srcHeight, options.maxImageSize)
+
+ val maxImageSize = if (options.size.isSpecified && !options.size.isEmpty()) {
+ minOf(options.size.width, options.size.height).toInt()
+ .coerceAtMost(options.maxImageSize)
+ } else {
+ options.maxImageSize
+ }
+ val (dstWidth, dstHeight) = calculateDstSize(srcWidth, srcHeight, maxImageSize)
+
// Calculate the image's sample size.
inSampleSize = calculateInSampleSize(
srcWidth = srcWidth,
@@ -163,7 +172,7 @@ class BitmapFactoryDecoder private constructor(
private val parallelismLock = Semaphore(maxParallelism)
- override suspend fun create(source: DecodeSource, options: Options): Decoder {
+ override fun create(source: DecodeSource, options: Options): Decoder {
return BitmapFactoryDecoder(
context = context ?: options.androidContext,
source = source,
diff --git a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/GifDecoder.kt b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/GifDecoder.kt
index 5902c5b7..27354775 100644
--- a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/GifDecoder.kt
+++ b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/GifDecoder.kt
@@ -10,7 +10,7 @@ import com.seiko.imageloader.util.FrameDelayRewritingSource
import com.seiko.imageloader.util.MovieDrawable
import com.seiko.imageloader.util.isGif
import com.seiko.imageloader.util.isHardware
-import com.seiko.imageloader.util.toBitmapConfig
+import com.seiko.imageloader.util.toAndroidConfig
import kotlinx.coroutines.runInterruptible
import okio.BufferedSource
import okio.buffer
@@ -40,7 +40,7 @@ class GifDecoder private constructor(
val movie: Movie? = bufferedSource.use { Movie.decodeStream(it.inputStream()) }
check(movie != null && movie.width() > 0 && movie.height() > 0) { "Failed to decode GIF." }
- val config = options.imageConfig.toBitmapConfig()
+ val config = options.bitmapConfig.toAndroidConfig()
val movieConfig = when {
// movie.isOpaque && options.allowRgb565 -> Bitmap.Config.RGB_565
config.isHardware -> Bitmap.Config.ARGB_8888
@@ -64,7 +64,7 @@ class GifDecoder private constructor(
// Set the animated transformation to be applied on each frame.
// drawable.setAnimatedTransformation(options.parameters.animatedTransformation())
- DecodeResult.Image(
+ DecodeResult.OfImage(
image = drawable.toImage(),
)
}
@@ -72,7 +72,7 @@ class GifDecoder private constructor(
class Factory @JvmOverloads constructor(
private val enforceMinimumFrameDelay: Boolean = true,
) : Decoder.Factory {
- override suspend fun create(source: DecodeSource, options: Options): Decoder? {
+ override fun create(source: DecodeSource, options: Options): Decoder? {
if (!options.playAnimate) return null
if (!isGif(source.source)) return null
return GifDecoder(source, options, enforceMinimumFrameDelay)
diff --git a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/ImageDecoderDecoder.kt b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/ImageDecoderDecoder.kt
index 624efb92..4bc6c91d 100644
--- a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/ImageDecoderDecoder.kt
+++ b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/ImageDecoderDecoder.kt
@@ -21,7 +21,7 @@ import com.seiko.imageloader.util.isAnimatedHeif
import com.seiko.imageloader.util.isAnimatedWebP
import com.seiko.imageloader.util.isGif
import com.seiko.imageloader.util.isHardware
-import com.seiko.imageloader.util.toBitmapConfig
+import com.seiko.imageloader.util.toAndroidConfig
import kotlinx.coroutines.runInterruptible
import okio.BufferedSource
import okio.FileSystem
@@ -62,7 +62,7 @@ class ImageDecoderDecoder private constructor(
imageDecoder?.close()
wrapDecodeSource.close()
}
- DecodeResult.Image(
+ DecodeResult.OfImage(
image = wrapDrawable(drawable).toImage(),
)
}
@@ -101,7 +101,7 @@ class ImageDecoderDecoder private constructor(
}
private fun ImageDecoder.configureImageDecoderProperties() {
- val config = options.imageConfig.toBitmapConfig()
+ val config = options.bitmapConfig.toAndroidConfig()
allocator = if (config.isHardware) {
ImageDecoder.ALLOCATOR_HARDWARE
} else {
@@ -175,7 +175,7 @@ class ImageDecoderDecoder private constructor(
private val enforceMinimumFrameDelay: Boolean = true,
) : Decoder.Factory {
- override suspend fun create(source: DecodeSource, options: Options): Decoder? {
+ override fun create(source: DecodeSource, options: Options): Decoder? {
if (!options.playAnimate) return null
if (!isApplicable(source.source)) return null
return ImageDecoderDecoder(
diff --git a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/SvgDecoder.kt b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/SvgDecoder.kt
index 9a6584ad..1a94751e 100644
--- a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/SvgDecoder.kt
+++ b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/decoder/SvgDecoder.kt
@@ -20,11 +20,8 @@ class SvgDecoder private constructor(
override suspend fun decode(): DecodeResult {
val svg = SVG.getFromInputStream(source.source.inputStream())
- val requestSize = options.sizeResolver.run {
- density.size()
- }
- return DecodeResult.Painter(
- painter = SVGPainter(svg, density, requestSize),
+ return DecodeResult.OfPainter(
+ painter = SVGPainter(svg, density, options.size),
)
}
@@ -32,7 +29,7 @@ class SvgDecoder private constructor(
private val density: Density? = null,
) : Decoder.Factory {
- override suspend fun create(source: DecodeSource, options: Options): Decoder? {
+ override fun create(source: DecodeSource, options: Options): Decoder? {
if (!isApplicable(source)) return null
return SvgDecoder(
source = source,
diff --git a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/AssetUriFetcher.kt b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/AssetUriFetcher.kt
index a8a7cc63..bbca5e1b 100644
--- a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/AssetUriFetcher.kt
+++ b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/AssetUriFetcher.kt
@@ -21,7 +21,7 @@ class AssetUriFetcher private constructor(
override suspend fun fetch(): FetchResult {
val path = data.pathSegments.drop(1).joinToString("/")
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = context.assets.open(path).source().buffer(),
extra = extraData {
mimeType(MimeTypeMap.getSingleton().getMimeTypeFromUrl(path))
diff --git a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/ContentUriFetcher.kt b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/ContentUriFetcher.kt
index d58f2848..07137eaf 100644
--- a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/ContentUriFetcher.kt
+++ b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/ContentUriFetcher.kt
@@ -46,7 +46,7 @@ class ContentUriFetcher private constructor(
checkNotNull(stream) { "Unable to open '$data'." }
}
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = inputStream.source().buffer(),
extra = extraData {
mimeType(contentResolver.getType(androidUri))
diff --git a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/DrawableFetcher.kt b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/DrawableFetcher.kt
index a98ee675..0c352809 100644
--- a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/DrawableFetcher.kt
+++ b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/DrawableFetcher.kt
@@ -10,11 +10,11 @@ class DrawableFetcher private constructor(
) : Fetcher {
override suspend fun fetch(): FetchResult {
return if (data is BitmapDrawable) {
- FetchResult.Bitmap(
+ FetchResult.OfBitmap(
bitmap = data.bitmap,
)
} else {
- FetchResult.Image(
+ FetchResult.OfImage(
image = data.toImage(),
)
}
diff --git a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/ResourceUriFetcher.kt b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/ResourceUriFetcher.kt
index 2386d312..34f9bd5c 100644
--- a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/ResourceUriFetcher.kt
+++ b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/component/fetcher/ResourceUriFetcher.kt
@@ -26,7 +26,7 @@ import com.seiko.imageloader.option.androidContext
import com.seiko.imageloader.toImage
import com.seiko.imageloader.util.DrawableUtils
import com.seiko.imageloader.util.getMimeTypeFromUrl
-import com.seiko.imageloader.util.toBitmapConfig
+import com.seiko.imageloader.util.toAndroidConfig
import okio.buffer
import okio.source
import org.xmlpull.v1.XmlPullParser
@@ -63,27 +63,27 @@ class ResourceUriFetcher private constructor(
val isVector = drawable.isVector
if (isVector) {
- FetchResult.Bitmap(
+ FetchResult.OfBitmap(
bitmap = DrawableUtils.convertToBitmap(
drawable = drawable,
- config = options.imageConfig.toBitmapConfig(),
+ config = options.bitmapConfig.toAndroidConfig(),
scale = options.scale,
allowInexactSize = options.allowInexactSize,
),
)
} else if (drawable is BitmapDrawable) {
- FetchResult.Bitmap(
+ FetchResult.OfBitmap(
bitmap = drawable.bitmap,
)
} else {
- FetchResult.Image(
+ FetchResult.OfImage(
image = drawable.toImage(),
)
}
} else {
val typedValue = TypedValue()
val inputStream = resources.openRawResource(resId, typedValue)
- FetchResult.Source(
+ FetchResult.OfSource(
source = inputStream.source().buffer(),
extra = extraData {
mimeType(mimeType)
diff --git a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/util/Bitmap.kt b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/util/Bitmap.kt
index 2cd383d2..ef0bc2fb 100644
--- a/image-loader/src/androidMain/kotlin/com/seiko/imageloader/util/Bitmap.kt
+++ b/image-loader/src/androidMain/kotlin/com/seiko/imageloader/util/Bitmap.kt
@@ -3,7 +3,7 @@ package com.seiko.imageloader.util
import android.graphics.Bitmap
import android.os.Build
import android.util.Log
-import com.seiko.imageloader.option.Options
+import com.seiko.imageloader.BitmapConfig
internal val Bitmap.safeConfig: Bitmap.Config
get() = config
@@ -11,16 +11,16 @@ internal val Bitmap.safeConfig: Bitmap.Config
internal val Bitmap.Config.isHardware: Boolean
get() = Build.VERSION.SDK_INT >= 26 && this == Bitmap.Config.HARDWARE
-internal fun Options.ImageConfig.toBitmapConfig(): Bitmap.Config = when (this) {
- Options.ImageConfig.ALPHA_8 -> Bitmap.Config.ALPHA_8
- Options.ImageConfig.ARGB_8888 -> Bitmap.Config.ARGB_8888
- Options.ImageConfig.RGBA_F16 -> if (Build.VERSION.SDK_INT >= 26) {
+internal fun BitmapConfig.toAndroidConfig(): Bitmap.Config = when (this) {
+ BitmapConfig.ALPHA_8 -> Bitmap.Config.ALPHA_8
+ BitmapConfig.ARGB_8888 -> Bitmap.Config.ARGB_8888
+ BitmapConfig.RGBA_F16 -> if (Build.VERSION.SDK_INT >= 26) {
Bitmap.Config.RGBA_F16
} else {
Log.w("ImageConfig", "ImageConfig.RGBA_F16 not support in android less than API 26")
Bitmap.Config.ARGB_8888
}
- Options.ImageConfig.HARDWARE -> if (Build.VERSION.SDK_INT >= 26) {
+ BitmapConfig.HARDWARE -> if (Build.VERSION.SDK_INT >= 26) {
Bitmap.Config.HARDWARE
} else {
Log.w("ImageConfig", "ImageConfig.HARDWARE not support in android less than API 26")
diff --git a/image-loader/src/androidMain/singleton/com/seiko/imageloader/ImageLoaderFactory.kt b/image-loader/src/androidMain/singleton/com/seiko/imageloader/ImageLoaderFactory.kt
index 8ee4501c..9e925390 100644
--- a/image-loader/src/androidMain/singleton/com/seiko/imageloader/ImageLoaderFactory.kt
+++ b/image-loader/src/androidMain/singleton/com/seiko/imageloader/ImageLoaderFactory.kt
@@ -22,5 +22,5 @@ val Context.imageLoader: ImageLoader
private fun Context.newImageLoader(): ImageLoader {
currentImageLoader?.let { return it }
return (applicationContext as? ImageLoaderFactory)?.newImageLoader()
- ?: ImageLoader.DefaultAndroid(this)
+ ?: ImageLoader.createDefaultAndroid(this)
}
diff --git a/image-loader/src/androidMain/singleton/com/seiko/imageloader/LocalImageLoader.android.kt b/image-loader/src/androidMain/singleton/com/seiko/imageloader/LocalImageLoader.android.kt
index e31f8a62..8d011593 100644
--- a/image-loader/src/androidMain/singleton/com/seiko/imageloader/LocalImageLoader.android.kt
+++ b/image-loader/src/androidMain/singleton/com/seiko/imageloader/LocalImageLoader.android.kt
@@ -29,8 +29,7 @@ actual fun createImageLoaderProvidableCompositionLocal() = ImageLoaderProvidable
delegate = staticCompositionLocalOf { null },
)
-@Suppress("FunctionName")
-fun ImageLoader.Companion.DefaultAndroid(context: Context): ImageLoader {
+fun ImageLoader.Companion.createDefaultAndroid(context: Context): ImageLoader {
return ImageLoader {
options {
androidContext(context.applicationContext)
diff --git a/image-loader/src/androidUnitTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlTest.kt b/image-loader/src/androidUnitTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlTest.kt
index 72df9e62..0fe05652 100644
--- a/image-loader/src/androidUnitTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlTest.kt
+++ b/image-loader/src/androidUnitTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlTest.kt
@@ -6,12 +6,7 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
-import com.github.takahirom.roborazzi.InternalRoborazziApi
-import com.github.takahirom.roborazzi.RoborazziContext
-import com.github.takahirom.roborazzi.RoborazziOptions
-import com.github.takahirom.roborazzi.captureRoboImage
-import org.junit.Before
+import com.github.takahirom.roborazzi.RoborazziRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -26,31 +21,23 @@ class ChangeImageUrlTest : ChangeImageUrlCommonTest() {
@get:Rule
val composeTestRule = createAndroidComposeRule()
- @OptIn(ExperimentalRoborazziApi::class, InternalRoborazziApi::class)
- @Before
- fun initRoborazziConfig() {
- RoborazziContext.setRuleOverrideOutputDirectory(
- outputDirectory = "src/androidUnitTest/snapshots/images",
- )
- }
+ @get:Rule
+ val roborazziRule = RoborazziRule(
+ composeRule = composeTestRule,
+ captureRoot = composeTestRule.onRoot(),
+ options = RoborazziRule.Options(
+ captureType = RoborazziRule.CaptureType.AllImage(),
+ outputDirectoryPath = "build/outputs/roborazzi/android",
+ ),
+ )
@Test
fun test_image_change() = with(composeTestRule) {
setContent {
TestUI()
}
- val roborazziOptions = RoborazziOptions(
- captureType = RoborazziOptions.CaptureType.Screenshot(),
- compareOptions = RoborazziOptions.CompareOptions(changeThreshold = 0F),
- )
- onRoot().captureRoboImage(
- roborazziOptions = roborazziOptions,
- )
(0..2).forEach { _ ->
onNodeWithTag(BUTTON_TAG).performClick()
- onRoot().captureRoboImage(
- roborazziOptions = roborazziOptions,
- )
}
}
}
diff --git a/image-loader/src/androidUnitTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotTest.kt b/image-loader/src/androidUnitTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotTest.kt
index 939bcc8e..c1eff7aa 100644
--- a/image-loader/src/androidUnitTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotTest.kt
+++ b/image-loader/src/androidUnitTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotTest.kt
@@ -25,7 +25,7 @@ class ComposeScreenShotTest : ComposeScreenShotCommonTest() {
captureRoot = composeTestRule.onRoot(),
options = RoborazziRule.Options(
captureType = RoborazziRule.CaptureType.LastImage(),
- outputDirectoryPath = "src/androidUnitTest/snapshots/images",
+ outputDirectoryPath = "build/outputs/roborazzi/android",
),
)
diff --git a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change.png b/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change.png
deleted file mode 100644
index bab35f16..00000000
--- a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:8efefc34190788fa550753f68a91e24901c35db9d9f5aae00f037c6d93ee6a81
-size 244
diff --git a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_2.png b/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_2.png
deleted file mode 100644
index 7089e409..00000000
--- a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_2.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:bb405b2da07222a107615a7a375d4334e87061124154af487fa7e259df788e51
-size 245
diff --git a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_3.png b/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_3.png
deleted file mode 100644
index c99cc537..00000000
--- a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_3.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:93e0b25a633563cf1482934edd6776f144b5b88d92e1e88d71a3c17672266470
-size 243
diff --git a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_4.png b/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_4.png
deleted file mode 100644
index 4cc5ca44..00000000
--- a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_4.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:925fd1a1dccd1de52e89bd601b5854664362a5803caf491163e0f67505d347d6
-size 246
diff --git a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_error_painter.png b/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_error_painter.png
deleted file mode 100644
index cd24e6a4..00000000
--- a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_error_painter.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:e025c7f7560d3dcef1bb0d7371573048c5fc10f35435f30733f31ee5a55d6340
-size 310
diff --git a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_load_image.png b/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_load_image.png
deleted file mode 100644
index ccfa73b3..00000000
--- a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_load_image.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:4af898ef87c20b18fcafb2deaa5d50689182ff9f29e3ee8d79bb245778e56256
-size 816
diff --git a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_placeholder_painter.png b/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_placeholder_painter.png
deleted file mode 100644
index e2aeb82a..00000000
--- a/image-loader/src/androidUnitTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_placeholder_painter.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a5154ac23b0ee9042e5f107c05782349f9aa544a8b0367b4145d2b767ef9480f
-size 311
diff --git a/image-loader/src/appleMain/kotlin/com/seiko/imageloader/util/Platform.apple.kt b/image-loader/src/appleMain/kotlin/com/seiko/imageloader/util/Platform.apple.kt
index fee06707..49431509 100644
--- a/image-loader/src/appleMain/kotlin/com/seiko/imageloader/util/Platform.apple.kt
+++ b/image-loader/src/appleMain/kotlin/com/seiko/imageloader/util/Platform.apple.kt
@@ -1,20 +1,7 @@
package com.seiko.imageloader.util
-import io.ktor.client.engine.HttpClientEngine
-import io.ktor.client.engine.darwin.Darwin
-import kotlinx.atomicfu.locks.SynchronizedObject
import okio.FileSystem
-import kotlin.experimental.ExperimentalNativeApi
-@OptIn(ExperimentalNativeApi::class)
actual typealias WeakReference = kotlin.native.ref.WeakReference
-actual typealias LockObject = SynchronizedObject
-
-internal actual inline fun synchronized(lock: LockObject, block: () -> R): R {
- return kotlinx.atomicfu.locks.synchronized(lock, block)
-}
-
-internal actual val httpEngine: HttpClientEngine get() = Darwin.create()
-
internal actual val defaultFileSystem: FileSystem? get() = FileSystem.SYSTEM
diff --git a/image-loader/src/appleMain/singleton/com/seiko/imageloader/LocalImageLoader.apple.kt b/image-loader/src/appleMain/singleton/com/seiko/imageloader/LocalImageLoader.apple.kt
index c32b70df..a550da4c 100644
--- a/image-loader/src/appleMain/singleton/com/seiko/imageloader/LocalImageLoader.apple.kt
+++ b/image-loader/src/appleMain/singleton/com/seiko/imageloader/LocalImageLoader.apple.kt
@@ -9,12 +9,12 @@ import platform.Foundation.NSUserDomainMask
actual fun createImageLoaderProvidableCompositionLocal() = ImageLoaderProvidableCompositionLocal(
delegate = staticCompositionLocalOf {
- ImageLoader.DefaultIOS
+ ImageLoader.createDefaultIOS()
},
)
-val ImageLoader.Companion.DefaultIOS: ImageLoader
- get() = ImageLoader {
+fun ImageLoader.Companion.createDefaultIOS(): ImageLoader {
+ return ImageLoader {
components {
setupDefaultComponents()
}
@@ -29,6 +29,7 @@ val ImageLoader.Companion.DefaultIOS: ImageLoader
}
}
}
+}
private fun getCacheDir(): String {
return NSSearchPathForDirectoriesInDomains(
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Bitmap.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Bitmap.kt
index 6ed2d167..930c0b54 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Bitmap.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Bitmap.kt
@@ -12,6 +12,18 @@ internal expect val Bitmap.size: Int
internal expect val Bitmap.identityHashCode: Int
+enum class BitmapConfig {
+ ALPHA_8,
+ ARGB_8888,
+ RGBA_F16,
+ HARDWARE,
+ ;
+
+ companion object {
+ val Default = ARGB_8888
+ }
+}
+
expect fun Bitmap.asImageBitmap(): ImageBitmap
fun Bitmap.toPainter(filterQuality: FilterQuality = DefaultFilterQuality): Painter {
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ImageLoader.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ImageLoader.kt
index 2e4ca4b8..3c03125e 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ImageLoader.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ImageLoader.kt
@@ -1,35 +1,26 @@
package com.seiko.imageloader
import androidx.compose.runtime.Immutable
+import androidx.compose.ui.geometry.isSpecified
import com.seiko.imageloader.intercept.InterceptorChainImpl
import com.seiko.imageloader.model.ImageAction
import com.seiko.imageloader.model.ImageEvent
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.ImageResult
+import com.seiko.imageloader.option.Options
import com.seiko.imageloader.util.ioDispatcher
import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.transformLatest
import kotlin.coroutines.CoroutineContext
@Immutable
interface ImageLoader {
val config: ImageLoaderConfig
- fun async(requestFlow: Flow): Flow
-
- fun async(request: ImageRequest): Flow = async(flowOf(request))
-
- @Deprecated("", ReplaceWith("Use imageloader.async(request).filterIsInstance().first()"))
- suspend fun execute(request: ImageRequest): ImageResult {
- return async(request).filterIsInstance().first()
- }
+ fun async(request: ImageRequest): Flow
companion object
}
@@ -47,21 +38,26 @@ private class RealImageLoader(
private val requestCoroutineContext: CoroutineContext,
override val config: ImageLoaderConfig,
) : ImageLoader {
- @OptIn(ExperimentalCoroutinesApi::class)
- override fun async(requestFlow: Flow) = requestFlow
- .transformLatest { request ->
- if (!request.skipEvent) {
- emit(ImageEvent.Start)
- }
- val chain = InterceptorChainImpl(
- initialRequest = request,
- config = config,
- flowCollector = this,
- )
- emit(chain.proceed(request))
- }.catch {
- if (it !is CancellationException) {
- emit(ImageResult.Error(it))
+ override fun async(request: ImageRequest) = flow {
+ if (!request.skipEvent) {
+ emit(ImageEvent.Start)
+ }
+ val initialSize = request.sizeResolver.size()
+ val options = Options(config.defaultOptions) {
+ if (initialSize.isSpecified) {
+ size = initialSize
}
- }.flowOn(requestCoroutineContext)
+ }
+ val chain = InterceptorChainImpl(
+ initialRequest = request,
+ initialOptions = options,
+ config = config,
+ flowCollector = this,
+ )
+ emit(chain.proceed(request))
+ }.catch {
+ if (it !is CancellationException) {
+ emit(ImageResult.OfError(it))
+ }
+ }.flowOn(requestCoroutineContext)
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/RememberExt.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Remember.ext.kt
similarity index 94%
rename from image-loader/src/commonMain/kotlin/com/seiko/imageloader/RememberExt.kt
rename to image-loader/src/commonMain/kotlin/com/seiko/imageloader/Remember.ext.kt
index 66015ecf..fd8edf2f 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/RememberExt.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Remember.ext.kt
@@ -69,8 +69,8 @@ fun rememberImagePainter(
request: ImageRequest,
imageLoader: ImageLoader = LocalImageLoader.current,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
- placeholderPainter: (@Composable () -> Painter)? = request.placeholderPainter,
- errorPainter: (@Composable () -> Painter)? = request.errorPainter,
+ placeholderPainter: (@Composable () -> Painter)? = null,
+ errorPainter: (@Composable () -> Painter)? = null,
): Painter {
val action by rememberImageAction(request, imageLoader)
return rememberImageActionPainter(
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Remember.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Remember.kt
index 795760b7..f8eabe33 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Remember.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/Remember.kt
@@ -5,7 +5,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.withFrameMillis
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.FilterQuality
@@ -17,27 +16,6 @@ import com.seiko.imageloader.model.ImageEvent
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.ImageResult
import com.seiko.imageloader.util.AnimationPainter
-import kotlinx.coroutines.flow.Flow
-
-@Composable
-fun rememberImageAction(
- request: State,
- imageLoader: ImageLoader = LocalImageLoader.current,
-): State {
- return remember(request, imageLoader) {
- imageLoader.async(snapshotFlow { request.value })
- }.collectAsState(ImageEvent.Start)
-}
-
-@Composable
-fun rememberImageAction(
- request: Flow,
- imageLoader: ImageLoader = LocalImageLoader.current,
-): State {
- return remember(request, imageLoader) {
- imageLoader.async(request)
- }.collectAsState(ImageEvent.Start)
-}
@Composable
fun rememberImageAction(
@@ -57,12 +35,9 @@ fun rememberImageActionPainter(
errorPainter: (@Composable () -> Painter)? = null,
): Painter {
return when (action) {
- is ImageEvent -> placeholderPainter?.invoke() ?: EmptyPainter
- is ImageResult -> rememberImageResultPainter(
- result = action,
- filterQuality = filterQuality,
- errorPainter = errorPainter,
- )
+ is ImageAction.Success -> rememberImageSuccessPainter(action, filterQuality)
+ is ImageAction.Loading -> placeholderPainter?.invoke() ?: EmptyPainter
+ is ImageAction.Failure -> errorPainter?.invoke() ?: EmptyPainter
}
}
@@ -73,22 +48,30 @@ fun rememberImageResultPainter(
errorPainter: (@Composable () -> Painter)? = null,
): Painter {
return when (result) {
- is ImageResult.Painter -> remember(result) {
- result.painter
+ is ImageAction.Success -> rememberImageSuccessPainter(result, filterQuality)
+ is ImageAction.Failure -> errorPainter?.invoke() ?: EmptyPainter
+ }
+}
+
+@Composable
+fun rememberImageSuccessPainter(
+ action: ImageAction.Success,
+ filterQuality: FilterQuality = DefaultFilterQuality,
+): Painter {
+ return when (action) {
+ is ImageResult.OfPainter -> remember(action) {
+ action.painter
}
- is ImageResult.Bitmap -> remember(result, filterQuality) {
- result.bitmap.toPainter(filterQuality)
+ is ImageResult.OfBitmap -> remember(action, filterQuality) {
+ action.bitmap.toPainter(filterQuality)
}
- is ImageResult.Image -> remember(result) {
- result.image.toPainter()
+ is ImageResult.OfImage -> remember(action) {
+ action.image.toPainter()
}
- is ImageResult.Error,
- is ImageResult.Source,
- -> errorPainter?.invoke() ?: EmptyPainter
}.also { painter ->
- if (painter is AnimationPainter) {
+ if (painter is AnimationPainter && painter.isPlay()) {
LaunchedEffect(painter) {
- while (painter.isPlay()) {
+ while (painter.nextPlay()) {
withFrameMillis { frameTimeMillis ->
painter.update(frameTimeMillis)
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/RememberDeprecated.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/RememberDeprecated.kt
deleted file mode 100644
index 0bbb9839..00000000
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/RememberDeprecated.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-package com.seiko.imageloader
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.graphics.FilterQuality
-import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.layout.ContentScale
-import com.seiko.imageloader.model.ImageRequest
-import com.seiko.imageloader.option.toScale
-
-@Deprecated(
- message = "move contentScale into ImageRequest",
- replaceWith = ReplaceWith("ImageRequest { scale(contentScale.toScale()) }"),
-)
-@Composable
-fun rememberImagePainter(
- url: String,
- contentScale: ContentScale,
- imageLoader: ImageLoader = LocalImageLoader.current,
- filterQuality: FilterQuality = DefaultFilterQuality,
- placeholderPainter: (@Composable () -> Painter)? = null,
- errorPainter: (@Composable () -> Painter)? = null,
-): Painter {
- return rememberImagePainter(
- request = remember(url, contentScale, placeholderPainter, errorPainter) {
- ImageRequest {
- data(url)
- scale(contentScale.toScale())
- placeholderPainter?.let { placeholderPainter(it) }
- errorPainter?.let { errorPainter(it) }
- }
- },
- imageLoader = imageLoader,
- filterQuality = filterQuality,
- )
-}
-
-@Deprecated(
- message = "move contentScale into ImageRequest",
- replaceWith = ReplaceWith("ImageRequest { scale(contentScale.toScale()) }"),
-)
-@Composable
-fun rememberImagePainter(
- resId: Int,
- contentScale: ContentScale,
- imageLoader: ImageLoader = LocalImageLoader.current,
- filterQuality: FilterQuality = DefaultFilterQuality,
- placeholderPainter: (@Composable () -> Painter)? = null,
- errorPainter: (@Composable () -> Painter)? = null,
-): Painter {
- return rememberImagePainter(
- request = remember(resId, contentScale, placeholderPainter, errorPainter) {
- ImageRequest {
- data(resId)
- scale(contentScale.toScale())
- placeholderPainter?.let { placeholderPainter(it) }
- errorPainter?.let { errorPainter(it) }
- }
- },
- imageLoader = imageLoader,
- filterQuality = filterQuality,
- )
-}
-
-@Deprecated(
- message = "move contentScale into ImageRequest",
- replaceWith = ReplaceWith("ImageRequest { scale(contentScale.toScale()) }"),
-)
-@Composable
-fun rememberImagePainter(
- request: ImageRequest,
- contentScale: ContentScale,
- imageLoader: ImageLoader = LocalImageLoader.current,
- filterQuality: FilterQuality = DefaultFilterQuality,
-): Painter {
- return rememberImagePainter(
- request = remember(request) {
- ImageRequest(request) {
- scale(contentScale.toScale())
- }
- },
- imageLoader = imageLoader,
- filterQuality = filterQuality,
- )
-}
-
-@Deprecated("Use rememberImageAction&rememberImageActionPainter or rememberImagePainter")
-@Composable
-fun rememberAsyncImagePainter(
- url: String,
- contentScale: ContentScale = ContentScale.Fit,
- imageLoader: ImageLoader = LocalImageLoader.current,
- filterQuality: FilterQuality = DefaultFilterQuality,
-): Painter {
- val request = remember(url) {
- ImageRequest {
- data(url)
- scale(contentScale.toScale())
- }
- }
- val action by rememberImageAction(request, imageLoader)
- return rememberImageActionPainter(action, filterQuality)
-}
-
-@Deprecated("Use rememberImageAction&rememberImageActionPainter or rememberImagePainter")
-@Composable
-fun rememberAsyncImagePainter(
- resId: Int,
- contentScale: ContentScale = ContentScale.Fit,
- imageLoader: ImageLoader = LocalImageLoader.current,
- filterQuality: FilterQuality = DefaultFilterQuality,
-): Painter {
- val request = remember(resId) {
- ImageRequest {
- data(resId)
- scale(contentScale.toScale())
- }
- }
- val action by rememberImageAction(request, imageLoader)
- return rememberImageActionPainter(action, filterQuality)
-}
-
-@Deprecated("Use rememberImageAction&rememberImageActionPainter or rememberImagePainter")
-@Composable
-fun rememberAsyncImagePainter(
- request: ImageRequest,
- contentScale: ContentScale = ContentScale.Fit,
- imageLoader: ImageLoader = LocalImageLoader.current,
- filterQuality: FilterQuality = DefaultFilterQuality,
-): Painter {
- val newRequest = remember(request) {
- ImageRequest(request) {
- scale(contentScale.toScale())
- }
- }
- val action by rememberImageAction(newRequest, imageLoader)
- return rememberImageActionPainter(action, filterQuality)
-}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/cache/disk/DiskLruCache.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/cache/disk/DiskLruCache.kt
index e23dd803..63320551 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/cache/disk/DiskLruCache.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/cache/disk/DiskLruCache.kt
@@ -1,18 +1,19 @@
package com.seiko.imageloader.cache.disk
import com.seiko.imageloader.cache.disk.DiskLruCache.Editor
-import com.seiko.imageloader.util.LockObject
import com.seiko.imageloader.util.LruHashMap
import com.seiko.imageloader.util.createFile
import com.seiko.imageloader.util.deleteContents
import com.seiko.imageloader.util.forEachIndices
import com.seiko.imageloader.util.getOrPut
-import com.seiko.imageloader.util.synchronized
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.internal.SynchronizedObject
+import kotlinx.coroutines.internal.synchronized
import kotlinx.coroutines.launch
import okio.BufferedSink
import okio.Closeable
@@ -69,7 +70,7 @@ import okio.buffer
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store.
*/
-@OptIn(ExperimentalCoroutinesApi::class)
+@OptIn(ExperimentalCoroutinesApi::class, InternalCoroutinesApi::class)
internal class DiskLruCache(
fileSystem: FileSystem,
private val directory: Path,
@@ -138,7 +139,7 @@ internal class DiskLruCache(
private var mostRecentTrimFailed = false
private var mostRecentRebuildFailed = false
- private val syncObject = LockObject()
+ private val syncObject = SynchronizedObject()
private val fileSystem = object : ForwardingFileSystem(fileSystem) {
override fun sink(file: Path, mustCreate: Boolean): Sink {
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/cache/memory/WeakMemoryCache.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/cache/memory/WeakMemoryCache.kt
index 979f0365..9996c4f1 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/cache/memory/WeakMemoryCache.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/cache/memory/WeakMemoryCache.kt
@@ -1,11 +1,12 @@
package com.seiko.imageloader.cache.memory
import androidx.compose.ui.graphics.painter.Painter
-import com.seiko.imageloader.util.LockObject
import com.seiko.imageloader.util.WeakReference
import com.seiko.imageloader.util.firstNotNullOfOrNullIndices
import com.seiko.imageloader.util.removeIfIndices
-import com.seiko.imageloader.util.synchronized
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.internal.SynchronizedObject
+import kotlinx.coroutines.internal.synchronized
/**
* An in-memory cache that holds weak references to [Painter]s.
@@ -30,6 +31,7 @@ internal open class EmptyWeakMemoryCache : WeakMemoryCache {
}
/** A [WeakMemoryCache] implementation backed by a [LinkedHashMap]. */
+@OptIn(InternalCoroutinesApi::class)
internal open class RealWeakMemoryCache(
private val valueHashProvider: (V) -> Int,
) : WeakMemoryCache {
@@ -37,7 +39,7 @@ internal open class RealWeakMemoryCache(
internal val cache = LinkedHashMap>>()
private var operationsSinceCleanUp = 0
- private val syncObject = LockObject()
+ private val syncObject = SynchronizedObject()
override val keys: Set
get() = synchronized(syncObject) { cache.keys.toSet() }
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/ComponentRegistry.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/ComponentRegistry.kt
index 96317dd3..1ac0a18a 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/ComponentRegistry.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/ComponentRegistry.kt
@@ -1,5 +1,6 @@
package com.seiko.imageloader.component
+import com.seiko.imageloader.Poko
import com.seiko.imageloader.component.decoder.DecodeSource
import com.seiko.imageloader.component.decoder.Decoder
import com.seiko.imageloader.component.fetcher.Fetcher
@@ -8,11 +9,11 @@ import com.seiko.imageloader.component.mapper.Mapper
import com.seiko.imageloader.option.Options
import com.seiko.imageloader.util.forEachIndices
-class ComponentRegistry internal constructor(
- private val mappers: List>,
- private val keyers: List,
- private val fetcherFactories: List,
- private val decoderFactories: List,
+@Poko class ComponentRegistry internal constructor(
+ val mappers: List>,
+ val keyers: List,
+ val fetcherFactories: List,
+ val decoderFactories: List,
) {
internal fun merge(component: ComponentRegistry) = ComponentRegistry(
mappers = mappers + component.mappers,
@@ -21,13 +22,6 @@ class ComponentRegistry internal constructor(
decoderFactories = decoderFactories + component.decoderFactories,
)
- internal fun newBuilder() = ComponentRegistryBuilder(
- mappers = mappers.toMutableList(),
- keyers = keyers.toMutableList(),
- fetcherFactories = fetcherFactories.toMutableList(),
- decoderFactories = decoderFactories.toMutableList(),
- )
-
fun map(data: Any, options: Options): Any {
var mappedData = data
mappers.forEachIndices { mapper ->
@@ -55,7 +49,7 @@ class ComponentRegistry internal constructor(
throw RuntimeException("Unable to create a fetcher that supports: $data")
}
- suspend fun decode(
+ fun decode(
source: DecodeSource,
options: Options,
startIndex: Int = 0,
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/ComponentRegistryBuilder.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/ComponentRegistryBuilder.kt
index 73d6a62c..55ad49a5 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/ComponentRegistryBuilder.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/ComponentRegistryBuilder.kt
@@ -5,29 +5,33 @@ import com.seiko.imageloader.component.fetcher.Fetcher
import com.seiko.imageloader.component.keyer.Keyer
import com.seiko.imageloader.component.mapper.Mapper
-class ComponentRegistryBuilder(
+class ComponentRegistryBuilder internal constructor(
private val mappers: MutableList> = mutableListOf(),
private val keyers: MutableList = mutableListOf(),
private val fetcherFactories: MutableList = mutableListOf(),
private val decoderFactories: MutableList = mutableListOf(),
) {
+ internal constructor(componentRegistry: ComponentRegistry) : this(
+ mappers = componentRegistry.mappers.toMutableList(),
+ keyers = componentRegistry.keyers.toMutableList(),
+ fetcherFactories = componentRegistry.fetcherFactories.toMutableList(),
+ decoderFactories = componentRegistry.decoderFactories.toMutableList(),
+ )
fun takeFrom(
componentRegistry: ComponentRegistry,
clearComponents: Boolean = false,
) {
- componentRegistry.newBuilder().let {
- if (clearComponents) {
- mappers.clear()
- keyers.clear()
- fetcherFactories.clear()
- decoderFactories.clear()
- }
- mappers.addAll(it.mappers)
- keyers.addAll(it.keyers)
- fetcherFactories.addAll(it.fetcherFactories)
- decoderFactories.addAll(it.decoderFactories)
+ if (clearComponents) {
+ mappers.clear()
+ keyers.clear()
+ fetcherFactories.clear()
+ decoderFactories.clear()
}
+ mappers.addAll(componentRegistry.mappers)
+ keyers.addAll(componentRegistry.keyers)
+ fetcherFactories.addAll(componentRegistry.fetcherFactories)
+ decoderFactories.addAll(componentRegistry.decoderFactories)
}
fun add(mapper: Mapper) {
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/SetupComponents.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/SetupComponents.kt
index 3a2e9cdd..9a27fe35 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/SetupComponents.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/SetupComponents.kt
@@ -4,7 +4,6 @@ import com.seiko.imageloader.component.fetcher.Base64Fetcher
import com.seiko.imageloader.component.fetcher.BitmapFetcher
import com.seiko.imageloader.component.fetcher.KtorUrlFetcher
import com.seiko.imageloader.component.keyer.KtorUrlKeyer
-import com.seiko.imageloader.component.mapper.Base64Mapper
import com.seiko.imageloader.component.mapper.KtorUrlMapper
import com.seiko.imageloader.component.mapper.StringUriMapper
import com.seiko.imageloader.util.httpEngineFactory
@@ -19,7 +18,6 @@ fun ComponentRegistryBuilder.setupKtorComponents(
}
fun ComponentRegistryBuilder.setupBase64Components() {
- add(Base64Mapper())
add(Base64Fetcher.Factory())
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/decoder/Decoder.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/decoder/Decoder.kt
index 9944c65a..350944d1 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/decoder/Decoder.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/decoder/Decoder.kt
@@ -1,28 +1,29 @@
package com.seiko.imageloader.component.decoder
+import androidx.compose.ui.graphics.painter.Painter
+import com.seiko.imageloader.Bitmap
+import com.seiko.imageloader.Image
import com.seiko.imageloader.Poko
import com.seiko.imageloader.model.ImageResult
import com.seiko.imageloader.option.Options
-typealias DecodeSource = ImageResult.Source
+typealias DecodeSource = ImageResult.OfSource
interface Decoder {
suspend fun decode(): DecodeResult?
- interface Factory {
- suspend fun create(source: DecodeSource, options: Options): Decoder?
+ fun interface Factory {
+ fun create(source: DecodeSource, options: Options): Decoder?
}
}
+fun Decoder(block: () -> DecodeResult?) = object : Decoder {
+ override suspend fun decode(): DecodeResult? = block()
+}
+
sealed interface DecodeResult {
- @Poko class Bitmap(
- val bitmap: com.seiko.imageloader.Bitmap,
- ) : DecodeResult
+ @Poko class OfBitmap(val bitmap: Bitmap) : DecodeResult
- @Poko class Image(
- val image: com.seiko.imageloader.Image,
- ) : DecodeResult
+ @Poko class OfImage(val image: Image) : DecodeResult
- @Poko class Painter(
- val painter: androidx.compose.ui.graphics.painter.Painter,
- ) : DecodeResult
+ @Poko class OfPainter(val painter: Painter) : DecodeResult
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/Base64Fetcher.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/Base64Fetcher.kt
index d73518c1..402cf6b3 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/Base64Fetcher.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/Base64Fetcher.kt
@@ -1,29 +1,38 @@
package com.seiko.imageloader.component.fetcher
-import com.seiko.imageloader.component.mapper.Base64Image
import com.seiko.imageloader.model.extraData
import com.seiko.imageloader.model.mimeType
import com.seiko.imageloader.option.Options
+import io.ktor.util.decodeBase64Bytes
import okio.Buffer
class Base64Fetcher private constructor(
- private val data: Base64Image,
+ private val data: String,
) : Fetcher {
override suspend fun fetch(): FetchResult {
- return FetchResult.Source(
- source = Buffer().apply {
- write(data.content)
- },
- extra = extraData {
- mimeType(data.contentType)
- },
- )
+ return data.split(',').let {
+ val contentType = it.firstOrNull()?.removePrefix("data:")?.removeSuffix(";base64")
+ val content = it.last()
+ FetchResult.OfSource(
+ source = Buffer().apply {
+ write(content.decodeBase64Bytes())
+ },
+ extra = extraData {
+ mimeType(contentType)
+ },
+ )
+ }
}
class Factory : Fetcher.Factory {
override fun create(data: Any, options: Options): Fetcher? {
- if (data !is Base64Image) return null
+ if (data !is String) return null
+ if (!isApplicable(data)) return null
return Base64Fetcher(data)
}
+
+ private fun isApplicable(data: String): Boolean {
+ return data.startsWith("data:")
+ }
}
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/BitmapFetcher.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/BitmapFetcher.kt
index 30625f7c..198d6caa 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/BitmapFetcher.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/BitmapFetcher.kt
@@ -7,7 +7,7 @@ class BitmapFetcher private constructor(
private val data: Bitmap,
) : Fetcher {
override suspend fun fetch(): FetchResult {
- return FetchResult.Bitmap(
+ return FetchResult.OfBitmap(
bitmap = data,
)
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/Fetcher.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/Fetcher.kt
index 9bbb4ca5..955cace8 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/Fetcher.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/Fetcher.kt
@@ -1,5 +1,8 @@
package com.seiko.imageloader.component.fetcher
+import androidx.compose.ui.graphics.painter.Painter
+import com.seiko.imageloader.Bitmap
+import com.seiko.imageloader.Image
import com.seiko.imageloader.Poko
import com.seiko.imageloader.model.EmptyExtraData
import com.seiko.imageloader.model.ExtraData
@@ -13,21 +16,19 @@ interface Fetcher {
}
}
+fun Fetcher(block: suspend () -> FetchResult?) = object : Fetcher {
+ override suspend fun fetch(): FetchResult? = block()
+}
+
sealed interface FetchResult {
- @Poko class Source(
+ @Poko class OfSource(
val source: BufferedSource,
val extra: ExtraData = EmptyExtraData,
) : FetchResult
- @Poko class Bitmap(
- val bitmap: com.seiko.imageloader.Bitmap,
- ) : FetchResult
+ @Poko class OfBitmap(val bitmap: Bitmap) : FetchResult
- @Poko class Image(
- val image: com.seiko.imageloader.Image,
- ) : FetchResult
+ @Poko class OfImage(val image: Image) : FetchResult
- @Poko class Painter(
- val painter: androidx.compose.ui.graphics.painter.Painter,
- ) : FetchResult
+ @Poko class OfPainter(val painter: Painter) : FetchResult
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/KtorUrlFetcher.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/KtorUrlFetcher.kt
index 21d23de7..18af6919 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/KtorUrlFetcher.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/fetcher/KtorUrlFetcher.kt
@@ -24,7 +24,7 @@ class KtorUrlFetcher private constructor(
url(httpUrl)
}
if (response.status.isSuccess()) {
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = response.bodyAsChannel().source(),
extra = extraData {
mimeType(response.contentType()?.toString())
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/keyer/KtorUrlKeyer.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/keyer/KtorUrlKeyer.kt
index 5f68ac02..5ec24718 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/keyer/KtorUrlKeyer.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/keyer/KtorUrlKeyer.kt
@@ -1,5 +1,6 @@
package com.seiko.imageloader.component.keyer
+import com.seiko.imageloader.BitmapConfig
import com.seiko.imageloader.option.Options
import com.seiko.imageloader.option.Scale
import com.seiko.imageloader.util.DEFAULT_MAX_IMAGE_SIZE
@@ -23,8 +24,8 @@ class KtorUrlKeyer : Keyer {
if (!options.premultipliedAlpha) {
append("-premultipliedAlpha")
}
- if (options.imageConfig != Options.ImageConfig.ARGB_8888) {
- append("-imageConfig=${options.imageConfig}")
+ if (options.bitmapConfig != BitmapConfig.Default) {
+ append("-imageConfig=${options.bitmapConfig}")
}
if (options.scale != Scale.FILL) {
append("-scale=fit")
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/Base64Mapper.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/Base64Mapper.kt
deleted file mode 100644
index 56c57e80..00000000
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/Base64Mapper.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.seiko.imageloader.component.mapper
-
-import com.seiko.imageloader.option.Options
-import io.ktor.util.decodeBase64Bytes
-
-class Base64Image(
- val contentType: String?,
- val content: ByteArray,
-)
-
-class Base64Mapper : Mapper {
- override fun map(data: Any, options: Options): Base64Image? {
- if (data !is String) return null
- if (!isApplicable(data)) return null
- return data.split(",").let {
- val contentType = it.firstOrNull()?.removePrefix("data:")?.removeSuffix(";base64")
- val content = it.last()
- Base64Image(
- contentType = contentType,
- content = content.decodeBase64Bytes(),
- )
- }
- }
-
- private fun isApplicable(data: String): Boolean {
- return data.startsWith("data:")
- }
-}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/KtorUrlMapper.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/KtorUrlMapper.kt
index 663606f7..744d92b2 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/KtorUrlMapper.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/KtorUrlMapper.kt
@@ -11,6 +11,6 @@ class KtorUrlMapper : Mapper {
}
private fun isApplicable(data: String): Boolean {
- return data.startsWith("http")
+ return data.startsWith("http:") || data.startsWith("https:")
}
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/StringUriMapper.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/StringUriMapper.kt
index 6cd496aa..4d5dae7d 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/StringUriMapper.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/component/mapper/StringUriMapper.kt
@@ -6,6 +6,11 @@ import com.seiko.imageloader.option.Options
class StringUriMapper : Mapper {
override fun map(data: Any, options: Options): Uri? {
if (data !is String) return null
+ // ignore data uri, see: https://en.wikipedia.org/wiki/Data_URI_scheme
+ if (data.startsWith("data:")) return null
+ // ignore http url
+ if (data.startsWith("http:")) return null
+ if (data.startsWith("https:")) return null
return Uri.parseOrNull(data)
}
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/DecodeInterceptor.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/DecodeInterceptor.kt
index 00855d19..dce9935b 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/DecodeInterceptor.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/DecodeInterceptor.kt
@@ -18,7 +18,7 @@ class DecodeInterceptor : Interceptor {
private suspend fun proceed(chain: Interceptor.Chain, request: ImageRequest): ImageResult {
val options = chain.options
return when (val result = chain.proceed(request)) {
- is ImageResult.Source -> {
+ is ImageResult.OfSource -> {
runCatching {
decode(chain.components, result, options)
}.fold(
@@ -68,8 +68,8 @@ class DecodeInterceptor : Interceptor {
}
}
-private fun DecodeResult.toImageResult() = when (this) {
- is DecodeResult.Bitmap -> ImageResult.Bitmap(bitmap = bitmap)
- is DecodeResult.Image -> ImageResult.Image(image = image)
- is DecodeResult.Painter -> ImageResult.Painter(painter = painter)
+private fun DecodeResult.toImageResult(): ImageResult = when (this) {
+ is DecodeResult.OfBitmap -> ImageResult.OfBitmap(bitmap = bitmap)
+ is DecodeResult.OfImage -> ImageResult.OfImage(image = image)
+ is DecodeResult.OfPainter -> ImageResult.OfPainter(painter = painter)
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/DiskCacheInterceptor.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/DiskCacheInterceptor.kt
index 0107479f..bd5a0010 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/DiskCacheInterceptor.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/DiskCacheInterceptor.kt
@@ -43,7 +43,7 @@ class DiskCacheInterceptor(
tag = "DiskCacheInterceptor",
data = request.data,
) { "read disk cache" }
- return ImageResult.Source(
+ return ImageResult.OfSource(
source = snapshot.source(),
dataSource = DataSource.Disk,
)
@@ -53,7 +53,7 @@ class DiskCacheInterceptor(
}
val result = chain.proceed(request)
when (result) {
- is ImageResult.Source -> {
+ is ImageResult.OfSource -> {
snapshot = runCatching {
writeToDiskCache(
options,
@@ -73,7 +73,7 @@ class DiskCacheInterceptor(
tag = "DiskCacheInterceptor",
data = request.data,
) { "write disk cache" }
- return ImageResult.Source(
+ return ImageResult.OfSource(
source = snapshot.source(),
dataSource = result.dataSource,
extra = result.extra,
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/FetchInterceptor.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/FetchInterceptor.kt
index e758674d..4a94c76c 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/FetchInterceptor.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/FetchInterceptor.kt
@@ -41,18 +41,18 @@ class FetchInterceptor : Interceptor {
}
private fun FetchResult.toImageResult() = when (this) {
- is FetchResult.Source -> ImageResult.Source(
+ is FetchResult.OfSource -> ImageResult.OfSource(
source = source,
dataSource = DataSource.Engine,
extra = extra,
)
- is FetchResult.Bitmap -> ImageResult.Bitmap(
+ is FetchResult.OfBitmap -> ImageResult.OfBitmap(
bitmap = bitmap,
)
- is FetchResult.Image -> ImageResult.Image(
+ is FetchResult.OfImage -> ImageResult.OfImage(
image = image,
)
- is FetchResult.Painter -> ImageResult.Painter(
+ is FetchResult.OfPainter -> ImageResult.OfPainter(
painter = painter,
)
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/InterceptorChainHelper.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/InterceptorChainHelper.kt
index f8e21a50..e1716f89 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/InterceptorChainHelper.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/InterceptorChainHelper.kt
@@ -4,16 +4,18 @@ import com.seiko.imageloader.ImageLoaderConfig
import com.seiko.imageloader.model.ImageAction
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.option.Options
+import com.seiko.imageloader.option.takeFrom
import kotlinx.coroutines.flow.FlowCollector
internal class InterceptorChainHelper(
initialImageRequest: ImageRequest,
+ private val initialOptions: Options,
private val config: ImageLoaderConfig,
private val flowCollector: FlowCollector,
) {
val logger get() = config.logger
- private val interceptors by lazy {
+ val interceptors by lazy {
initialImageRequest.interceptors?.plus(config.interceptors.list)
?: config.interceptors.list
}
@@ -24,17 +26,11 @@ internal class InterceptorChainHelper(
}
fun getOptions(request: ImageRequest): Options {
- return Options(config.defaultOptions) {
- request.optionsBuilders.forEach { builder ->
- builder.invoke(this)
- }
+ return Options(initialOptions) {
+ takeFrom(request)
}
}
- fun getInterceptor(index: Int): Interceptor {
- return interceptors[index]
- }
-
suspend fun emit(action: ImageAction) {
flowCollector.emit(action)
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/InterceptorChainImpl.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/InterceptorChainImpl.kt
index 855e0668..9ea2e51b 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/InterceptorChainImpl.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/InterceptorChainImpl.kt
@@ -17,11 +17,13 @@ internal class InterceptorChainImpl(
constructor(
initialRequest: ImageRequest,
+ initialOptions: Options,
config: ImageLoaderConfig,
flowCollector: FlowCollector,
) : this(
helper = InterceptorChainHelper(
initialImageRequest = initialRequest,
+ initialOptions = initialOptions,
config = config,
flowCollector = flowCollector,
),
@@ -36,7 +38,7 @@ internal class InterceptorChainImpl(
)
override suspend fun proceed(request: ImageRequest): ImageResult {
- val interceptor = helper.getInterceptor(index)
+ val interceptor = helper.interceptors[index]
val chain = copy(index = index + 1, request = request)
return interceptor.intercept(chain)
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/Interceptors.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/Interceptors.kt
index cc582e8c..adeca9b2 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/Interceptors.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/intercept/Interceptors.kt
@@ -92,16 +92,8 @@ class InterceptorsBuilder internal constructor() {
}
fun memoryCache(
- mapToMemoryValue: (ImageResult) -> Bitmap? = {
- if (it is ImageResult.Bitmap) {
- it.bitmap
- } else {
- null
- }
- },
- mapToImageResult: (Bitmap) -> ImageResult? = {
- ImageResult.Bitmap(it)
- },
+ mapToMemoryValue: (ImageResult) -> Bitmap? = { (it as? ImageResult.OfBitmap)?.bitmap },
+ mapToImageResult: (Bitmap) -> ImageResult? = { ImageResult.OfBitmap(it) },
block: () -> MemoryCache,
) {
memoryCaches.add(
@@ -113,26 +105,6 @@ class InterceptorsBuilder internal constructor() {
)
}
- fun anyMemoryCacheConfig(
- valueHashProvider: (T) -> Int,
- valueSizeProvider: (T) -> Int,
- mapToMemoryValue: (ImageResult) -> T?,
- mapToImageResult: (T) -> ImageResult?,
- block: MemoryCacheBuilder.() -> Unit,
- ) {
- anyMemoryCache(
- mapToMemoryValue = mapToMemoryValue,
- mapToImageResult = mapToImageResult,
- block = {
- MemoryCache(
- valueHashProvider = valueHashProvider,
- valueSizeProvider = valueSizeProvider,
- block = block,
- )
- },
- )
- }
-
fun anyMemoryCache(
mapToMemoryValue: (ImageResult) -> T?,
mapToImageResult: (T) -> ImageResult?,
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/model/ImageAction.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/model/ImageAction.kt
index 2876fdf5..c8db14e7 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/model/ImageAction.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/model/ImageAction.kt
@@ -1,20 +1,27 @@
package com.seiko.imageloader.model
import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.painter.Painter
+import com.seiko.imageloader.Bitmap
+import com.seiko.imageloader.Image
import com.seiko.imageloader.Poko
import okio.BufferedSource
@Immutable
-sealed interface ImageAction
+sealed interface ImageAction {
+ sealed interface Loading : ImageAction
+ sealed interface Success : ImageAction
+ sealed interface Failure : ImageAction {
+ val error: Throwable
+ }
+}
@Immutable
-sealed interface ImageEvent : ImageAction {
- object Start : ImageEvent
- object StartWithMemory : ImageEvent
- object StartWithDisk : ImageEvent
- object StartWithFetch : ImageEvent
-
- @Poko class Progress(val progress: Float) : ImageEvent
+sealed interface ImageEvent : ImageAction.Loading {
+ data object Start : ImageEvent
+ data object StartWithMemory : ImageEvent
+ data object StartWithDisk : ImageEvent
+ data object StartWithFetch : ImageEvent
}
@Immutable
@@ -22,33 +29,28 @@ sealed interface ImageResult : ImageAction {
@Immutable
@Poko
- class Source(
- val source: BufferedSource,
- val dataSource: DataSource,
- val extra: ExtraData = EmptyExtraData,
- ) : ImageResult
+ class OfBitmap(val bitmap: Bitmap) : ImageResult, ImageAction.Success
@Immutable
@Poko
- class Bitmap(
- val bitmap: com.seiko.imageloader.Bitmap,
- ) : ImageResult
+ class OfImage(val image: Image) : ImageResult, ImageAction.Success
@Immutable
@Poko
- class Image(
- val image: com.seiko.imageloader.Image,
- ) : ImageResult
+ class OfPainter(val painter: Painter) : ImageResult, ImageAction.Success
@Immutable
@Poko
- class Painter(
- val painter: androidx.compose.ui.graphics.painter.Painter,
- ) : ImageResult
+ class OfError(override val error: Throwable) : ImageResult, ImageAction.Failure
@Immutable
@Poko
- class Error(
- val error: Throwable,
- ) : ImageResult
+ class OfSource(
+ val source: BufferedSource,
+ val dataSource: DataSource,
+ val extra: ExtraData = EmptyExtraData,
+ ) : ImageResult, ImageAction.Failure {
+ override val error: Throwable
+ get() = IllegalStateException("failure to decode image source")
+ }
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/model/ImageRequest.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/model/ImageRequest.kt
index 720aff3e..90485afd 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/model/ImageRequest.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/model/ImageRequest.kt
@@ -1,8 +1,6 @@
package com.seiko.imageloader.model
-import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
-import androidx.compose.ui.graphics.painter.Painter
import com.seiko.imageloader.component.ComponentRegistry
import com.seiko.imageloader.component.ComponentRegistryBuilder
import com.seiko.imageloader.intercept.Interceptor
@@ -13,37 +11,69 @@ import com.seiko.imageloader.option.SizeResolver
@Immutable
class ImageRequest internal constructor(
val data: Any,
- val optionsBuilders: List Unit>,
val extra: ExtraData,
- val placeholderPainter: (@Composable () -> Painter)?,
- val errorPainter: (@Composable () -> Painter)?,
+ val sizeResolver: SizeResolver,
val skipEvent: Boolean,
+ internal val optionsBuilders: List Unit>,
internal val components: ComponentRegistry?,
internal val interceptors: List?,
) {
- @Deprecated("", ReplaceWith("ImageRequest(request) {}"))
- fun newBuilder(block: ImageRequestBuilder.() -> Unit) = ImageRequest(this, block)
+
+ override fun equals(other: Any?): Boolean {
+ return other is ImageRequest &&
+ data == other.data &&
+ extra == other.extra &&
+ sizeResolver == other.sizeResolver &&
+ skipEvent == other.skipEvent &&
+ optionsBuilders == other.optionsBuilders &&
+ components == other.components &&
+ interceptors == other.interceptors
+ }
+
+ override fun hashCode(): Int {
+ var result = data.hashCode()
+ result = 31 * result + extra.hashCode()
+ result = 31 * result + sizeResolver.hashCode()
+ result = 31 * result + skipEvent.hashCode()
+ result = 31 * result + optionsBuilders.hashCode()
+ result = 31 * result + components.hashCode()
+ result = 31 * result + interceptors.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "ImageRequest(" +
+ "data=$data," +
+ "extra=$extra," +
+ "sizeResolver=$sizeResolver," +
+ "skipEvent=$skipEvent," +
+ "optionsBuilders=$optionsBuilders," +
+ "components=$components," +
+ "interceptors=$interceptors)"
+ }
}
class ImageRequestBuilder internal constructor() {
private var data: Any? = null
+ private var sizeResolver: SizeResolver = SizeResolver.Unspecified
private val optionsBuilders: MutableList Unit> = mutableListOf()
private var extraData: ExtraData? = null
- private var placeholderPainter: (@Composable () -> Painter)? = null
- private var errorPainter: (@Composable () -> Painter)? = null
private var componentBuilder: ComponentRegistryBuilder? = null
private var interceptors: MutableList? = null
var skipEvent: Boolean = false
- fun takeFrom(request: ImageRequest) {
+ fun takeFrom(
+ request: ImageRequest,
+ clearOptions: Boolean = false,
+ ) {
data = request.data
- optionsBuilders.clear()
+ if (clearOptions) {
+ optionsBuilders.clear()
+ }
optionsBuilders.addAll(request.optionsBuilders)
extraData = request.extra
- placeholderPainter = request.placeholderPainter
- errorPainter = request.errorPainter
- componentBuilder = request.components?.newBuilder()
+ componentBuilder = request.components?.let { ComponentRegistryBuilder(it) }
interceptors = request.interceptors?.toMutableList()
skipEvent = request.skipEvent
}
@@ -53,9 +83,7 @@ class ImageRequestBuilder internal constructor() {
}
fun size(sizeResolver: SizeResolver) {
- optionsBuilders.add {
- this.sizeResolver = sizeResolver
- }
+ this.sizeResolver = sizeResolver
}
fun scale(scale: Scale) {
@@ -84,20 +112,11 @@ class ImageRequestBuilder internal constructor() {
?: extraData(builder)
}
- fun placeholderPainter(loader: @Composable () -> Painter) {
- placeholderPainter = loader
- }
-
- fun errorPainter(loader: @Composable () -> Painter) {
- errorPainter = loader
- }
-
internal fun build() = ImageRequest(
data = data ?: NullRequestData,
+ sizeResolver = sizeResolver,
optionsBuilders = optionsBuilders,
extra = extraData ?: EmptyExtraData,
- placeholderPainter = placeholderPainter,
- errorPainter = errorPainter,
skipEvent = skipEvent,
components = componentBuilder?.build(),
interceptors = interceptors,
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/option/Options.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/option/Options.kt
index c60f3378..83e5f52b 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/option/Options.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/option/Options.kt
@@ -1,10 +1,13 @@
package com.seiko.imageloader.option
+import androidx.compose.ui.geometry.Size
+import com.seiko.imageloader.BitmapConfig
import com.seiko.imageloader.Poko
import com.seiko.imageloader.cache.CachePolicy
import com.seiko.imageloader.model.EmptyExtraData
import com.seiko.imageloader.model.ExtraData
import com.seiko.imageloader.model.ExtraDataBuilder
+import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.extraData
import com.seiko.imageloader.util.DEFAULT_MAX_IMAGE_SIZE
@@ -12,9 +15,9 @@ import com.seiko.imageloader.util.DEFAULT_MAX_IMAGE_SIZE
val allowInexactSize: Boolean,
val premultipliedAlpha: Boolean,
val retryIfDiskDecodeError: Boolean,
- val imageConfig: ImageConfig,
+ val bitmapConfig: BitmapConfig,
+ val size: Size,
val scale: Scale,
- val sizeResolver: SizeResolver,
val memoryCachePolicy: CachePolicy,
val diskCachePolicy: CachePolicy,
val playAnimate: Boolean,
@@ -22,17 +25,6 @@ import com.seiko.imageloader.util.DEFAULT_MAX_IMAGE_SIZE
val maxImageSize: Int,
val extra: ExtraData,
) {
-
- @Deprecated("", ReplaceWith("Options(options) {}"))
- fun newBuilder(block: OptionsBuilder.() -> Unit) = Options(this, block)
-
- enum class ImageConfig {
- ALPHA_8,
- ARGB_8888,
- RGBA_F16,
- HARDWARE,
- }
-
companion object {
internal const val REPEAT_INFINITE = -1
}
@@ -43,9 +35,9 @@ class OptionsBuilder internal constructor() {
var allowInexactSize: Boolean = false
var premultipliedAlpha: Boolean = true
var retryIfDiskDecodeError: Boolean = true
- var imageConfig: Options.ImageConfig = Options.ImageConfig.ARGB_8888
+ var bitmapConfig: BitmapConfig = BitmapConfig.Default
+ var size: Size = Size.Unspecified
var scale: Scale = Scale.FILL
- var sizeResolver: SizeResolver = SizeResolver.Unspecified
var memoryCachePolicy: CachePolicy = CachePolicy.ENABLED
var diskCachePolicy: CachePolicy = CachePolicy.ENABLED
var playAnimate: Boolean = true
@@ -66,9 +58,9 @@ class OptionsBuilder internal constructor() {
allowInexactSize = options.allowInexactSize
premultipliedAlpha = options.premultipliedAlpha
retryIfDiskDecodeError = options.retryIfDiskDecodeError
- imageConfig = options.imageConfig
+ bitmapConfig = options.bitmapConfig
+ size = options.size
scale = options.scale
- sizeResolver = options.sizeResolver
memoryCachePolicy = options.memoryCachePolicy
diskCachePolicy = options.diskCachePolicy
playAnimate = options.playAnimate
@@ -94,9 +86,9 @@ class OptionsBuilder internal constructor() {
allowInexactSize = allowInexactSize,
premultipliedAlpha = premultipliedAlpha,
retryIfDiskDecodeError = retryIfDiskDecodeError,
- imageConfig = imageConfig,
+ bitmapConfig = bitmapConfig,
+ size = size,
scale = scale,
- sizeResolver = sizeResolver,
memoryCachePolicy = memoryCachePolicy,
diskCachePolicy = diskCachePolicy,
playAnimate = playAnimate,
@@ -106,6 +98,12 @@ class OptionsBuilder internal constructor() {
)
}
+internal fun OptionsBuilder.takeFrom(request: ImageRequest) {
+ request.optionsBuilders.forEach { builder ->
+ builder.invoke(this)
+ }
+}
+
fun Options(block: OptionsBuilder.() -> Unit = {}) =
OptionsBuilder().apply(block).build()
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/option/SizeResolver.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/option/SizeResolver.kt
index 0f4abb94..24440456 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/option/SizeResolver.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/option/SizeResolver.kt
@@ -1,25 +1,45 @@
package com.seiko.imageloader.option
import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.DpSize
+import kotlinx.coroutines.CompletableDeferred
interface SizeResolver {
- suspend fun Density.size(): Size
+ suspend fun size(): Size
companion object {
- val Unspecified = SizeResolver(Size.Unspecified)
+ val Unspecified: SizeResolver = RealSizeResolver(Size.Unspecified)
}
}
fun SizeResolver(block: suspend () -> Size) = object : SizeResolver {
- override suspend fun Density.size(): Size = block()
+ override suspend fun size(): Size = block()
}
-fun SizeResolver(size: Size): SizeResolver = object : SizeResolver {
- override suspend fun Density.size(): Size = size
+class AsyncSizeResolver : SizeResolver {
+
+ private val sizeObserver = CompletableDeferred()
+
+ override suspend fun size(): Size {
+ return sizeObserver.await()
+ }
+
+ fun setSize(size: Size) {
+ sizeObserver.complete(size)
+ }
}
-fun SizeResolver(size: DpSize): SizeResolver = object : SizeResolver {
- override suspend fun Density.size(): Size = size.toSize()
+fun SizeResolver(size: Size): SizeResolver = RealSizeResolver(size)
+
+private class RealSizeResolver(private val size: Size) : SizeResolver {
+ override suspend fun size(): Size = size
+
+ override fun equals(other: Any?): Boolean {
+ return other is RealSizeResolver && size == other.size
+ }
+
+ override fun hashCode(): Int {
+ var result = super.hashCode()
+ result = 31 * result + size.hashCode()
+ return result
+ }
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ui/AutoSizeBox.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ui/AutoSizeBox.kt
new file mode 100644
index 00000000..ef809b56
--- /dev/null
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ui/AutoSizeBox.kt
@@ -0,0 +1,209 @@
+package com.seiko.imageloader.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+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.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.geometry.isSpecified
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.LocalImageLoader
+import com.seiko.imageloader.model.ImageAction
+import com.seiko.imageloader.model.ImageEvent
+import com.seiko.imageloader.model.ImageRequest
+import com.seiko.imageloader.option.AsyncSizeResolver
+import com.seiko.imageloader.option.SizeResolver
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+@Composable
+fun AutoSizeBox(
+ request: ImageRequest,
+ modifier: Modifier = Modifier,
+ imageLoader: ImageLoader = LocalImageLoader.current,
+ contentAlignment: Alignment = Alignment.Center,
+ propagateMinConstraints: Boolean = false,
+ isOnlyPostFirstEvent: Boolean = true,
+ content: @Composable BoxScope.(ImageAction) -> Unit,
+) {
+ var action by remember {
+ mutableStateOf(ImageEvent.Start)
+ }
+ Box(
+ modifier = modifier.autoSizeBoxNode(
+ request = request,
+ imageLoader = imageLoader,
+ onImageActionChange = { action = it },
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+ ),
+ contentAlignment = contentAlignment,
+ propagateMinConstraints = propagateMinConstraints,
+ ) {
+ content(action)
+ }
+}
+
+private fun Modifier.autoSizeBoxNode(
+ request: ImageRequest,
+ imageLoader: ImageLoader,
+ onImageActionChange: (ImageAction) -> Unit,
+ isOnlyPostFirstEvent: Boolean,
+): Modifier = this then AutoSizeBoxNodeElement(
+ request = request,
+ imageLoader = imageLoader,
+ onImageActionChange = onImageActionChange,
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+)
+
+private data class AutoSizeBoxNodeElement(
+ val request: ImageRequest,
+ val imageLoader: ImageLoader,
+ val onImageActionChange: (ImageAction) -> Unit,
+ val isOnlyPostFirstEvent: Boolean,
+) : ModifierNodeElement() {
+
+ override fun create(): AutoSizeBoxNode {
+ return AutoSizeBoxNode(
+ request = request,
+ imageLoader = imageLoader,
+ onImageActionChange = onImageActionChange,
+ )
+ }
+
+ override fun update(node: AutoSizeBoxNode) {
+ node.update(
+ request = request,
+ imageLoader = imageLoader,
+ onImageActionChange = onImageActionChange,
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+ )
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "autoSizeBox"
+ properties["request"] = request
+ properties["imageLoader"] = imageLoader
+ properties["onImageActionChange"] = onImageActionChange
+ }
+}
+
+private class AutoSizeBoxNode(
+ request: ImageRequest,
+ private var imageLoader: ImageLoader,
+ private var onImageActionChange: (ImageAction) -> Unit,
+) : Modifier.Node(), LayoutModifierNode {
+
+ private var currentImageJob: Job? = null
+
+ private var cachedSize: Size = Size.Unspecified
+
+ private var isReset = false
+
+ private var request: ImageRequest = modifyRequest(request, cachedSize)
+
+ override val shouldAutoInvalidate: Boolean
+ get() = false
+
+ override fun onAttach() {
+ super.onAttach()
+ isReset = false
+ launchImage()
+ }
+
+ override fun onReset() {
+ super.onReset()
+ isReset = true
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ if (!isReset) {
+ cachedSize = Size.Unspecified
+ }
+ }
+
+ fun update(
+ request: ImageRequest,
+ imageLoader: ImageLoader,
+ onImageActionChange: (ImageAction) -> Unit,
+ isOnlyPostFirstEvent: Boolean,
+ ) {
+ val finalRequest = modifyRequest(
+ request = request,
+ cachedSize = cachedSize,
+ skipEvent = isOnlyPostFirstEvent,
+ )
+ val isRequestChange = this.request != finalRequest
+
+ this.request = finalRequest
+ this.imageLoader = imageLoader
+ this.onImageActionChange = onImageActionChange
+
+ if (isAttached && isRequestChange) {
+ launchImage()
+ }
+ }
+
+ private fun launchImage() {
+ currentImageJob?.cancel()
+ currentImageJob = coroutineScope.launch {
+ imageLoader.async(request).collect { action ->
+ onImageActionChange(action)
+ }
+ }
+ }
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ cachedSize = constraints.inferredSize()
+
+ val sizeResolver = request.sizeResolver
+ if (sizeResolver is AsyncSizeResolver) {
+ sizeResolver.setSize(cachedSize)
+ }
+
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.placeRelative(0, 0)
+ }
+ }
+}
+
+internal fun modifyRequest(
+ request: ImageRequest,
+ cachedSize: Size,
+ skipEvent: Boolean = false,
+): ImageRequest {
+ return if (request.sizeResolver == SizeResolver.Unspecified) {
+ ImageRequest(request) {
+ if (cachedSize.isSpecified && !cachedSize.isEmpty()) {
+ size(SizeResolver(cachedSize))
+ } else {
+ size(AsyncSizeResolver())
+ }
+ this.skipEvent = skipEvent
+ }
+ } else {
+ request
+ }
+}
+
+internal data class CachedPositionAndSize(
+ val position: Offset,
+ val size: Size,
+)
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ui/AutoSizeImage.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ui/AutoSizeImage.kt
new file mode 100644
index 00000000..c3adfec8
--- /dev/null
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ui/AutoSizeImage.kt
@@ -0,0 +1,484 @@
+package com.seiko.imageloader.ui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.withFrameMillis
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.DefaultAlpha
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.withSave
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.times
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.invalidateMeasurement
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.toOffset
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.LocalImageLoader
+import com.seiko.imageloader.model.ImageEvent
+import com.seiko.imageloader.model.ImageRequest
+import com.seiko.imageloader.model.ImageResult
+import com.seiko.imageloader.option.AsyncSizeResolver
+import com.seiko.imageloader.toPainter
+import com.seiko.imageloader.util.AnimationPainter
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+// AutoSizeImage ≈ AutoSizeBox + Painter
+@Composable
+fun AutoSizeImage(
+ request: ImageRequest,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ alignment: Alignment = Alignment.Center,
+ contentScale: ContentScale = ContentScale.Fit,
+ alpha: Float = DefaultAlpha,
+ colorFilter: ColorFilter? = null,
+ imageLoader: ImageLoader = LocalImageLoader.current,
+ placeholderPainter: (@Composable () -> Painter)? = null,
+ errorPainter: (@Composable () -> Painter)? = null,
+ isOnlyPostFirstEvent: Boolean = true,
+) {
+ val semantics = if (contentDescription != null) {
+ Modifier.semantics {
+ this.contentDescription = contentDescription
+ this.role = Role.Image
+ }
+ } else {
+ Modifier
+ }
+ Layout(
+ modifier.then(semantics).clipToBounds().autoSizeImageNode(
+ request = request,
+ alignment = alignment,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ imageLoader = imageLoader,
+ placeholderPainter = placeholderPainter?.invoke(),
+ errorPainter = errorPainter?.invoke(),
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+ ),
+ ) { _, constraints ->
+ layout(constraints.minWidth, constraints.minHeight) {}
+ }
+}
+
+private fun Modifier.autoSizeImageNode(
+ request: ImageRequest,
+ alignment: Alignment,
+ contentScale: ContentScale,
+ alpha: Float,
+ colorFilter: ColorFilter?,
+ imageLoader: ImageLoader,
+ placeholderPainter: Painter?,
+ errorPainter: Painter?,
+ isOnlyPostFirstEvent: Boolean,
+): Modifier = this then AutoSizeImageNodeElement(
+ request = request,
+ alignment = alignment,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ imageLoader = imageLoader,
+ placeholderPainter = placeholderPainter,
+ errorPainter = errorPainter,
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+)
+
+private data class AutoSizeImageNodeElement(
+ private val request: ImageRequest,
+ private val alignment: Alignment,
+ private val contentScale: ContentScale,
+ private val alpha: Float,
+ private val colorFilter: ColorFilter?,
+ private val imageLoader: ImageLoader,
+ private val placeholderPainter: Painter?,
+ private val errorPainter: Painter?,
+ private val isOnlyPostFirstEvent: Boolean,
+) : ModifierNodeElement() {
+
+ override fun create(): AutoSizeImageNode {
+ return AutoSizeImageNode(
+ request = request,
+ alignment = alignment,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ imageLoader = imageLoader,
+ placeholderPainter = placeholderPainter,
+ errorPainter = errorPainter,
+ )
+ }
+
+ override fun update(node: AutoSizeImageNode) {
+ node.update(
+ request = request,
+ alignment = alignment,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ imageLoader = imageLoader,
+ placeholderPainter = placeholderPainter,
+ errorPainter = errorPainter,
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+ )
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "autoSizeImage"
+ properties["request"] = request
+ properties["alignment"] = alignment
+ properties["contentScale"] = contentScale
+ properties["alpha"] = alpha
+ properties["colorFilter"] = colorFilter
+ properties["imageLoader"] = imageLoader
+ properties["placeholderPainter"] = placeholderPainter
+ properties["errorPainter"] = errorPainter
+ }
+}
+
+private class AutoSizeImageNode(
+ request: ImageRequest,
+ private var alignment: Alignment,
+ private var contentScale: ContentScale,
+ private var alpha: Float,
+ private var colorFilter: ColorFilter?,
+ private var imageLoader: ImageLoader,
+ private var placeholderPainter: Painter?,
+ private var errorPainter: Painter?,
+) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, CompositionLocalConsumerModifierNode {
+
+ private var currentImageJob: Job? = null
+ private var currentPlayerJob: Job? = null
+
+ private var cachedSize: Size = Size.Unspecified
+
+ private var drawPainter: Painter? = null
+ private var drawPainterPositionAndSize: CachedPositionAndSize? = null
+
+ private var hasFixedSize: Boolean = false
+ private var isReset: Boolean = false
+
+ private var request: ImageRequest = modifyRequest(request, cachedSize)
+
+ override val shouldAutoInvalidate: Boolean
+ get() = false
+
+ override fun onAttach() {
+ super.onAttach()
+ isReset = false
+ launchImage()
+ }
+
+ override fun onReset() {
+ super.onReset()
+ isReset = true
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ // if this node is reset from pool, not need to reset size
+ if (!isReset) {
+ hasFixedSize = false
+ cachedSize = Size.Unspecified
+ }
+ updatePainter(null)
+ }
+
+ fun update(
+ request: ImageRequest,
+ alignment: Alignment,
+ contentScale: ContentScale,
+ alpha: Float,
+ colorFilter: ColorFilter?,
+ imageLoader: ImageLoader,
+ placeholderPainter: Painter?,
+ errorPainter: Painter?,
+ isOnlyPostFirstEvent: Boolean,
+ ) {
+ val finalRequest = modifyRequest(
+ request = request,
+ cachedSize = cachedSize,
+ skipEvent = isOnlyPostFirstEvent,
+ )
+ val isRequestChange = this.request != finalRequest
+
+ this.request = finalRequest
+ this.alignment = alignment
+ this.contentScale = contentScale
+ this.alpha = alpha
+ this.colorFilter = colorFilter
+ this.imageLoader = imageLoader
+ this.placeholderPainter = placeholderPainter
+ this.errorPainter = errorPainter
+
+ if (isAttached && isRequestChange) {
+ launchImage()
+ }
+ }
+
+ private fun launchImage() {
+ currentImageJob?.cancel()
+ currentImageJob = coroutineScope.launch {
+ imageLoader.async(request).collect { action ->
+ when (action) {
+ is ImageEvent -> placeholderPainter
+ is ImageResult.OfPainter -> action.painter
+ is ImageResult.OfBitmap -> action.bitmap.toPainter()
+ is ImageResult.OfImage -> action.image.toPainter()
+ is ImageResult.OfError -> errorPainter
+ is ImageResult.OfSource -> errorPainter
+ }?.let { painter ->
+ updatePainter(painter)
+ }
+ }
+ }
+ }
+
+ private fun updatePainter(painter: Painter?) {
+ (drawPainter as? RememberObserver)?.onAbandoned()
+ currentPlayerJob?.cancel()
+ currentPlayerJob = null
+
+ drawPainter = painter
+
+ if (isAttached) {
+ (painter as? RememberObserver)?.onRemembered()
+ checkPainterPlay()
+ }
+
+ drawPainterPositionAndSize = null
+
+ if (hasFixedSize) {
+ invalidateDraw()
+ } else {
+ invalidateMeasurement()
+ }
+ }
+
+ private fun checkPainterPlay() {
+ val painter = drawPainter ?: return
+ if (painter is AnimationPainter && painter.isPlay()) {
+ currentPlayerJob?.cancel()
+ currentPlayerJob = coroutineScope.launch {
+ while (painter.nextPlay()) {
+ withFrameMillis { frameTimeMillis ->
+ painter.update(frameTimeMillis)
+ }
+ }
+ }
+ }
+ }
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ drawPainterPositionAndSize = null
+ hasFixedSize = constraints.hasFixedSize()
+ cachedSize = constraints.inferredSize()
+
+ val sizeResolver = request.sizeResolver
+ if (sizeResolver is AsyncSizeResolver) {
+ sizeResolver.setSize(cachedSize)
+ }
+
+ val placeable = measurable.measure(
+ modifyConstraints(constraints, drawPainter, contentScale),
+ )
+ return layout(placeable.width, placeable.height) {
+ placeable.placeRelative(0, 0)
+ }
+ }
+
+ override fun ContentDrawScope.draw() {
+ drawPainter?.let { painter ->
+ drawContext.canvas.withSave {
+ val positionAndSize = drawPainterPositionAndSize
+ ?: calcPositionAndSize(painter).also {
+ drawPainterPositionAndSize = it
+ }
+ val dx = positionAndSize.position.x
+ val dy = positionAndSize.position.y
+ val scaledSize = positionAndSize.size
+ translate(dx, dy) {
+ with(painter) {
+ draw(scaledSize, alpha, colorFilter)
+ }
+ }
+ }
+ }
+
+ // Maintain the same pattern as Modifier.drawBehind to allow chaining of DrawModifiers
+ drawContent()
+ }
+
+ private fun ContentDrawScope.calcPositionAndSize(painter: Painter): CachedPositionAndSize {
+ val intrinsicSize = painter.intrinsicSize
+ val srcWidth = if (intrinsicSize.hasSpecifiedAndFiniteWidth()) {
+ intrinsicSize.width
+ } else {
+ size.width
+ }
+
+ val srcHeight = if (intrinsicSize.hasSpecifiedAndFiniteHeight()) {
+ intrinsicSize.height
+ } else {
+ size.height
+ }
+ val srcSize = Size(srcWidth, srcHeight)
+
+ // Compute the offset to translate the content based on the given alignment
+ // and size to draw based on the ContentScale parameter
+ val scaledSize = if (size.width != 0f && size.height != 0f) {
+ srcSize * contentScale.computeScaleFactor(srcSize, size)
+ } else {
+ Size.Zero
+ }
+
+ val alignedPosition = alignment.align(
+ IntSize(scaledSize.width.roundToInt(), scaledSize.height.roundToInt()),
+ IntSize(size.width.roundToInt(), size.height.roundToInt()),
+ layoutDirection,
+ ).toOffset()
+ return CachedPositionAndSize(
+ alignedPosition,
+ scaledSize,
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return (other is AutoSizeImageNode) &&
+ request == other.request &&
+ alignment == other.alignment &&
+ contentScale == other.contentScale &&
+ alpha == other.alpha &&
+ colorFilter == other.colorFilter &&
+ imageLoader == other.imageLoader
+ }
+
+ override fun hashCode(): Int {
+ var result = request.hashCode()
+ result = 31 * result + alignment.hashCode()
+ result = 31 * result + contentScale.hashCode()
+ result = 31 * result + alpha.hashCode()
+ result = 31 * result + colorFilter.hashCode()
+ result = 31 * result + imageLoader.hashCode()
+ result = 31 * result + placeholderPainter.hashCode()
+ result = 31 * result + errorPainter.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "AutoSizeImageModifier(" +
+ "painter=$drawPainter, " +
+ "alignment=$alignment, " +
+ "contentScale=$contentScale, " +
+ "alpha=$alpha, " +
+ "colorFilter=$colorFilter, " +
+ "imageLoader=$imageLoader, " +
+ "placeholderPainter=$placeholderPainter, " +
+ "errorPainter=$errorPainter)"
+ }
+}
+
+private fun modifyConstraints(
+ constraints: Constraints,
+ painter: Painter?,
+ contentScale: ContentScale,
+): Constraints {
+ if (constraints.hasFixedSize()) {
+ return constraints.copy(
+ minWidth = constraints.maxWidth,
+ minHeight = constraints.maxHeight,
+ )
+ }
+
+ val intrinsicSize = painter?.intrinsicSize ?: return constraints
+ val intrinsicWidth = if (intrinsicSize.hasSpecifiedAndFiniteWidth()) {
+ intrinsicSize.width.roundToInt()
+ } else {
+ constraints.minWidth
+ }
+
+ val intrinsicHeight = if (intrinsicSize.hasSpecifiedAndFiniteHeight()) {
+ intrinsicSize.height.roundToInt()
+ } else {
+ constraints.minHeight
+ }
+
+ // Scale the width and height appropriately based on the given constraints
+ // and ContentScale
+ val constrainedWidth = constraints.constrainWidth(intrinsicWidth)
+ val constrainedHeight = constraints.constrainHeight(intrinsicHeight)
+ val scaledSize = calculateScaledSize(
+ Size(constrainedWidth.toFloat(), constrainedHeight.toFloat()),
+ painter,
+ contentScale,
+ )
+
+ // For both width and height constraints, consume the minimum of the scaled width
+ // and the maximum constraint as some scale types can scale larger than the maximum
+ // available size (ex ContentScale.Crop)
+ // In this case the larger of the 2 dimensions is used and the aspect ratio is
+ // maintained. Even if the size of the composable is smaller, the painter will
+ // draw its content clipped
+ val minWidth = constraints.constrainWidth(scaledSize.width.roundToInt())
+ val minHeight = constraints.constrainHeight(scaledSize.height.roundToInt())
+ return constraints.copy(minWidth = minWidth, minHeight = minHeight)
+}
+
+private fun calculateScaledSize(dstSize: Size, painter: Painter, contentScale: ContentScale): Size {
+ val srcWidth = if (!painter.intrinsicSize.hasSpecifiedAndFiniteWidth()) {
+ dstSize.width
+ } else {
+ painter.intrinsicSize.width
+ }
+
+ val srcHeight = if (!painter.intrinsicSize.hasSpecifiedAndFiniteHeight()) {
+ dstSize.height
+ } else {
+ painter.intrinsicSize.height
+ }
+
+ val srcSize = Size(srcWidth, srcHeight)
+ return if (dstSize.width != 0f && dstSize.height != 0f) {
+ srcSize * contentScale.computeScaleFactor(srcSize, dstSize)
+ } else {
+ Size.Zero
+ }
+}
+
+private fun Constraints.hasFixedSize() = hasFixedWidth && hasFixedHeight
+
+internal fun Constraints.inferredSize(): Size {
+ if (!hasBoundedWidth || !hasBoundedHeight) return Size.Unspecified
+ return Size(maxWidth.toFloat(), maxHeight.toFloat())
+}
+
+private fun Size.hasSpecifiedAndFiniteWidth() = this != Size.Unspecified && width.isFinite()
+private fun Size.hasSpecifiedAndFiniteHeight() = this != Size.Unspecified && height.isFinite()
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ui/AutoSizeWidget.ext.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ui/AutoSizeWidget.ext.kt
new file mode 100644
index 00000000..f507ff4d
--- /dev/null
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/ui/AutoSizeWidget.ext.kt
@@ -0,0 +1,115 @@
+package com.seiko.imageloader.ui
+
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.DefaultAlpha
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.LocalImageLoader
+import com.seiko.imageloader.model.ImageAction
+import com.seiko.imageloader.model.ImageRequest
+
+@Composable
+fun AutoSizeBox(
+ url: String,
+ modifier: Modifier = Modifier,
+ imageLoader: ImageLoader = LocalImageLoader.current,
+ contentAlignment: Alignment = Alignment.Center,
+ propagateMinConstraints: Boolean = false,
+ isOnlyPostFirstEvent: Boolean = true,
+ content: @Composable BoxScope.(ImageAction) -> Unit,
+) {
+ AutoSizeBox(
+ request = remember(url) { ImageRequest(url) },
+ modifier = modifier,
+ imageLoader = imageLoader,
+ contentAlignment = contentAlignment,
+ propagateMinConstraints = propagateMinConstraints,
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+ content = content,
+ )
+}
+
+@Composable
+fun AutoSizeBox(
+ resId: Int,
+ modifier: Modifier = Modifier,
+ imageLoader: ImageLoader = LocalImageLoader.current,
+ contentAlignment: Alignment = Alignment.Center,
+ propagateMinConstraints: Boolean = false,
+ isOnlyPostFirstEvent: Boolean = true,
+ content: @Composable BoxScope.(ImageAction) -> Unit,
+) {
+ AutoSizeBox(
+ request = remember(resId) { ImageRequest(resId) },
+ modifier = modifier,
+ imageLoader = imageLoader,
+ contentAlignment = contentAlignment,
+ propagateMinConstraints = propagateMinConstraints,
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+ content = content,
+ )
+}
+
+@Composable
+fun AutoSizeImage(
+ url: String,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ alignment: Alignment = Alignment.Center,
+ contentScale: ContentScale = ContentScale.Fit,
+ alpha: Float = DefaultAlpha,
+ colorFilter: ColorFilter? = null,
+ imageLoader: ImageLoader = LocalImageLoader.current,
+ placeholderPainter: (@Composable () -> Painter)? = null,
+ errorPainter: (@Composable () -> Painter)? = null,
+ isOnlyPostFirstEvent: Boolean = true,
+) {
+ AutoSizeImage(
+ request = remember(url) { ImageRequest(url) },
+ contentDescription = contentDescription,
+ modifier = modifier,
+ alignment = alignment,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ imageLoader = imageLoader,
+ placeholderPainter = placeholderPainter,
+ errorPainter = errorPainter,
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+ )
+}
+
+@Composable
+fun AutoSizeImage(
+ resId: Int,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ alignment: Alignment = Alignment.Center,
+ contentScale: ContentScale = ContentScale.Fit,
+ alpha: Float = DefaultAlpha,
+ colorFilter: ColorFilter? = null,
+ imageLoader: ImageLoader = LocalImageLoader.current,
+ placeholderPainter: (@Composable () -> Painter)? = null,
+ errorPainter: (@Composable () -> Painter)? = null,
+ isOnlyPostFirstEvent: Boolean = true,
+) {
+ AutoSizeImage(
+ request = remember(resId) { ImageRequest(resId) },
+ contentDescription = contentDescription,
+ modifier = modifier,
+ alignment = alignment,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ imageLoader = imageLoader,
+ placeholderPainter = placeholderPainter,
+ errorPainter = errorPainter,
+ isOnlyPostFirstEvent = isOnlyPostFirstEvent,
+ )
+}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/AnimationPainter.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/AnimationPainter.kt
index 4a96623f..24bb3de1 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/AnimationPainter.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/AnimationPainter.kt
@@ -4,5 +4,7 @@ interface AnimationPainter {
fun isPlay(): Boolean
+ fun nextPlay(): Boolean
+
fun update(frameTimeMillis: Long)
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/CalculateSIze.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/CalculateSIze.kt
index da7ea968..962ba7f5 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/CalculateSIze.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/CalculateSIze.kt
@@ -19,7 +19,7 @@ internal fun calculateDstSize(
dstHeight = ((maxImageSize.toFloat() / srcWidth.toFloat()) * dstHeight).toInt()
dstWidth = maxImageSize
} else {
- dstWidth = ((maxImageSize.toFloat() / srcWidth.toFloat()) * dstWidth).toInt()
+ dstWidth = ((maxImageSize.toFloat() / srcHeight.toFloat()) * dstWidth).toInt()
dstHeight = maxImageSize
}
}
diff --git a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/Platform.kt b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/Platform.kt
index 59f958f7..c61a9298 100644
--- a/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/Platform.kt
+++ b/image-loader/src/commonMain/kotlin/com/seiko/imageloader/util/Platform.kt
@@ -1,7 +1,6 @@
package com.seiko.imageloader.util
import io.ktor.client.HttpClient
-import io.ktor.client.engine.HttpClientEngine
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.CoroutineDispatcher
import okio.BufferedSource
@@ -13,17 +12,11 @@ expect class WeakReference(referred: T) {
fun clear()
}
-expect class LockObject()
-
-internal expect inline fun synchronized(lock: LockObject, block: () -> R): R
-
internal expect suspend fun ByteReadChannel.source(): BufferedSource
internal expect val ioDispatcher: CoroutineDispatcher
-internal expect val httpEngine: HttpClientEngine
-
internal expect val defaultFileSystem: FileSystem?
internal val httpEngineFactory: () -> HttpClient
- get() = { HttpClient(httpEngine) }
+ get() = { HttpClient() }
diff --git a/image-loader/src/commonMain/singleton/com/seiko/imageloader/LocalImageLoader.kt b/image-loader/src/commonMain/singleton/com/seiko/imageloader/LocalImageLoader.kt
index 001834e6..2140c7d5 100644
--- a/image-loader/src/commonMain/singleton/com/seiko/imageloader/LocalImageLoader.kt
+++ b/image-loader/src/commonMain/singleton/com/seiko/imageloader/LocalImageLoader.kt
@@ -3,6 +3,7 @@ package com.seiko.imageloader
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidedValue
import androidx.compose.runtime.ReadOnlyComposable
+import com.seiko.imageloader.cache.memory.MemoryCache
import com.seiko.imageloader.component.setupDefaultComponents
import com.seiko.imageloader.intercept.InterceptorsBuilder
import com.seiko.imageloader.model.ImageResult
@@ -21,8 +22,8 @@ expect class ImageLoaderProvidableCompositionLocal {
expect fun createImageLoaderProvidableCompositionLocal(): ImageLoaderProvidableCompositionLocal
-val ImageLoader.Companion.Default: ImageLoader
- get() = ImageLoader {
+fun ImageLoader.Companion.createDefault(): ImageLoader {
+ return ImageLoader {
components {
setupDefaultComponents()
}
@@ -33,6 +34,7 @@ val ImageLoader.Companion.Default: ImageLoader
}
}
}
+}
// cache 100 image result, without bitmap
fun InterceptorsBuilder.defaultImageResultMemoryCache(
@@ -42,23 +44,27 @@ fun InterceptorsBuilder.defaultImageResultMemoryCache(
valueSizeProvider: (ImageResult) -> Int = { 1 },
mapToMemoryValue: (ImageResult) -> ImageResult? = {
when (it) {
- is ImageResult.Image,
- is ImageResult.Painter,
+ is ImageResult.OfImage,
+ is ImageResult.OfPainter,
-> it
- is ImageResult.Bitmap -> if (includeBitmap) it else null
- is ImageResult.Source,
- is ImageResult.Error,
+ is ImageResult.OfBitmap -> if (includeBitmap) it else null
+ is ImageResult.OfSource,
+ is ImageResult.OfError,
-> null
}
},
mapToImageResult: (ImageResult) -> ImageResult? = { it },
) {
- anyMemoryCacheConfig(
- valueHashProvider = valueHashProvider,
- valueSizeProvider = valueSizeProvider,
+ anyMemoryCache(
mapToMemoryValue = mapToMemoryValue,
mapToImageResult = mapToImageResult,
) {
- maxSizeBytes(saveSize)
+ MemoryCache(
+ valueHashProvider = valueHashProvider,
+ valueSizeProvider = valueSizeProvider,
+ block = {
+ maxSizeBytes(saveSize)
+ },
+ )
}
}
diff --git a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/ImageLoaderTest.kt b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/ImageLoaderTest.kt
index 3ca2a074..443fe59d 100644
--- a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/ImageLoaderTest.kt
+++ b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/ImageLoaderTest.kt
@@ -9,7 +9,9 @@ import com.seiko.imageloader.component.fetcher.Fetcher
import com.seiko.imageloader.model.ImageEvent
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.ImageResult
+import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@@ -30,10 +32,10 @@ class ImageLoaderTest {
resultPainter3 = ColorPainter(Color.Blue)
imageLoader = ImageLoader {
components {
- add { data, _ ->
- object : Fetcher {
- override suspend fun fetch(): FetchResult {
- return FetchResult.Painter(
+ add(
+ Fetcher.Factory { data, _ ->
+ Fetcher {
+ FetchResult.OfPainter(
when (data) {
"1" -> resultPainter1
"2" -> resultPainter2
@@ -41,8 +43,8 @@ class ImageLoaderTest {
},
)
}
- }
- }
+ },
+ )
}
}
}
@@ -53,7 +55,7 @@ class ImageLoaderTest {
imageLoader.async(request).test {
assertEquals(ImageEvent.Start, awaitItem())
assertEquals(ImageEvent.StartWithFetch, awaitItem())
- assertEquals(ImageResult.Painter(resultPainter1), awaitItem())
+ assertEquals(ImageResult.OfPainter(resultPainter1), awaitItem())
awaitComplete()
}
}
@@ -65,17 +67,17 @@ class ImageLoaderTest {
emit(ImageRequest("2"))
emit(ImageRequest("3") { skipEvent = true })
}
- imageLoader.async(requestFlow).test {
+ requestFlow.transform { emitAll(imageLoader.async(it)) }.test {
// 1
assertEquals(ImageEvent.Start, awaitItem())
assertEquals(ImageEvent.StartWithFetch, awaitItem())
- assertEquals(ImageResult.Painter(resultPainter1), awaitItem())
+ assertEquals(ImageResult.OfPainter(resultPainter1), awaitItem())
// 2
assertEquals(ImageEvent.Start, awaitItem())
assertEquals(ImageEvent.StartWithFetch, awaitItem())
- assertEquals(ImageResult.Painter(resultPainter2), awaitItem())
+ assertEquals(ImageResult.OfPainter(resultPainter2), awaitItem())
// 3
- assertEquals(ImageResult.Painter(resultPainter3), awaitItem())
+ assertEquals(ImageResult.OfPainter(resultPainter3), awaitItem())
awaitComplete()
}
}
diff --git a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/component/mapper/BaseMapperTest.kt b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/component/mapper/BaseMapperTest.kt
new file mode 100644
index 00000000..dfda284b
--- /dev/null
+++ b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/component/mapper/BaseMapperTest.kt
@@ -0,0 +1,18 @@
+package com.seiko.imageloader.component.mapper
+
+import com.seiko.imageloader.option.Options
+import kotlin.test.BeforeTest
+
+abstract class BaseMapperTest> {
+
+ abstract fun createMapper(): M
+
+ private lateinit var mapper: M
+
+ @BeforeTest
+ fun onBefore() {
+ mapper = createMapper()
+ }
+
+ fun map(data: Any, options: Options = Options()) = mapper.map(data, options)
+}
diff --git a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/component/mapper/KtorUrlMapperTest.kt b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/component/mapper/KtorUrlMapperTest.kt
new file mode 100644
index 00000000..ca09d41e
--- /dev/null
+++ b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/component/mapper/KtorUrlMapperTest.kt
@@ -0,0 +1,21 @@
+package com.seiko.imageloader.component.mapper
+
+import kotlin.test.Test
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+class KtorUrlMapperTest : BaseMapperTest() {
+
+ override fun createMapper() = KtorUrlMapper()
+
+ @Test
+ fun test() {
+ assertNull(map("data:image/png;base64,iVBOR..."))
+ assertNotNull(map("http://www.google.com"))
+ assertNotNull(map("https://www.google.com"))
+ assertNull(map("content://com.example.project:80/folder/etc"))
+ assertNull(map("imarsthink://www.marsthink.com/travel/oversea?id=1000"))
+ assertNull(map("tel:10086"))
+ assertNull(map("geo:52.76,-79.0342"))
+ }
+}
diff --git a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/component/mapper/StringUriMapperTest.kt b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/component/mapper/StringUriMapperTest.kt
new file mode 100644
index 00000000..0e408b8e
--- /dev/null
+++ b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/component/mapper/StringUriMapperTest.kt
@@ -0,0 +1,21 @@
+package com.seiko.imageloader.component.mapper
+
+import kotlin.test.Test
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+class StringUriMapperTest : BaseMapperTest() {
+
+ override fun createMapper() = StringUriMapper()
+
+ @Test
+ fun test() {
+ assertNull(map("data:image/png;base64,iVBOR..."))
+ assertNull(map("http://www.google.com"))
+ assertNull(map("https://www.google.com"))
+ assertNotNull(map("content://com.example.project:80/folder/etc"))
+ assertNotNull(map("imarsthink://www.marsthink.com/travel/oversea?id=1000"))
+ assertNotNull(map("tel:10086"))
+ assertNotNull(map("geo:52.76,-79.0342"))
+ }
+}
diff --git a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/model/ImageActionTest.kt b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/model/ImageActionTest.kt
new file mode 100644
index 00000000..f34ac3ff
--- /dev/null
+++ b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/model/ImageActionTest.kt
@@ -0,0 +1,137 @@
+package com.seiko.imageloader.model
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.painter.Painter
+import com.seiko.imageloader.rememberImageAction
+import com.seiko.imageloader.rememberImageActionPainter
+import com.seiko.imageloader.rememberImageSuccessPainter
+
+class ImageActionTest {
+
+ @Composable
+ fun test() {
+ val request = remember { ImageRequest("") }
+ val action by rememberImageAction(request)
+ when_normal_test(action)
+ when_image_action_test(action)
+ when_image_action_no_result_test(action)
+ when_image_event_test(action)
+ when_image_event_no_result_test(action)
+ when_image_result_test(action)
+ when_image_result_success_no_result_failure_test(action)
+ when_all_test(action)
+ }
+
+ @Composable
+ private fun when_normal_test(action: ImageAction) {
+ when (action) {
+ is ImageAction.Loading -> LoadingUI()
+ is ImageAction.Success -> SuccessUI(rememberImageActionPainter(action))
+ is ImageAction.Failure -> ErrorUI(action.error)
+ }
+ }
+
+ @Composable
+ private fun when_image_action_test(action: ImageAction) {
+ when (action) {
+ is ImageEvent -> LoadingUI()
+ is ImageResult -> {
+ when (action) {
+ is ImageAction.Success -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageAction.Failure -> ErrorUI(action.error)
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun when_image_action_no_result_test(action: ImageAction) {
+ when (action) {
+ is ImageEvent -> LoadingUI()
+ is ImageAction.Success -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageAction.Failure -> ErrorUI(action.error)
+ }
+ }
+
+ @Composable
+ private fun when_image_event_test(action: ImageAction) {
+ when (action) {
+ is ImageEvent.Start,
+ is ImageEvent.StartWithMemory,
+ is ImageEvent.StartWithDisk,
+ is ImageEvent.StartWithFetch,
+ -> LoadingUI()
+ is ImageResult -> {
+ when (action) {
+ is ImageAction.Success -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageAction.Failure -> ErrorUI(action.error)
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun when_image_event_no_result_test(action: ImageAction) {
+ when (action) {
+ is ImageEvent.Start,
+ is ImageEvent.StartWithMemory,
+ is ImageEvent.StartWithDisk,
+ is ImageEvent.StartWithFetch,
+ -> LoadingUI()
+ is ImageAction.Success -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageAction.Failure -> ErrorUI(action.error)
+ }
+ }
+
+ @Composable
+ private fun when_image_result_test(action: ImageAction) {
+ when (action) {
+ is ImageEvent -> LoadingUI()
+ is ImageResult.OfImage -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageResult.OfBitmap -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageResult.OfPainter -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageResult.OfError -> ErrorUI(action.error)
+ is ImageResult.OfSource -> ErrorUI(action.error)
+ }
+ }
+
+ @Composable
+ private fun when_image_result_success_no_result_failure_test(action: ImageAction) {
+ when (action) {
+ is ImageAction.Loading -> LoadingUI()
+ is ImageResult.OfImage -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageResult.OfBitmap -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageResult.OfPainter -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageAction.Failure -> ErrorUI(action.error)
+ }
+ }
+
+ @Composable
+ private fun when_all_test(action: ImageAction) {
+ when (action) {
+ is ImageEvent.Start,
+ is ImageEvent.StartWithMemory,
+ is ImageEvent.StartWithDisk,
+ is ImageEvent.StartWithFetch,
+ -> LoadingUI()
+ is ImageResult.OfImage -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageResult.OfBitmap -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageResult.OfPainter -> SuccessUI(rememberImageSuccessPainter(action))
+ is ImageResult.OfError -> ErrorUI(action.error)
+ is ImageResult.OfSource -> ErrorUI(action.error)
+ }
+ }
+
+ @Composable
+ private fun LoadingUI() = Unit
+
+ @Suppress("UNUSED_PARAMETER")
+ @Composable
+ private fun SuccessUI(painter: Painter) = Unit
+
+ @Suppress("UNUSED_PARAMETER")
+ @Composable
+ private fun ErrorUI(error: Throwable) = Unit
+}
diff --git a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/model/ImageRequestTest.kt b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/model/ImageRequestTest.kt
new file mode 100644
index 00000000..5979ffce
--- /dev/null
+++ b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/model/ImageRequestTest.kt
@@ -0,0 +1,111 @@
+package com.seiko.imageloader.model
+
+import com.seiko.imageloader.BitmapConfig
+import com.seiko.imageloader.cache.CachePolicy
+import com.seiko.imageloader.component.decoder.Decoder
+import com.seiko.imageloader.component.fetcher.Fetcher
+import com.seiko.imageloader.component.keyer.Keyer
+import com.seiko.imageloader.component.mapper.Mapper
+import com.seiko.imageloader.option.Options
+import com.seiko.imageloader.option.Scale
+import com.seiko.imageloader.option.takeFrom
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+class ImageRequestTest {
+
+ @Test
+ fun image_request_data_test() {
+ assertEquals(ImageRequest {}.data, NullRequestData)
+ assertEquals(ImageRequest(1).data, 1)
+ assertEquals(ImageRequest("data").data, "data")
+ assertEquals(ImageRequest(ImageRequest("data")).data, "data")
+ assertEquals(ImageRequest(ImageRequest("data")) {}.data, "data")
+ }
+
+ @Test
+ fun image_request_options_test() {
+ val request = ImageRequest {
+ options {
+ allowInexactSize = true
+ premultipliedAlpha = false
+ retryIfDiskDecodeError = false
+ bitmapConfig = BitmapConfig.ALPHA_8
+ scale = Scale.FIT
+ memoryCachePolicy = CachePolicy.DISABLED
+ diskCachePolicy = CachePolicy.READ_ONLY
+ repeatCount = 5
+ maxImageSize = 100
+ extra {
+ put("a", "aa")
+ }
+ }
+ }
+ val options = Options {
+ takeFrom(request)
+ }
+ assertTrue(options.allowInexactSize)
+ assertFalse(options.premultipliedAlpha)
+ assertFalse(options.retryIfDiskDecodeError)
+ assertEquals(options.bitmapConfig, BitmapConfig.ALPHA_8)
+ assertEquals(options.scale, Scale.FIT)
+ assertEquals(options.memoryCachePolicy, CachePolicy.DISABLED)
+ assertEquals(options.diskCachePolicy, CachePolicy.READ_ONLY)
+ assertEquals(options.repeatCount, 5)
+ assertEquals(options.maxImageSize, 100)
+ assertEquals(options.extra["a"], "aa")
+ }
+
+ @Test
+ fun image_request_other_params_test() {
+ val mapperFactory = Mapper { _, _ -> }
+ val keyerFactory = Keyer { _, _, _ -> null }
+ val fetcherFactory = Fetcher.Factory { _, _ ->
+ Fetcher { null }
+ }
+ val decoderFactory = Decoder.Factory { _, _ ->
+ Decoder { null }
+ }
+ val request = ImageRequest {
+ extra {
+ put("a", "aaa")
+ }
+ skipEvent = true
+ components {
+ add(mapperFactory)
+ add(keyerFactory)
+ add(fetcherFactory)
+ add(decoderFactory)
+ }
+ }
+ assertEquals(request.extra["a"], "aaa")
+ assertEquals(request.skipEvent, true)
+ assertNotNull(request.components)
+ assertEquals(request.components.mappers.first(), mapperFactory)
+ assertEquals(request.components.keyers.first(), keyerFactory)
+ assertEquals(request.components.fetcherFactories.first(), fetcherFactory)
+ assertEquals(request.components.decoderFactories.first(), decoderFactory)
+ }
+
+ @Test
+ fun image_request_compare_test() {
+ val request1 = ImageRequest {
+ data("aa")
+ skipEvent = true
+ }
+ assertNotEquals(request1, ImageRequest("aa"))
+ assertEquals(request1, ImageRequest("aa") { skipEvent = true })
+ val request2 = ImageRequest {
+ data("aa")
+ extra {
+ put("a", "b")
+ }
+ }
+ assertNotEquals(request2, ImageRequest("aa"))
+ assertEquals(request2, ImageRequest("aa") { extra { put("a", "b") } })
+ }
+}
diff --git a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/option/SizeResolverTest.kt b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/option/SizeResolverTest.kt
new file mode 100644
index 00000000..020a96db
--- /dev/null
+++ b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/option/SizeResolverTest.kt
@@ -0,0 +1,31 @@
+package com.seiko.imageloader.option
+
+import androidx.compose.ui.geometry.Size
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class SizeResolverTest {
+
+ @Test
+ fun compare_test() {
+ assertEquals(
+ SizeResolver(Size(10f, 10f)),
+ SizeResolver(Size(10.0f, 10.0f)),
+ )
+ assertEquals(
+ SizeResolver(Size.Unspecified),
+ SizeResolver.Unspecified,
+ )
+ }
+
+ @Test
+ fun async_size_test() = runTest {
+ val sizeResolver = AsyncSizeResolver()
+ launch {
+ sizeResolver.setSize(Size(10f, 10f))
+ }
+ assertEquals(Size(10f, 10f), sizeResolver.size())
+ }
+}
diff --git a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlCommonTest.kt b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlCommonTest.kt
index 0de7e4e2..780f6bbd 100644
--- a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlCommonTest.kt
+++ b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlCommonTest.kt
@@ -33,7 +33,7 @@ abstract class ChangeImageUrlCommonTest {
3 -> Color.Gray
else -> Color.Red // no display
}
- ImageResult.Painter(ColorPainter(color))
+ ImageResult.OfPainter(ColorPainter(color))
}
}
}
diff --git a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotCommonTest.kt b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotCommonTest.kt
index 7fb16a8b..7334c3fb 100644
--- a/image-loader/src/commonTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotCommonTest.kt
+++ b/image-loader/src/commonTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotCommonTest.kt
@@ -29,7 +29,7 @@ abstract class ComposeScreenShotCommonTest {
addInterceptor(
Interceptor { chain ->
val color = if (url == chain.request.data) Color.Green else Color.Blue
- ImageResult.Painter(ColorPainter(color))
+ ImageResult.OfPainter(ColorPainter(color))
},
)
}
@@ -58,7 +58,7 @@ abstract class ComposeScreenShotCommonTest {
useDefaultInterceptors = false
addInterceptor {
delay(100)
- ImageResult.Painter(ColorPainter(Color.Green))
+ ImageResult.OfPainter(ColorPainter(Color.Green))
}
}
}
@@ -81,7 +81,7 @@ abstract class ComposeScreenShotCommonTest {
interceptor {
useDefaultInterceptors = false
addInterceptor {
- ImageResult.Error(RuntimeException("error"))
+ ImageResult.OfError(RuntimeException("error"))
}
}
}
diff --git a/image-loader/src/desktopMain/singleton/com/seiko/imageloader/LocalImageLoader.desktop.kt b/image-loader/src/desktopMain/singleton/com/seiko/imageloader/LocalImageLoader.desktop.kt
index 1f1635ce..20942199 100644
--- a/image-loader/src/desktopMain/singleton/com/seiko/imageloader/LocalImageLoader.desktop.kt
+++ b/image-loader/src/desktopMain/singleton/com/seiko/imageloader/LocalImageLoader.desktop.kt
@@ -5,6 +5,6 @@ import androidx.compose.runtime.staticCompositionLocalOf
actual fun createImageLoaderProvidableCompositionLocal() = ImageLoaderProvidableCompositionLocal(
delegate = staticCompositionLocalOf {
// no disk cache in desktop
- ImageLoader.Default
+ ImageLoader.createDefault()
},
)
diff --git a/image-loader/src/desktopTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlTest.kt b/image-loader/src/desktopTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlTest.kt
index 113e7076..8b7958be 100644
--- a/image-loader/src/desktopTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlTest.kt
+++ b/image-loader/src/desktopTest/kotlin/com/seiko/imageloader/screenshot/ChangeImageUrlTest.kt
@@ -19,13 +19,13 @@ class ChangeImageUrlTest : ChangeImageUrlCommonTest() {
@Before
fun initRoborazziConfig() {
RoborazziContext.setRuleOverrideOutputDirectory(
- outputDirectory = "src/desktopTest/snapshots/images",
+ outputDirectory = "build/outputs/roborazzi/desktop",
)
}
- @OptIn(ExperimentalTestApi::class)
+ @OptIn(ExperimentalTestApi::class, ExperimentalRoborazziApi::class)
@Test
- fun test_image_change() = runDesktopComposeUiTest(width = 80, height = 80) {
+ fun desktop_test_image_change() = runDesktopComposeUiTest(width = 80, height = 80) {
setContent {
TestUI()
}
diff --git a/image-loader/src/desktopTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotTest.kt b/image-loader/src/desktopTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotTest.kt
index 431ed763..59ba1e64 100644
--- a/image-loader/src/desktopTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotTest.kt
+++ b/image-loader/src/desktopTest/kotlin/com/seiko/imageloader/screenshot/ComposeScreenShotTest.kt
@@ -18,22 +18,22 @@ class ComposeScreenShotTest : ComposeScreenShotCommonTest() {
@Before
fun initRoborazziConfig() {
RoborazziContext.setRuleOverrideOutputDirectory(
- outputDirectory = "src/desktopTest/snapshots/images",
+ outputDirectory = "build/outputs/roborazzi/desktop",
)
}
@Test
- fun test_load_image() = runMyDesktopComposeUiTest(width = 100 + 8 + 100) {
+ fun desktop_test_load_image() = runMyDesktopComposeUiTest(width = 100 + 8 + 100) {
TestLoadImageUI()
}
@Test
- fun test_placeholder_painter() = runMyDesktopComposeUiTest {
+ fun desktop_test_placeholder_painter() = runMyDesktopComposeUiTest {
TestPlaceholderPainterUI()
}
@Test
- fun test_error_painter() = runMyDesktopComposeUiTest {
+ fun desktop_test_error_painter() = runMyDesktopComposeUiTest {
TestErrorPainterUI()
}
diff --git a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change.png b/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change.png
deleted file mode 100644
index bab35f16..00000000
--- a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:8efefc34190788fa550753f68a91e24901c35db9d9f5aae00f037c6d93ee6a81
-size 244
diff --git a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_2.png b/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_2.png
deleted file mode 100644
index 0a6a9b59..00000000
--- a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_2.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:0a0ddfea35a63c2e68784a0ce1eb7830e96e36b304476b480321cdfbdb165cb7
-size 246
diff --git a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_3.png b/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_3.png
deleted file mode 100644
index 1a366673..00000000
--- a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_3.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:71a17cd9e761a358e8d6d3ee1d9549a21bf897c8d986f44eb443aea5c9fbfcb9
-size 246
diff --git a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_4.png b/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_4.png
deleted file mode 100644
index f2765a23..00000000
--- a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ChangeImageUrlTest.test_image_change_4.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c517fa85a7044189415cde4815296aefb0a3cae686cffcb3f62a8552ad96ba90
-size 246
diff --git a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_error_painter.png b/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_error_painter.png
deleted file mode 100644
index cd24e6a4..00000000
--- a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_error_painter.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:e025c7f7560d3dcef1bb0d7371573048c5fc10f35435f30733f31ee5a55d6340
-size 310
diff --git a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_load_image.png b/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_load_image.png
deleted file mode 100644
index 5a1c6035..00000000
--- a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_load_image.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:6016a459f91e28f88dd32a57658a4897d0bf18ff17594a3ab585b2449698b642
-size 661
diff --git a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_placeholder_painter.png b/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_placeholder_painter.png
deleted file mode 100644
index e2aeb82a..00000000
--- a/image-loader/src/desktopTest/snapshots/images/com.seiko.imageloader.screenshot.ComposeScreenShotTest.test_placeholder_painter.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a5154ac23b0ee9042e5f107c05782349f9aa544a8b0367b4145d2b767ef9480f
-size 311
diff --git a/image-loader/src/jsMain/kotlin/com/seiko/imageloader/util/LruCache.kt b/image-loader/src/jsMain/kotlin/com/seiko/imageloader/util/LruCache.kt
index 7919b203..ebfd7922 100644
--- a/image-loader/src/jsMain/kotlin/com/seiko/imageloader/util/LruCache.kt
+++ b/image-loader/src/jsMain/kotlin/com/seiko/imageloader/util/LruCache.kt
@@ -1,5 +1,10 @@
package com.seiko.imageloader.util
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.internal.SynchronizedObject
+import kotlinx.coroutines.internal.synchronized
+
+@OptIn(InternalCoroutinesApi::class)
actual open class LruCache actual constructor(maxSize: Int) {
private var _maxSize = 0
@@ -12,7 +17,7 @@ actual open class LruCache actual constructor(maxSize: Int) {
private val map: LinkedHashMap
- private val syncObject = LockObject()
+ private val syncObject = SynchronizedObject()
actual fun size() = synchronized(syncObject) { _size }
diff --git a/image-loader/src/jsMain/kotlin/com/seiko/imageloader/util/Platform.js.kt b/image-loader/src/jsMain/kotlin/com/seiko/imageloader/util/Platform.js.kt
index 2622cab4..e2088450 100644
--- a/image-loader/src/jsMain/kotlin/com/seiko/imageloader/util/Platform.js.kt
+++ b/image-loader/src/jsMain/kotlin/com/seiko/imageloader/util/Platform.js.kt
@@ -1,7 +1,5 @@
package com.seiko.imageloader.util
-import io.ktor.client.engine.HttpClientEngine
-import io.ktor.client.engine.js.Js
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import okio.FileSystem
@@ -28,14 +26,6 @@ actual class WeakReference actual constructor(referred: T) {
}
}
-actual typealias LockObject = Any
-
-internal actual inline fun synchronized(lock: LockObject, block: () -> R): R {
- return kotlinx.atomicfu.locks.synchronized(lock, block)
-}
-
internal actual val ioDispatcher: CoroutineDispatcher get() = Dispatchers.Default
-internal actual val httpEngine: HttpClientEngine get() = Js.create()
-
internal actual val defaultFileSystem: FileSystem? get() = null
diff --git a/image-loader/src/jsMain/singleton/com/seiko/imageloader/LocalImageLoader.js.kt b/image-loader/src/jsMain/singleton/com/seiko/imageloader/LocalImageLoader.js.kt
index 51414222..53f4d3b6 100644
--- a/image-loader/src/jsMain/singleton/com/seiko/imageloader/LocalImageLoader.js.kt
+++ b/image-loader/src/jsMain/singleton/com/seiko/imageloader/LocalImageLoader.js.kt
@@ -5,6 +5,6 @@ import androidx.compose.runtime.staticCompositionLocalOf
actual fun createImageLoaderProvidableCompositionLocal() = ImageLoaderProvidableCompositionLocal(
delegate = staticCompositionLocalOf {
// no disk cache in js
- ImageLoader.Default
+ ImageLoader.createDefault()
},
)
diff --git a/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/component/fetcher/ByteBufferFetcher.kt b/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/component/fetcher/ByteBufferFetcher.kt
index 7570cbcd..a0255f68 100644
--- a/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/component/fetcher/ByteBufferFetcher.kt
+++ b/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/component/fetcher/ByteBufferFetcher.kt
@@ -14,7 +14,7 @@ class ByteBufferFetcher private constructor(
// Reset the position so we can read the byte buffer again.
data.position(0)
}
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = source,
)
}
diff --git a/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/component/fetcher/FileFetcher.kt b/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/component/fetcher/FileFetcher.kt
index d2b4e789..3e2afc9c 100644
--- a/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/component/fetcher/FileFetcher.kt
+++ b/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/component/fetcher/FileFetcher.kt
@@ -12,7 +12,7 @@ class FileFetcher private constructor(
private val data: File,
) : Fetcher {
override suspend fun fetch(): FetchResult {
- return FetchResult.Source(
+ return FetchResult.OfSource(
source = data.source().buffer(),
extra = extraData {
mimeType(getMimeTypeFromExtension(data.extension))
diff --git a/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/util/Platform.jvm.kt b/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/util/Platform.jvm.kt
index a566ac14..4223145d 100644
--- a/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/util/Platform.jvm.kt
+++ b/image-loader/src/jvmMain/kotlin/com/seiko/imageloader/util/Platform.jvm.kt
@@ -1,7 +1,5 @@
package com.seiko.imageloader.util
-import io.ktor.client.engine.HttpClientEngine
-import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.jvm.javaio.toInputStream
import okio.BufferedSource
@@ -11,18 +9,10 @@ import okio.source
actual typealias WeakReference = java.lang.ref.WeakReference
-actual typealias LockObject = Any
-
-internal actual inline fun synchronized(lock: LockObject, block: () -> R): R {
- return kotlin.synchronized(lock, block)
-}
-
internal actual suspend fun ByteReadChannel.source(): BufferedSource {
return toInputStream().source().buffer()
}
-internal actual val httpEngine: HttpClientEngine get() = OkHttp.create()
-
internal actual val defaultFileSystem: FileSystem? get() = FileSystem.SYSTEM
internal expect fun getMimeTypeFromExtension(extension: String): String?
diff --git a/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/GifDecoder.kt b/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/GifDecoder.kt
index 0f36064d..00549ccb 100644
--- a/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/GifDecoder.kt
+++ b/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/GifDecoder.kt
@@ -16,7 +16,7 @@ class GifDecoder private constructor(
val codec = source.use {
Codec.makeFromData(Data.makeFromBytes(it.readByteArray()))
}
- return DecodeResult.Painter(
+ return DecodeResult.OfPainter(
painter = GifPainter(
codec = codec,
repeatCount = options.repeatCount,
@@ -25,7 +25,7 @@ class GifDecoder private constructor(
}
class Factory : Decoder.Factory {
- override suspend fun create(source: DecodeSource, options: Options): Decoder? {
+ override fun create(source: DecodeSource, options: Options): Decoder? {
if (!options.playAnimate) return null
if (!isGif(source.source)) return null
return GifDecoder(source.source, options)
diff --git a/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/SkiaImageDecoder.kt b/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/SkiaImageDecoder.kt
index 160dda00..6f314828 100644
--- a/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/SkiaImageDecoder.kt
+++ b/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/SkiaImageDecoder.kt
@@ -1,5 +1,6 @@
package com.seiko.imageloader.component.decoder
+import androidx.compose.ui.geometry.isSpecified
import com.seiko.imageloader.option.Options
import com.seiko.imageloader.util.DEFAULT_MAX_PARALLELISM
import com.seiko.imageloader.util.calculateDstSize
@@ -11,7 +12,6 @@ import org.jetbrains.skia.Bitmap
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Image
import org.jetbrains.skia.Rect
-import org.jetbrains.skia.SamplingMode
import org.jetbrains.skia.impl.use
class SkiaImageDecoder private constructor(
@@ -24,7 +24,7 @@ class SkiaImageDecoder private constructor(
val image = source.use {
Image.makeFromEncoded(it.readByteArray())
}
- DecodeResult.Bitmap(
+ DecodeResult.OfBitmap(
bitmap = image.toBitmap(),
)
}
@@ -32,16 +32,24 @@ class SkiaImageDecoder private constructor(
// TODO wait to fix high probability crash on ios
private fun Image.toBitmap(): Bitmap {
val bitmap = Bitmap()
- val (dstWidth, dstHeight) = calculateDstSize(width, height, options.maxImageSize)
+
+ val srcWidth = width
+ val srcHeight = height
+
+ val maxImageSize = if (options.size.isSpecified && !options.size.isEmpty()) {
+ minOf(options.size.width, options.size.height).toInt()
+ .coerceAtMost(options.maxImageSize)
+ } else {
+ options.maxImageSize
+ }
+ val (dstWidth, dstHeight) = calculateDstSize(srcWidth, srcHeight, maxImageSize)
+
bitmap.allocN32Pixels(dstWidth, dstHeight)
Canvas(bitmap).use { canvas ->
canvas.drawImageRect(
this,
- Rect.makeWH(width.toFloat(), height.toFloat()),
+ Rect.makeWH(srcWidth.toFloat(), srcHeight.toFloat()),
Rect.makeWH(dstWidth.toFloat(), dstHeight.toFloat()),
- SamplingMode.DEFAULT,
- null,
- true,
)
}
bitmap.setImmutable()
@@ -54,7 +62,7 @@ class SkiaImageDecoder private constructor(
private val parallelismLock = Semaphore(maxParallelism)
- override suspend fun create(source: DecodeSource, options: Options): Decoder {
+ override fun create(source: DecodeSource, options: Options): Decoder {
return SkiaImageDecoder(
source = source.source,
options = options,
diff --git a/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/SvgDecoder.kt b/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/SvgDecoder.kt
index 84caa2ee..7124e316 100644
--- a/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/SvgDecoder.kt
+++ b/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/component/decoder/SvgDecoder.kt
@@ -20,18 +20,15 @@ class SvgDecoder private constructor(
val data = source.use {
Data.makeFromBytes(it.readByteArray())
}
- val requestSize = options.sizeResolver.run {
- density.size()
- }
- return DecodeResult.Painter(
- painter = SVGPainter(SVGDOM(data), density, requestSize),
+ return DecodeResult.OfPainter(
+ painter = SVGPainter(SVGDOM(data), density, options.size),
)
}
class Factory(
private val density: Density,
) : Decoder.Factory {
- override suspend fun create(source: DecodeSource, options: Options): Decoder? {
+ override fun create(source: DecodeSource, options: Options): Decoder? {
if (!isApplicable(source)) return null
return SvgDecoder(
source = source.source,
diff --git a/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/util/GifPainter.kt b/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/util/GifPainter.kt
index 6a5aedbf..4d0117fd 100644
--- a/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/util/GifPainter.kt
+++ b/image-loader/src/skiaMain/kotlin/com/seiko/imageloader/util/GifPainter.kt
@@ -21,7 +21,7 @@ internal class GifPainter(
private val durations = codec.framesInfo.map { it.duration }
private val totalDuration = durations.sum()
- private var startTime = -1L
+ private var startTimeMillis = -1L
private var frame by mutableStateOf(0)
private var loopIteration = -1
@@ -52,14 +52,20 @@ internal class GifPainter(
}
override fun isPlay(): Boolean {
- return totalDuration > 0 && (repeatCount == Options.REPEAT_INFINITE || loopIteration++ < repeatCount)
+ return totalDuration > 0 && (repeatCount == Options.REPEAT_INFINITE || repeatCount > 0)
+ }
+
+ override fun nextPlay(): Boolean {
+ return totalDuration > 0 && (repeatCount == Options.REPEAT_INFINITE || loopIteration < repeatCount)
}
override fun update(frameTimeMillis: Long) {
- if (startTime == -1L) {
- startTime = frameTimeMillis
+ if (startTimeMillis == -1L) {
+ startTimeMillis = frameTimeMillis
}
- frame = frameOf(time = (frameTimeMillis - startTime) % totalDuration)
+ val playTimeMillis = frameTimeMillis - startTimeMillis
+ frame = frameOf(time = playTimeMillis % totalDuration)
+ loopIteration = (playTimeMillis / totalDuration).toInt()
}
// WARNING: it is not optimal
@@ -87,7 +93,7 @@ internal class GifPainter(
}
private fun clear() {
- startTime = -1
+ startTimeMillis = -1
frame = 0
loopIteration = -1
bitmapCache?.close()