Add uikit to experimental/examples/imageviewer (#2571)
15
artwork/imageviewerrepo/pictures.json
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"big": "1.jpg", "small": "small/1.jpg"},
|
||||
{"big": "2.jpg", "small": "small/2.jpg"},
|
||||
{"big": "3.jpg", "small": "small/3.jpg"},
|
||||
{"big": "4.jpg", "small": "small/4.jpg"},
|
||||
{"big": "5.jpg", "small": "small/5.jpg"},
|
||||
{"big": "6.jpg", "small": "small/6.jpg"},
|
||||
{"big": "7.jpg", "small": "small/7.jpg"},
|
||||
{"big": "8.jpg", "small": "small/8.jpg"},
|
||||
{"big": "9.jpg", "small": "small/9.jpg"},
|
||||
{"big": "10.jpg", "small": "small/10.jpg"},
|
||||
{"big": "11.jpg", "small": "small/11.jpg"},
|
||||
{"big": "12.jpg", "small": "small/12.jpg"},
|
||||
{"big": "13.jpg", "small": "small/13.jpg"}
|
||||
]
|
||||
BIN
artwork/imageviewerrepo/small/1.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
artwork/imageviewerrepo/small/10.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
artwork/imageviewerrepo/small/11.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
artwork/imageviewerrepo/small/12.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
artwork/imageviewerrepo/small/13.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
artwork/imageviewerrepo/small/2.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
artwork/imageviewerrepo/small/3.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
artwork/imageviewerrepo/small/4.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
artwork/imageviewerrepo/small/5.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
artwork/imageviewerrepo/small/6.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
artwork/imageviewerrepo/small/7.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
artwork/imageviewerrepo/small/8.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
artwork/imageviewerrepo/small/9.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
6
experimental/examples/imageviewer/.gitignore
vendored
@@ -13,3 +13,9 @@ build/
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
iosApp/Podfile.lock
|
||||
iosApp/Pods/*
|
||||
iosApp/Imageviewer.xcworkspace/*
|
||||
iosApp/Imageviewer.xcodeproj/*
|
||||
!iosApp/Imageviewer.xcodeproj/project.pbxproj
|
||||
shared/shared.podspec
|
||||
|
||||
8
experimental/examples/imageviewer/.run/desktop.run.xml → experimental/examples/imageviewer/.run/desktopApp.run.xml
Executable file → Normal file
@@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="desktop" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<configuration default="false" name="desktopApp" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/desktop" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/desktopApp" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
@@ -15,7 +15,9 @@
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -0,0 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="iosApp (AndroidStudio)" type="KmmRunConfiguration" factoryName="iOS Application" CONFIG_VERSION="1" XCODE_PROJECT="$PROJECT_DIR$/iosApp/Imageviewer.xcworkspace" XCODE_CONFIGURATION="Debug" XCODE_SCHEME="Imageviewer">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.kmm.ios.BuildIOSAppTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
11
experimental/examples/imageviewer/.run/iosApp.run.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="iosApp" type="AppleRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="Imageviewer" TARGET_NAME="Imageviewer" CONFIG_NAME="Debug" IS_LOCATION_SIMULATION_SUPPORTED="true" SCHEME_NAME="iosApp" IS_LOCATION_SIMULATION_ALLOWED="true" LOCATION_SCENARIO_ID="com.apple.dt.IDEFoundation.CurrentLocationScenarioIdentifier" LOCATION_SCENARIO_TYPE="1" APPLICATION_LANGUAGE="IDELaunchSchemeLanguageUseSystemLanguage" APPLICATION_REGION="" RUN_TARGET_PROJECT_NAME="Imageviewer" RUN_TARGET_NAME="Imageviewer" MAKE_ACTIVE="TRUE" SHOULD_DEBUG_EXTENSIONS="false">
|
||||
<EXTENSION ID="org.jetbrains.appcode.reveal.RevealRunConfigurationExtension">
|
||||
<REVEAL_SETTINGS autoInject="false" autoInstall="true" askToEnableAutoInstall="true" />
|
||||
</EXTENSION>
|
||||
<embedded_app_extension_list />
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,11 +1,27 @@
|
||||
An example of image gallery for remote server image viewing, based on Jetpack Compose UI library (desktop and android).
|
||||
# Imageviewer
|
||||
|
||||
### Running desktop application
|
||||
* To run, launch command: `./gradlew :desktop:run`
|
||||
* Or choose **desktop** configuration in IDE and run it.
|
||||

|
||||
An example of image gallery for remote server image viewing,
|
||||
based on Compose Multiplatform UI library (desktop, android and iOS).
|
||||
|
||||
## How to run
|
||||
|
||||
Choose a run configuration for an appropriate target in IDE and run it.
|
||||
|
||||

|
||||
|
||||
To run on iOS device, please correct `iosApp/Configuration/TeamId.xcconfig` with your Apple Team ID.
|
||||
Alternatively, you may setup signing within XCode opening `iosApp/Imageviewer.xcworkspace` and then
|
||||
using "Signing & Capabilities" tab of `ImageViewer` target.
|
||||
|
||||
Then choose **iosApp** configuration in IDE and run it
|
||||
(may also be referred as `ImageViewer` in the Run Configurations or `iosApp (AndroidStudio)` for Android studio).
|
||||
|
||||
## Run on desktop via Gradle
|
||||
|
||||
`./gradlew desktopApp:run`
|
||||
|
||||
### Building native desktop distribution
|
||||
|
||||
```
|
||||
./gradlew :desktop:packageDistributionForCurrentOS
|
||||
# outputs are written to desktop/build/compose/binaries
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
id("org.jetbrains.compose")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 32
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
targetSdk = 32
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation("androidx.activity:activity-compose:1.5.0")
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package example.imageviewer
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import example.imageviewer.view.AppUI
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.model.ImageRepository
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val content = ContentState.applyContent(
|
||||
this@MainActivity,
|
||||
"https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list"
|
||||
)
|
||||
|
||||
setContent {
|
||||
AppUI(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
33
experimental/examples/imageviewer/androidApp/build.gradle.kts
Executable file
@@ -0,0 +1,33 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.compose")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android()
|
||||
sourceSets {
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
implementation(project(":shared"))
|
||||
implementation("androidx.appcompat:appcompat:1.5.1")
|
||||
implementation("androidx.activity:activity-compose:1.6.1")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
defaultConfig {
|
||||
applicationId = "org.jetbrains.imageviewer"
|
||||
minSdk = 24
|
||||
targetSdk = 33
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -0,0 +1,15 @@
|
||||
package example.imageviewer
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import example.imageviewer.view.ImageViewerAndroid
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
ImageViewerAndroid()
|
||||
}
|
||||
}
|
||||
}
|
||||
1
experimental/examples/imageviewer/build.gradle.kts
Executable file → Normal file
@@ -14,5 +14,6 @@ allprojects {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android()
|
||||
jvm("desktop")
|
||||
sourceSets {
|
||||
named("commonMain") {
|
||||
dependencies {
|
||||
api(compose.runtime)
|
||||
api(compose.foundation)
|
||||
api(compose.material)
|
||||
implementation("io.ktor:ktor-client-core:1.4.1")
|
||||
}
|
||||
}
|
||||
named("androidMain") {
|
||||
dependencies {
|
||||
api("androidx.appcompat:appcompat:1.5.1")
|
||||
api("androidx.core:core-ktx:1.8.0")
|
||||
implementation("io.ktor:ktor-client-cio:1.4.1")
|
||||
}
|
||||
}
|
||||
named("desktopMain") {
|
||||
dependencies {
|
||||
api(compose.desktop.common)
|
||||
implementation("io.ktor:ktor-client-cio:1.4.1")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 32
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
targetSdk = 32
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
named("main") {
|
||||
manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
||||
res.srcDirs("src/androidMain/res")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="example.imageviewer.common"/>
|
||||
@@ -1,7 +0,0 @@
|
||||
package example.imageviewer.core
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
interface BitmapFilter {
|
||||
fun apply(bitmap: Bitmap) : Bitmap
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
package example.imageviewer.model
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import example.imageviewer.common.R
|
||||
import example.imageviewer.core.FilterType
|
||||
import example.imageviewer.model.filtration.FiltersManager
|
||||
import example.imageviewer.utils.clearCache
|
||||
import example.imageviewer.utils.isInternetAvailable
|
||||
import example.imageviewer.view.showPopUpMessage
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
object ContentState {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var repository: ImageRepository
|
||||
private lateinit var uriRepository: String
|
||||
|
||||
fun applyContent(context: Context, uriRepository: String): ContentState {
|
||||
if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.context = context
|
||||
this.uriRepository = uriRepository
|
||||
repository = ImageRepository(uriRepository)
|
||||
appliedFilters = FiltersManager(context)
|
||||
isContentReady.value = false
|
||||
|
||||
initData()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private val executor: ExecutorService by lazy { Executors.newFixedThreadPool(2) }
|
||||
|
||||
private val handler: Handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
|
||||
fun getContext(): Context {
|
||||
return context
|
||||
}
|
||||
|
||||
fun getOrientation(): Int {
|
||||
return context.resources.configuration.orientation
|
||||
}
|
||||
|
||||
private val isAppReady = mutableStateOf(false)
|
||||
fun isAppReady(): Boolean {
|
||||
return isAppReady.value
|
||||
}
|
||||
|
||||
private val isContentReady = mutableStateOf(false)
|
||||
fun isContentReady(): Boolean {
|
||||
return isContentReady.value
|
||||
}
|
||||
|
||||
fun getString(id: Int): String {
|
||||
return context.getString(id)
|
||||
}
|
||||
|
||||
// drawable content
|
||||
private val mainImage = mutableStateOf(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
|
||||
private val currentImageIndex = mutableStateOf(0)
|
||||
private val miniatures = Miniatures()
|
||||
|
||||
fun getMiniatures(): List<Picture> {
|
||||
return miniatures.getMiniatures()
|
||||
}
|
||||
|
||||
fun getSelectedImage(): Bitmap {
|
||||
return mainImage.value
|
||||
}
|
||||
|
||||
fun getSelectedImageName(): String {
|
||||
return MainImageWrapper.getName()
|
||||
}
|
||||
|
||||
// filters managing
|
||||
private lateinit var appliedFilters: FiltersManager
|
||||
private val filterUIState: MutableMap<FilterType, MutableState<Boolean>> = LinkedHashMap()
|
||||
|
||||
private fun toggleFilterState(filter: FilterType) {
|
||||
|
||||
if (!filterUIState.containsKey(filter)) {
|
||||
filterUIState[filter] = mutableStateOf(true)
|
||||
} else {
|
||||
val value = filterUIState[filter]!!.value
|
||||
filterUIState[filter]!!.value = !value
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFilter(filter: FilterType) {
|
||||
|
||||
if (containsFilter(filter)) {
|
||||
removeFilter(filter)
|
||||
} else {
|
||||
addFilter(filter)
|
||||
}
|
||||
|
||||
toggleFilterState(filter)
|
||||
|
||||
var bitmap = MainImageWrapper.origin
|
||||
|
||||
if (bitmap != null) {
|
||||
bitmap = appliedFilters.applyFilters(bitmap)
|
||||
MainImageWrapper.setImage(bitmap)
|
||||
mainImage.value = bitmap
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFilter(filter: FilterType) {
|
||||
appliedFilters.add(filter)
|
||||
MainImageWrapper.addFilter(filter)
|
||||
}
|
||||
|
||||
private fun removeFilter(filter: FilterType) {
|
||||
appliedFilters.remove(filter)
|
||||
MainImageWrapper.removeFilter(filter)
|
||||
}
|
||||
|
||||
private fun containsFilter(type: FilterType): Boolean {
|
||||
return appliedFilters.contains(type)
|
||||
}
|
||||
|
||||
fun isFilterEnabled(type: FilterType): Boolean {
|
||||
if (!filterUIState.containsKey(type)) {
|
||||
filterUIState[type] = mutableStateOf(false)
|
||||
}
|
||||
return filterUIState[type]!!.value
|
||||
}
|
||||
|
||||
private fun restoreFilters(): Bitmap {
|
||||
filterUIState.clear()
|
||||
appliedFilters.clear()
|
||||
return MainImageWrapper.restore()
|
||||
}
|
||||
|
||||
fun restoreMainImage() {
|
||||
mainImage.value = restoreFilters()
|
||||
}
|
||||
|
||||
// application content initialization
|
||||
private fun initData() {
|
||||
if (isContentReady.value)
|
||||
return
|
||||
|
||||
val directory = context.cacheDir.absolutePath
|
||||
|
||||
executor.execute {
|
||||
try {
|
||||
if (isInternetAvailable()) {
|
||||
val imageList = repository.get()
|
||||
|
||||
if (imageList.isEmpty()) {
|
||||
handler.post {
|
||||
showPopUpMessage(
|
||||
getString(R.string.repo_invalid),
|
||||
context
|
||||
)
|
||||
onContentReady()
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
val pictureList = loadImages(directory, imageList)
|
||||
|
||||
if (pictureList.isEmpty()) {
|
||||
handler.post {
|
||||
showPopUpMessage(
|
||||
getString(R.string.repo_empty),
|
||||
context
|
||||
)
|
||||
onContentReady()
|
||||
}
|
||||
} else {
|
||||
val picture = loadFullImage(imageList[0])
|
||||
|
||||
handler.post {
|
||||
miniatures.setMiniatures(pictureList)
|
||||
|
||||
if (isMainImageEmpty()) {
|
||||
wrapPictureIntoMainImage(picture)
|
||||
} else {
|
||||
appliedFilters.add(MainImageWrapper.getFilters())
|
||||
mainImage.value = MainImageWrapper.getImage()
|
||||
currentImageIndex.value = MainImageWrapper.getId()
|
||||
}
|
||||
onContentReady()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handler.post {
|
||||
showPopUpMessage(
|
||||
getString(R.string.no_internet),
|
||||
context
|
||||
)
|
||||
onContentReady()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preview/fullscreen image managing
|
||||
fun isMainImageEmpty(): Boolean {
|
||||
return MainImageWrapper.isEmpty()
|
||||
}
|
||||
|
||||
fun fullscreen(picture: Picture) {
|
||||
isContentReady.value = false
|
||||
AppState.screenState(ScreenType.FullscreenImage)
|
||||
setMainImage(picture)
|
||||
}
|
||||
|
||||
fun setMainImage(picture: Picture) {
|
||||
if (MainImageWrapper.getId() == picture.id) {
|
||||
if (!isContentReady())
|
||||
onContentReady()
|
||||
return
|
||||
}
|
||||
isContentReady.value = false
|
||||
|
||||
executor.execute {
|
||||
if (isInternetAvailable()) {
|
||||
|
||||
val fullSizePicture = loadFullImage(picture.source)
|
||||
fullSizePicture.id = picture.id
|
||||
|
||||
handler.post {
|
||||
wrapPictureIntoMainImage(fullSizePicture)
|
||||
onContentReady()
|
||||
}
|
||||
} else {
|
||||
handler.post {
|
||||
showPopUpMessage(
|
||||
"${getString(R.string.no_internet)}\n${getString(R.string.load_image_unavailable)}",
|
||||
context
|
||||
)
|
||||
wrapPictureIntoMainImage(picture)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContentReady() {
|
||||
isContentReady.value = true
|
||||
isAppReady.value = true
|
||||
}
|
||||
|
||||
private fun wrapPictureIntoMainImage(picture: Picture) {
|
||||
MainImageWrapper.wrapPicture(picture)
|
||||
MainImageWrapper.saveOrigin()
|
||||
mainImage.value = picture.image
|
||||
currentImageIndex.value = picture.id
|
||||
}
|
||||
|
||||
fun swipeNext() {
|
||||
if (currentImageIndex.value == miniatures.size() - 1) {
|
||||
showPopUpMessage(
|
||||
getString(R.string.last_image),
|
||||
context
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
restoreFilters()
|
||||
setMainImage(miniatures.get(++currentImageIndex.value))
|
||||
}
|
||||
|
||||
fun swipePrevious() {
|
||||
if (currentImageIndex.value == 0) {
|
||||
showPopUpMessage(
|
||||
getString(R.string.first_image),
|
||||
context
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
restoreFilters()
|
||||
setMainImage(miniatures.get(--currentImageIndex.value))
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
executor.execute {
|
||||
if (isInternetAvailable()) {
|
||||
handler.post {
|
||||
clearCache(context)
|
||||
MainImageWrapper.clear()
|
||||
miniatures.clear()
|
||||
isContentReady.value = false
|
||||
initData()
|
||||
}
|
||||
} else {
|
||||
handler.post {
|
||||
showPopUpMessage(
|
||||
"${getString(R.string.no_internet)}\n${getString(R.string.refresh_unavailable)}",
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object MainImageWrapper {
|
||||
// origin image
|
||||
var origin: Bitmap? = null
|
||||
private set
|
||||
|
||||
fun saveOrigin() {
|
||||
origin = copy(picture.value.image)
|
||||
}
|
||||
|
||||
fun restore(): Bitmap {
|
||||
|
||||
if (origin != null) {
|
||||
filtersSet.clear()
|
||||
picture.value.image = copy(origin!!)
|
||||
}
|
||||
|
||||
return copy(picture.value.image)
|
||||
}
|
||||
|
||||
// picture adapter
|
||||
private var picture = mutableStateOf(
|
||||
Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
|
||||
)
|
||||
|
||||
fun wrapPicture(picture: Picture) {
|
||||
this.picture.value = picture
|
||||
}
|
||||
|
||||
fun setImage(bitmap: Bitmap) {
|
||||
picture.value.image = bitmap
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return (picture.value.name == "")
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
picture.value = Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
|
||||
}
|
||||
|
||||
fun getName(): String {
|
||||
return picture.value.name
|
||||
}
|
||||
|
||||
fun getImage(): Bitmap {
|
||||
return picture.value.image
|
||||
}
|
||||
|
||||
fun getId(): Int {
|
||||
return picture.value.id
|
||||
}
|
||||
|
||||
// applied filters
|
||||
private var filtersSet: MutableSet<FilterType> = LinkedHashSet()
|
||||
|
||||
fun addFilter(filter: FilterType) {
|
||||
filtersSet.add(filter)
|
||||
}
|
||||
|
||||
fun removeFilter(filter: FilterType) {
|
||||
filtersSet.remove(filter)
|
||||
}
|
||||
|
||||
fun getFilters(): Set<FilterType> {
|
||||
return filtersSet
|
||||
}
|
||||
|
||||
private fun copy(bitmap: Bitmap): Bitmap {
|
||||
return bitmap.copy(bitmap.config, false)
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package example.imageviewer.model
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import example.imageviewer.utils.cacheImage
|
||||
import example.imageviewer.utils.cacheImagePostfix
|
||||
import example.imageviewer.utils.scaleBitmapAspectRatio
|
||||
import example.imageviewer.utils.toPx
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.BufferedReader
|
||||
import java.lang.Exception
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
fun loadFullImage(source: String): Picture {
|
||||
try {
|
||||
val url = URL(source)
|
||||
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
|
||||
connection.connectTimeout = 5000
|
||||
connection.connect()
|
||||
|
||||
val input: InputStream = connection.inputStream
|
||||
val bitmap: Bitmap? = BitmapFactory.decodeStream(input)
|
||||
if (bitmap != null) {
|
||||
return Picture(
|
||||
source = source,
|
||||
image = bitmap,
|
||||
name = getNameURL(source),
|
||||
width = bitmap.width,
|
||||
height = bitmap.height
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
|
||||
}
|
||||
|
||||
fun loadImages(cachePath: String, list: List<String>): MutableList<Picture> {
|
||||
val result: MutableList<Picture> = ArrayList()
|
||||
|
||||
for (source in list) {
|
||||
val name = getNameURL(source)
|
||||
val path = cachePath + File.separator + name
|
||||
|
||||
if (File(path + "info").exists()) {
|
||||
addCachedMiniature(filePath = path, outList = result)
|
||||
} else {
|
||||
addFreshMiniature(source = source, outList = result, path = cachePath)
|
||||
}
|
||||
|
||||
result.last().id = result.size - 1
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun addFreshMiniature(
|
||||
source: String,
|
||||
outList: MutableList<Picture>,
|
||||
path: String
|
||||
) {
|
||||
try {
|
||||
val url = URL(source)
|
||||
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
|
||||
connection.connectTimeout = 5000
|
||||
connection.connect()
|
||||
|
||||
val input: InputStream = connection.inputStream
|
||||
val result: Bitmap? = BitmapFactory.decodeStream(input)
|
||||
|
||||
if (result != null) {
|
||||
val picture = Picture(
|
||||
source,
|
||||
getNameURL(source),
|
||||
scaleBitmapAspectRatio(result, 200, 164),
|
||||
result.width,
|
||||
result.height
|
||||
)
|
||||
|
||||
outList.add(picture)
|
||||
cacheImage(path + getNameURL(source), picture)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCachedMiniature(
|
||||
filePath: String,
|
||||
outList: MutableList<Picture>
|
||||
) {
|
||||
try {
|
||||
val read = BufferedReader(
|
||||
InputStreamReader(
|
||||
FileInputStream(filePath + cacheImagePostfix),
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
)
|
||||
|
||||
val source = read.readLine()
|
||||
val width = read.readLine().toInt()
|
||||
val height = read.readLine().toInt()
|
||||
|
||||
read.close()
|
||||
|
||||
val result: Bitmap? = BitmapFactory.decodeFile(filePath)
|
||||
|
||||
if (result != null) {
|
||||
val picture = Picture(
|
||||
source,
|
||||
getNameURL(source),
|
||||
result,
|
||||
width,
|
||||
height
|
||||
)
|
||||
outList.add(picture)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNameURL(url: String): String {
|
||||
return url.substring(url.lastIndexOf('/') + 1, url.length)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package example.imageviewer.model
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
actual data class Picture(
|
||||
var source: String = "",
|
||||
var name: String = "",
|
||||
var image: Bitmap,
|
||||
var width: Int = 0,
|
||||
var height: Int = 0,
|
||||
var id: Int = 0
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
import example.imageviewer.utils.applyBlurFilter
|
||||
|
||||
class BlurFilter(private val context: Context) : BitmapFilter {
|
||||
|
||||
override fun apply(bitmap: Bitmap): Bitmap {
|
||||
return applyBlurFilter(bitmap, context)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
|
||||
class EmptyFilter : BitmapFilter {
|
||||
|
||||
override fun apply(bitmap: Bitmap): Bitmap {
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
import example.imageviewer.core.FilterType
|
||||
|
||||
class FiltersManager(private val context: Context) {
|
||||
|
||||
private var filtersMap: MutableMap<FilterType, BitmapFilter> = LinkedHashMap()
|
||||
|
||||
fun clear() {
|
||||
filtersMap = LinkedHashMap()
|
||||
}
|
||||
|
||||
fun add(filters: Collection<FilterType>) {
|
||||
|
||||
for (filter in filters)
|
||||
add(filter)
|
||||
}
|
||||
|
||||
fun add(filter: FilterType) {
|
||||
|
||||
if (!filtersMap.containsKey(filter))
|
||||
filtersMap[filter] = getFilter(filter, context)
|
||||
}
|
||||
|
||||
fun remove(filter: FilterType) {
|
||||
filtersMap.remove(filter)
|
||||
}
|
||||
|
||||
fun contains(filter: FilterType): Boolean {
|
||||
return filtersMap.contains(filter)
|
||||
}
|
||||
|
||||
fun applyFilters(bitmap: Bitmap): Bitmap {
|
||||
|
||||
var result: Bitmap = bitmap
|
||||
for (filter in filtersMap) {
|
||||
result = filter.value.apply(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFilter(type: FilterType, context: Context): BitmapFilter {
|
||||
|
||||
return when (type) {
|
||||
FilterType.GrayScale -> GrayScaleFilter()
|
||||
FilterType.Pixel -> PixelFilter()
|
||||
FilterType.Blur -> BlurFilter(context)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
import example.imageviewer.utils.applyGrayScaleFilter
|
||||
|
||||
class GrayScaleFilter : BitmapFilter {
|
||||
|
||||
override fun apply(bitmap: Bitmap) : Bitmap {
|
||||
return applyGrayScaleFilter(bitmap)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
import example.imageviewer.utils.applyPixelFilter
|
||||
|
||||
class PixelFilter : BitmapFilter {
|
||||
|
||||
override fun apply(bitmap: Bitmap): Bitmap {
|
||||
return applyPixelFilter(bitmap)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package example.imageviewer.style
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import example.imageviewer.common.R
|
||||
|
||||
@Composable
|
||||
fun icEmpty() = painterResource(R.drawable.empty)
|
||||
|
||||
@Composable
|
||||
fun icBack() = painterResource(R.drawable.back)
|
||||
|
||||
@Composable
|
||||
fun icRefresh() = painterResource(R.drawable.refresh)
|
||||
|
||||
@Composable
|
||||
fun icDots() = painterResource(R.drawable.dots)
|
||||
|
||||
@Composable
|
||||
fun icFilterGrayscaleOn() = painterResource(R.drawable.grayscale_on)
|
||||
|
||||
@Composable
|
||||
fun icFilterGrayscaleOff() = painterResource(R.drawable.grayscale_off)
|
||||
|
||||
@Composable
|
||||
fun icFilterPixelOn() = painterResource(R.drawable.pixel_on)
|
||||
|
||||
@Composable
|
||||
fun icFilterPixelOff() = painterResource(R.drawable.pixel_off)
|
||||
|
||||
@Composable
|
||||
fun icFilterBlurOn() = painterResource(R.drawable.blur_on)
|
||||
|
||||
@Composable
|
||||
fun icFilterBlurOff() = painterResource(R.drawable.blur_off)
|
||||
|
||||
@Composable
|
||||
fun icFilterUnknown() = painterResource(R.drawable.filter_unknown)
|
||||
@@ -1,52 +0,0 @@
|
||||
package example.imageviewer.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import example.imageviewer.model.Picture
|
||||
import java.io.File
|
||||
import java.io.BufferedWriter
|
||||
import java.io.OutputStreamWriter
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
val cacheImagePostfix = "info"
|
||||
|
||||
fun cacheImage(path: String, picture: Picture) {
|
||||
try {
|
||||
FileOutputStream(path).use { out ->
|
||||
picture.image.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
}
|
||||
|
||||
val bw =
|
||||
BufferedWriter(
|
||||
OutputStreamWriter(
|
||||
FileOutputStream(path + cacheImagePostfix), StandardCharsets.UTF_8
|
||||
)
|
||||
)
|
||||
|
||||
bw.write(picture.source)
|
||||
bw.write("\r\n${picture.width}")
|
||||
bw.write("\r\n${picture.height}")
|
||||
bw.close()
|
||||
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCache(context: Context) {
|
||||
|
||||
val directory = File(context.cacheDir.absolutePath)
|
||||
|
||||
val files: Array<File>? = directory.listFiles()
|
||||
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (file.isDirectory)
|
||||
continue
|
||||
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package example.imageviewer.utils
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
actual fun <T> runBlocking(
|
||||
context: CoroutineContext,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): T = kotlinx.coroutines.runBlocking(context, block)
|
||||
@@ -1,195 +0,0 @@
|
||||
package example.imageviewer.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.*
|
||||
import android.renderscript.Allocation
|
||||
import android.renderscript.Element
|
||||
import android.renderscript.RenderScript
|
||||
import android.renderscript.ScriptIntrinsicBlur
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import example.imageviewer.view.DragHandler
|
||||
|
||||
fun scaleBitmapAspectRatio(
|
||||
bitmap: Bitmap,
|
||||
width: Int,
|
||||
height: Int,
|
||||
filter: Boolean = false
|
||||
): Bitmap {
|
||||
val boundW: Float = width.toFloat()
|
||||
val boundH: Float = height.toFloat()
|
||||
|
||||
val ratioX: Float = boundW / bitmap.width
|
||||
val ratioY: Float = boundH / bitmap.height
|
||||
val ratio: Float = if (ratioX < ratioY) ratioX else ratioY
|
||||
|
||||
val resultH = (bitmap.height * ratio).toInt()
|
||||
val resultW = (bitmap.width * ratio).toInt()
|
||||
|
||||
return Bitmap.createScaledBitmap(bitmap, resultW, resultH, filter)
|
||||
}
|
||||
|
||||
fun getDisplayBounds(bitmap: Bitmap): Rect {
|
||||
|
||||
val boundW: Float = displayWidth().toFloat()
|
||||
val boundH: Float = displayHeight().toFloat()
|
||||
|
||||
val ratioX: Float = bitmap.width / boundW
|
||||
val ratioY: Float = bitmap.height / boundH
|
||||
val ratio: Float = if (ratioX > ratioY) ratioX else ratioY
|
||||
val resultW = (boundW * ratio)
|
||||
val resultH = (boundH * ratio)
|
||||
|
||||
return Rect(0, 0, resultW.toInt(), resultH.toInt())
|
||||
}
|
||||
|
||||
fun applyGrayScaleFilter(bitmap: Bitmap): Bitmap {
|
||||
|
||||
val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||
|
||||
val canvas = Canvas(result)
|
||||
|
||||
val colorMatrix = ColorMatrix()
|
||||
colorMatrix.setSaturation(0f)
|
||||
|
||||
val paint = Paint()
|
||||
paint.colorFilter = ColorMatrixColorFilter(colorMatrix)
|
||||
|
||||
canvas.drawBitmap(result, 0f, 0f, paint)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun applyPixelFilter(bitmap: Bitmap): Bitmap {
|
||||
|
||||
var result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||
val w: Int = bitmap.width
|
||||
val h: Int = bitmap.height
|
||||
result = scaleBitmapAspectRatio(result, w / 20, h / 20)
|
||||
result = scaleBitmapAspectRatio(result, w, h)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun applyBlurFilter(bitmap: Bitmap, context: Context): Bitmap {
|
||||
|
||||
val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||
|
||||
val renderScript: RenderScript = RenderScript.create(context)
|
||||
|
||||
val tmpIn: Allocation = Allocation.createFromBitmap(renderScript, bitmap)
|
||||
val tmpOut: Allocation = Allocation.createFromBitmap(renderScript, result)
|
||||
|
||||
val theIntrinsic: ScriptIntrinsicBlur =
|
||||
ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
|
||||
|
||||
theIntrinsic.setRadius(25f)
|
||||
theIntrinsic.setInput(tmpIn)
|
||||
theIntrinsic.forEach(tmpOut)
|
||||
|
||||
tmpOut.copyTo(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun adjustImageScale(bitmap: Bitmap): ContentScale {
|
||||
val bitmapRatio = (10 * bitmap.width.toFloat() / bitmap.height).toInt()
|
||||
val displayRatio = (10 * displayWidth().toFloat() / displayHeight()).toInt()
|
||||
|
||||
if (displayRatio > bitmapRatio) {
|
||||
return ContentScale.FillHeight
|
||||
}
|
||||
return ContentScale.FillWidth
|
||||
}
|
||||
|
||||
fun toPx(dp: Int): Int {
|
||||
return (dp * Resources.getSystem().displayMetrics.density).toInt()
|
||||
}
|
||||
|
||||
fun toDp(px: Int): Int {
|
||||
return (px / Resources.getSystem().displayMetrics.density).toInt()
|
||||
}
|
||||
|
||||
fun displayWidth(): Int {
|
||||
return Resources.getSystem().displayMetrics.widthPixels
|
||||
}
|
||||
|
||||
fun displayHeight(): Int {
|
||||
return Resources.getSystem().displayMetrics.heightPixels
|
||||
}
|
||||
|
||||
fun cropBitmapByScale(bitmap: Bitmap, scale: Float, drag: DragHandler): Bitmap {
|
||||
val crop = cropBitmapByBounds(
|
||||
bitmap,
|
||||
getDisplayBounds(bitmap),
|
||||
scale,
|
||||
drag
|
||||
)
|
||||
return Bitmap.createBitmap(
|
||||
bitmap,
|
||||
crop.left,
|
||||
crop.top,
|
||||
crop.right - crop.left,
|
||||
crop.bottom - crop.top
|
||||
)
|
||||
}
|
||||
|
||||
fun cropBitmapByBounds(
|
||||
bitmap: Bitmap,
|
||||
bounds: Rect,
|
||||
scaleFactor: Float,
|
||||
drag: DragHandler
|
||||
): Rect {
|
||||
if (scaleFactor <= 1f)
|
||||
return Rect(0, 0, bitmap.width, bitmap.height)
|
||||
|
||||
var scale = scaleFactor.toDouble().pow(1.4)
|
||||
|
||||
var boundW = (bounds.width() / scale).roundToInt()
|
||||
var boundH = (bounds.height() / scale).roundToInt()
|
||||
|
||||
scale *= displayWidth() / bounds.width().toDouble()
|
||||
|
||||
val offsetX = drag.getAmount().x / scale
|
||||
val offsetY = drag.getAmount().y / scale
|
||||
|
||||
if (boundW > bitmap.width) {
|
||||
boundW = bitmap.width
|
||||
}
|
||||
if (boundH > bitmap.height) {
|
||||
boundH = bitmap.height
|
||||
}
|
||||
|
||||
val invisibleW = bitmap.width - boundW
|
||||
var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt().toFloat()
|
||||
|
||||
if (leftOffset > invisibleW) {
|
||||
leftOffset = invisibleW.toFloat()
|
||||
drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (leftOffset < 0) {
|
||||
drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
leftOffset = 0f
|
||||
}
|
||||
|
||||
val invisibleH = bitmap.height - boundH
|
||||
var topOffset = (invisibleH / 2 - offsetY).roundToInt().toFloat()
|
||||
|
||||
if (topOffset > invisibleH) {
|
||||
topOffset = invisibleH.toFloat()
|
||||
drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (topOffset < 0) {
|
||||
drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
topOffset = 0f
|
||||
}
|
||||
|
||||
return Rect(
|
||||
leftOffset.toInt(),
|
||||
topOffset.toInt(),
|
||||
(leftOffset + boundW).toInt(),
|
||||
(topOffset + boundH).toInt()
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import example.imageviewer.model.AppState
|
||||
import example.imageviewer.model.ScreenType
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.style.Gray
|
||||
|
||||
@Composable
|
||||
fun AppUI(content: ContentState) {
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = Gray
|
||||
) {
|
||||
when (AppState.screenState()) {
|
||||
ScreenType.MainScreen -> {
|
||||
MainScreen(content)
|
||||
}
|
||||
ScreenType.FullscreenImage -> {
|
||||
FullscreenImage(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showPopUpMessage(text: String, context: Context) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
text,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.unit.dp
|
||||
import example.imageviewer.core.FilterType
|
||||
import example.imageviewer.model.AppState
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.model.ScreenType
|
||||
import example.imageviewer.style.DarkGray
|
||||
import example.imageviewer.style.DarkGreen
|
||||
import example.imageviewer.style.Foreground
|
||||
import example.imageviewer.style.MiniatureColor
|
||||
import example.imageviewer.style.Transparent
|
||||
import example.imageviewer.style.icBack
|
||||
import example.imageviewer.style.icFilterBlurOff
|
||||
import example.imageviewer.style.icFilterBlurOn
|
||||
import example.imageviewer.style.icFilterGrayscaleOff
|
||||
import example.imageviewer.style.icFilterGrayscaleOn
|
||||
import example.imageviewer.style.icFilterPixelOff
|
||||
import example.imageviewer.style.icFilterPixelOn
|
||||
import example.imageviewer.utils.adjustImageScale
|
||||
import example.imageviewer.utils.cropBitmapByScale
|
||||
import example.imageviewer.utils.displayWidth
|
||||
import example.imageviewer.utils.getDisplayBounds
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun FullscreenImage(
|
||||
content: ContentState
|
||||
) {
|
||||
Column {
|
||||
ToolBar(content.getSelectedImageName(), content)
|
||||
Image(content)
|
||||
}
|
||||
if (!content.isContentReady()) {
|
||||
LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ToolBar(
|
||||
text: String,
|
||||
content: ContentState
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Surface(color = MiniatureColor, modifier = Modifier.height(44.dp)) {
|
||||
Row(modifier = Modifier.padding(end = 30.dp)) {
|
||||
Surface(
|
||||
color = Transparent,
|
||||
modifier = Modifier.padding(start = 20.dp).align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Clickable(
|
||||
onClick = {
|
||||
if (content.isContentReady()) {
|
||||
content.restoreMainImage()
|
||||
AppState.screenState(ScreenType.MainScreen)
|
||||
}
|
||||
}) {
|
||||
Image(
|
||||
icBack(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(38.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text,
|
||||
color = Foreground,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.padding(start = 30.dp).weight(1f)
|
||||
.align(Alignment.CenterVertically),
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
|
||||
Surface(
|
||||
color = Color(255, 255, 255, 40),
|
||||
modifier = Modifier.size(154.dp, 38.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Row(Modifier.horizontalScroll(scrollState)) {
|
||||
for (type in FilterType.values()) {
|
||||
FilterButton(content, type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FilterButton(
|
||||
content: ContentState,
|
||||
type: FilterType,
|
||||
modifier: Modifier = Modifier.size(38.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.background(color = Transparent).clip(CircleShape)
|
||||
) {
|
||||
Clickable(
|
||||
onClick = { content.toggleFilter(type) }
|
||||
) {
|
||||
Image(
|
||||
getFilterImage(type = type, content = content),
|
||||
contentDescription = null,
|
||||
modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(20.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getFilterImage(type: FilterType, content: ContentState): Painter {
|
||||
return when (type) {
|
||||
FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
|
||||
FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
|
||||
FilterType.Blur -> if (content.isFilterEnabled(type)) icFilterBlurOn() else icFilterBlurOff()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Image(content: ContentState) {
|
||||
val drag = remember { DragHandler() }
|
||||
val scale = remember { ScaleHandler() }
|
||||
|
||||
Surface(
|
||||
color = DarkGray,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Draggable(dragHandler = drag, modifier = Modifier.fillMaxSize()) {
|
||||
Scalable(onScale = scale, modifier = Modifier.fillMaxSize()) {
|
||||
val bitmap = imageByGesture(content, scale, drag)
|
||||
Image(
|
||||
bitmap = bitmap.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
contentScale = adjustImageScale(bitmap)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun imageByGesture(
|
||||
content: ContentState,
|
||||
scale: ScaleHandler,
|
||||
drag: DragHandler
|
||||
): Bitmap {
|
||||
val bitmap = cropBitmapByScale(content.getSelectedImage(), scale.factor.value, drag)
|
||||
|
||||
if (scale.factor.value > 1f)
|
||||
return bitmap
|
||||
|
||||
if (abs(drag.getDistance().x) > displayWidth() / 10) {
|
||||
if (drag.getDistance().x < 0) {
|
||||
content.swipeNext()
|
||||
} else {
|
||||
content.swipePrevious()
|
||||
}
|
||||
drag.cancel()
|
||||
}
|
||||
|
||||
return bitmap
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import example.imageviewer.common.R
|
||||
import example.imageviewer.model.AppState
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.model.Picture
|
||||
import example.imageviewer.model.ScreenType
|
||||
import example.imageviewer.style.DarkGray
|
||||
import example.imageviewer.style.DarkGreen
|
||||
import example.imageviewer.style.Foreground
|
||||
import example.imageviewer.style.LightGray
|
||||
import example.imageviewer.style.MiniatureColor
|
||||
import example.imageviewer.style.Transparent
|
||||
import example.imageviewer.style.icDots
|
||||
import example.imageviewer.style.icEmpty
|
||||
import example.imageviewer.style.icRefresh
|
||||
|
||||
@Composable
|
||||
fun MainScreen(content: ContentState) {
|
||||
Column {
|
||||
TopContent(content)
|
||||
ScrollableArea(content)
|
||||
}
|
||||
if (!content.isContentReady()) {
|
||||
LoadingScreen(content.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopContent(content: ContentState) {
|
||||
TitleBar(text = content.getString(R.string.app_name), content = content)
|
||||
if (content.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
|
||||
PreviewImage(content)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Divider()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TitleBar(text: String, content: ContentState) {
|
||||
TopAppBar(
|
||||
backgroundColor = DarkGreen,
|
||||
title = {
|
||||
Row(Modifier.height(50.dp)) {
|
||||
Text(
|
||||
text,
|
||||
color = Foreground,
|
||||
modifier = Modifier.weight(1f).align(Alignment.CenterVertically)
|
||||
)
|
||||
Surface(
|
||||
color = Transparent,
|
||||
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Clickable(
|
||||
onClick = {
|
||||
if (content.isContentReady()) {
|
||||
content.refresh()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
icRefresh(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(35.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreviewImage(content: ContentState) {
|
||||
Clickable(onClick = {
|
||||
AppState.screenState(ScreenType.FullscreenImage)
|
||||
}) {
|
||||
Card(
|
||||
backgroundColor = DarkGray,
|
||||
modifier = Modifier.height(250.dp),
|
||||
shape = RectangleShape,
|
||||
elevation = 1.dp
|
||||
) {
|
||||
Image(
|
||||
if (content.isMainImageEmpty()) {
|
||||
icEmpty()
|
||||
} else {
|
||||
BitmapPainter(content.getSelectedImage().asImageBitmap())
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Miniature(
|
||||
picture: Picture,
|
||||
content: ContentState
|
||||
) {
|
||||
Card(
|
||||
backgroundColor = MiniatureColor,
|
||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
content.setMainImage(picture)
|
||||
},
|
||||
shape = RectangleShape,
|
||||
elevation = 2.dp
|
||||
) {
|
||||
Row(modifier = Modifier.padding(end = 30.dp)) {
|
||||
Clickable(
|
||||
onClick = {
|
||||
content.fullscreen(picture)
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
picture.image.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(70.dp)
|
||||
.width(90.dp)
|
||||
.padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 1.dp),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = picture.name,
|
||||
color = Foreground,
|
||||
modifier = Modifier.weight(1f).align(Alignment.CenterVertically).padding(start = 16.dp),
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
|
||||
Clickable(
|
||||
modifier = Modifier.height(70.dp)
|
||||
.width(30.dp),
|
||||
onClick = {
|
||||
showPopUpMessage(
|
||||
"${content.getString(R.string.picture)} " +
|
||||
"${picture.name} \n" +
|
||||
"${content.getString(R.string.size)} " +
|
||||
"${picture.width}x${picture.height} " +
|
||||
"${content.getString(R.string.pixels)}",
|
||||
content.getContext()
|
||||
)
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
icDots(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(70.dp)
|
||||
.width(30.dp)
|
||||
.padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp),
|
||||
contentScale = ContentScale.FillHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScrollableArea(content: ContentState) {
|
||||
var index = 1
|
||||
val scrollState = rememberScrollState()
|
||||
Column(Modifier.verticalScroll(scrollState)) {
|
||||
for (picture in content.getMiniatures()) {
|
||||
Miniature(
|
||||
picture = picture,
|
||||
content = content
|
||||
)
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Divider() {
|
||||
Divider(
|
||||
color = LightGray,
|
||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp)
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,18 +0,0 @@
|
||||
package example.imageviewer.core
|
||||
|
||||
class EventLocker {
|
||||
|
||||
private var value: Boolean = false
|
||||
|
||||
fun lock() {
|
||||
value = false
|
||||
}
|
||||
|
||||
fun unlock() {
|
||||
value = true
|
||||
}
|
||||
|
||||
fun isLocked(): Boolean {
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package example.imageviewer.core
|
||||
|
||||
interface Repository<T> {
|
||||
fun get() : T
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// READ ME FIRST!
|
||||
//
|
||||
// Code in this file is shared between the Android and Desktop JVM targets.
|
||||
// Kotlin's hierarchical multiplatform projects currently
|
||||
// don't support sharing code depending on JVM declarations.
|
||||
//
|
||||
// You can follow the progress for HMPP JVM & Android intermediate source sets here:
|
||||
// https://youtrack.jetbrains.com/issue/KT-42466
|
||||
//
|
||||
// The workaround used here to access JVM libraries causes IntelliJ IDEA to not
|
||||
// resolve symbols in this file properly.
|
||||
//
|
||||
// Resolution errors in your IDE do not indicate a problem with your setup.
|
||||
|
||||
|
||||
package example.imageviewer.model
|
||||
|
||||
import example.imageviewer.core.Repository
|
||||
import example.imageviewer.utils.ktorHttpClient
|
||||
import example.imageviewer.utils.runBlocking
|
||||
import io.ktor.client.request.*
|
||||
|
||||
class ImageRepository(
|
||||
private val httpsURL: String
|
||||
) : Repository<MutableList<String>> {
|
||||
|
||||
override fun get(): MutableList<String> {
|
||||
return runBlocking {
|
||||
val content = ktorHttpClient.get<String>(httpsURL)
|
||||
content.lines().toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// READ ME FIRST!
|
||||
//
|
||||
// Code in this file is shared between the Android and Desktop JVM targets.
|
||||
// Kotlin's hierarchical multiplatform projects currently
|
||||
// don't support sharing code depending on JVM declarations.
|
||||
//
|
||||
// You can follow the progress for HMPP JVM & Android intermediate source sets here:
|
||||
// https://youtrack.jetbrains.com/issue/KT-42466
|
||||
//
|
||||
// The workaround used here to access JVM libraries causes IntelliJ IDEA to not
|
||||
// resolve symbols in this file properly.
|
||||
//
|
||||
// Resolution errors in your IDE do not indicate a problem with your setup.
|
||||
|
||||
package example.imageviewer.model
|
||||
|
||||
expect class Picture
|
||||
|
||||
class Miniatures(
|
||||
private var list: List<Picture> = emptyList()
|
||||
) {
|
||||
fun get(index: Int): Picture {
|
||||
return list[index]
|
||||
}
|
||||
|
||||
fun getMiniatures(): List<Picture> {
|
||||
return list.toList()
|
||||
}
|
||||
|
||||
fun setMiniatures(list: List<Picture>) {
|
||||
this.list = list.toList()
|
||||
}
|
||||
|
||||
fun size(): Int {
|
||||
return list.size
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
list = emptyList()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package example.imageviewer.model
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
enum class ScreenType {
|
||||
MainScreen, FullscreenImage
|
||||
}
|
||||
|
||||
object AppState {
|
||||
private var screen: MutableState<ScreenType>
|
||||
init {
|
||||
screen = mutableStateOf(ScreenType.MainScreen)
|
||||
}
|
||||
|
||||
fun screenState() : ScreenType {
|
||||
return screen.value
|
||||
}
|
||||
|
||||
fun screenState(state: ScreenType) {
|
||||
screen.value = state
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package example.imageviewer.style
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val DarkGreen = Color(16, 139, 102)
|
||||
val Gray = Color.DarkGray
|
||||
val LightGray = Color(100, 100, 100)
|
||||
val DarkGray = Color(32, 32, 32)
|
||||
val PreviewImageAreaHoverColor = Color(45, 45, 45)
|
||||
val ToastBackground = Color(23, 23, 23)
|
||||
val MiniatureColor = Color(50, 50, 50)
|
||||
val MiniatureHoverColor = Color(55, 55, 55)
|
||||
val Foreground = Color(210, 210, 210)
|
||||
val TranslucentBlack = Color(0, 0, 0, 60)
|
||||
val TranslucentWhite = Color(255, 255, 255, 20)
|
||||
val Transparent = Color.Transparent
|
||||
@@ -1,7 +0,0 @@
|
||||
package example.imageviewer.utils
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
expect fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
|
||||
@@ -1,37 +0,0 @@
|
||||
// READ ME FIRST!
|
||||
//
|
||||
// Code in this file is shared between the Android and Desktop JVM targets.
|
||||
// Kotlin's hierarchical multiplatform projects currently
|
||||
// don't support sharing code depending on JVM declarations.
|
||||
//
|
||||
// You can follow the progress for HMPP JVM & Android intermediate source sets here:
|
||||
// https://youtrack.jetbrains.com/issue/KT-42466
|
||||
//
|
||||
// The workaround used here to access JVM libraries causes IntelliJ IDEA to not
|
||||
// resolve symbols in this file properly.
|
||||
//
|
||||
// Resolution errors in your IDE do not indicate a problem with your setup.
|
||||
|
||||
package example.imageviewer.utils
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
//import java.net.InetAddress
|
||||
|
||||
fun isInternetAvailable(): Boolean {
|
||||
return runBlocking {
|
||||
try {
|
||||
ktorHttpClient.head<String>("http://google.com")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ktorHttpClient = HttpClient {}
|
||||
@@ -1,21 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun Clickable(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
children: @Composable () -> Unit = { }
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.clickable {
|
||||
onClick?.invoke()
|
||||
}
|
||||
) {
|
||||
children()
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import example.imageviewer.core.EventLocker
|
||||
import example.imageviewer.style.Transparent
|
||||
|
||||
@Composable
|
||||
fun Draggable(
|
||||
dragHandler: DragHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
onUpdate: (() -> Unit)? = null,
|
||||
children: @Composable() () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
color = Transparent,
|
||||
modifier = modifier.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
onDragStart = { dragHandler.reset() },
|
||||
onDragEnd = { dragHandler.reset() },
|
||||
onDragCancel = { dragHandler.cancel() },
|
||||
) { change, dragAmount ->
|
||||
dragHandler.drag(dragAmount)
|
||||
onUpdate?.invoke()
|
||||
change.consume()
|
||||
}
|
||||
}
|
||||
) {
|
||||
children()
|
||||
}
|
||||
}
|
||||
|
||||
class DragHandler {
|
||||
|
||||
private val amount = mutableStateOf(Point(0f, 0f))
|
||||
private val distance = mutableStateOf(Point(0f, 0f))
|
||||
private val locker: EventLocker = EventLocker()
|
||||
|
||||
fun getAmount(): Point {
|
||||
return amount.value
|
||||
}
|
||||
|
||||
fun getDistance(): Point {
|
||||
return distance.value
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
distance.value = Point(Offset.Zero)
|
||||
locker.unlock()
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
distance.value = Point(Offset.Zero)
|
||||
locker.lock()
|
||||
}
|
||||
|
||||
fun drag(dragDistance: Offset) {
|
||||
if (locker.isLocked()) {
|
||||
val dx = dragDistance.x
|
||||
val dy = dragDistance.y
|
||||
|
||||
distance.value = Point(distance.value.x + dx, distance.value.y + dy)
|
||||
amount.value = Point(amount.value.x + dx, amount.value.y + dy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Point {
|
||||
var x: Float = 0f
|
||||
var y: Float = 0f
|
||||
constructor(x: Float, y: Float) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
constructor(point: Offset) {
|
||||
this.x = point.x
|
||||
this.y = point.y
|
||||
}
|
||||
fun setAttr(x: Float, y: Float) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import example.imageviewer.style.Transparent
|
||||
|
||||
@Composable
|
||||
fun Scalable(
|
||||
onScale: ScaleHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
children: @Composable() () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
color = Transparent,
|
||||
modifier = modifier.pointerInput(Unit) {
|
||||
detectTapGestures(onDoubleTap = { onScale.reset() })
|
||||
detectTransformGestures { _, _, zoom, _ ->
|
||||
onScale.onScale(zoom)
|
||||
}
|
||||
},
|
||||
) {
|
||||
children()
|
||||
}
|
||||
}
|
||||
|
||||
class ScaleHandler(private val maxFactor: Float = 5f, private val minFactor: Float = 1f) {
|
||||
val factor = mutableStateOf(1f)
|
||||
|
||||
fun reset() {
|
||||
if (factor.value > minFactor)
|
||||
factor.value = minFactor
|
||||
}
|
||||
|
||||
fun onScale(scaleFactor: Float): Float {
|
||||
factor.value += scaleFactor - 1f
|
||||
|
||||
if (maxFactor < factor.value) factor.value = maxFactor
|
||||
if (minFactor > factor.value) factor.value = minFactor
|
||||
|
||||
return scaleFactor
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import example.imageviewer.style.DarkGray
|
||||
|
||||
@Composable
|
||||
fun SplashUI() {
|
||||
Box(Modifier.fillMaxSize().background(DarkGray)) {
|
||||
Text(
|
||||
// TODO implement common resources
|
||||
"Image Viewer",
|
||||
Modifier.align(Alignment.Center),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 100.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package example.imageviewer.core
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
interface BitmapFilter {
|
||||
fun apply(bitmap: BufferedImage) : BufferedImage
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
package example.imageviewer.model
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import example.imageviewer.ResString
|
||||
import example.imageviewer.core.FilterType
|
||||
import example.imageviewer.model.filtration.FiltersManager
|
||||
import example.imageviewer.utils.cacheImagePath
|
||||
import example.imageviewer.utils.clearCache
|
||||
import example.imageviewer.utils.isInternetAvailable
|
||||
import example.imageviewer.view.showPopUpMessage
|
||||
import example.imageviewer.view.DragHandler
|
||||
import example.imageviewer.view.ScaleHandler
|
||||
import example.imageviewer.utils.cropBitmapByScale
|
||||
import example.imageviewer.utils.toByteArray
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.skia.Image
|
||||
|
||||
object ContentState {
|
||||
val drag = DragHandler()
|
||||
val scale = ScaleHandler()
|
||||
lateinit var windowState: WindowState
|
||||
private lateinit var repository: ImageRepository
|
||||
private lateinit var uriRepository: String
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
fun applyContent(state: WindowState, uriRepository: String): ContentState {
|
||||
windowState = state
|
||||
if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) {
|
||||
return this
|
||||
}
|
||||
this.uriRepository = uriRepository
|
||||
repository = ImageRepository(uriRepository)
|
||||
isContentReady.value = false
|
||||
|
||||
initData()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private val isAppReady = mutableStateOf(false)
|
||||
fun isAppReady(): Boolean {
|
||||
return isAppReady.value
|
||||
}
|
||||
|
||||
private val isContentReady = mutableStateOf(false)
|
||||
fun isContentReady(): Boolean {
|
||||
return isContentReady.value
|
||||
}
|
||||
|
||||
// drawable content
|
||||
private val mainImage = mutableStateOf(BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
|
||||
private val currentImageIndex = mutableStateOf(0)
|
||||
private val miniatures = Miniatures()
|
||||
|
||||
fun getMiniatures(): List<Picture> {
|
||||
return miniatures.getMiniatures()
|
||||
}
|
||||
|
||||
fun getSelectedImage(): ImageBitmap {
|
||||
return MainImageWrapper.mainImageAsImageBitmap.value
|
||||
}
|
||||
|
||||
fun getSelectedImageName(): String {
|
||||
return MainImageWrapper.getName()
|
||||
}
|
||||
|
||||
// filters managing
|
||||
private val appliedFilters = FiltersManager()
|
||||
private val filterUIState: MutableMap<FilterType, MutableState<Boolean>> = LinkedHashMap()
|
||||
|
||||
private fun toggleFilterState(filter: FilterType) {
|
||||
if (!filterUIState.containsKey(filter)) {
|
||||
filterUIState[filter] = mutableStateOf(true)
|
||||
} else {
|
||||
val value = filterUIState[filter]!!.value
|
||||
filterUIState[filter]!!.value = !value
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFilter(filter: FilterType) {
|
||||
if (containsFilter(filter)) {
|
||||
removeFilter(filter)
|
||||
} else {
|
||||
addFilter(filter)
|
||||
}
|
||||
|
||||
toggleFilterState(filter)
|
||||
|
||||
var bitmap = MainImageWrapper.origin
|
||||
|
||||
if (bitmap != null) {
|
||||
bitmap = appliedFilters.applyFilters(bitmap)
|
||||
MainImageWrapper.setImage(bitmap)
|
||||
mainImage.value = bitmap
|
||||
updateMainImage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFilter(filter: FilterType) {
|
||||
appliedFilters.add(filter)
|
||||
MainImageWrapper.addFilter(filter)
|
||||
}
|
||||
|
||||
private fun removeFilter(filter: FilterType) {
|
||||
appliedFilters.remove(filter)
|
||||
MainImageWrapper.removeFilter(filter)
|
||||
}
|
||||
|
||||
private fun containsFilter(type: FilterType): Boolean {
|
||||
return appliedFilters.contains(type)
|
||||
}
|
||||
|
||||
fun isFilterEnabled(type: FilterType): Boolean {
|
||||
if (!filterUIState.containsKey(type)) {
|
||||
filterUIState[type] = mutableStateOf(false)
|
||||
}
|
||||
return filterUIState[type]!!.value
|
||||
}
|
||||
|
||||
private fun restoreFilters(): BufferedImage {
|
||||
filterUIState.clear()
|
||||
appliedFilters.clear()
|
||||
return MainImageWrapper.restore()
|
||||
}
|
||||
|
||||
fun restoreMainImage() {
|
||||
mainImage.value = restoreFilters()
|
||||
}
|
||||
|
||||
// application content initialization
|
||||
private fun initData() {
|
||||
if (isContentReady.value)
|
||||
return
|
||||
|
||||
val directory = File(cacheImagePath)
|
||||
if (!directory.exists()) {
|
||||
directory.mkdir()
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (isInternetAvailable()) {
|
||||
val imageList = repository.get()
|
||||
|
||||
if (imageList.isEmpty()) {
|
||||
showPopUpMessage(
|
||||
ResString.repoInvalid
|
||||
)
|
||||
onContentReady()
|
||||
} else {
|
||||
val pictureList = loadImages(cacheImagePath, imageList)
|
||||
|
||||
if (pictureList.isEmpty()) {
|
||||
showPopUpMessage(
|
||||
ResString.repoEmpty
|
||||
)
|
||||
onContentReady()
|
||||
} else {
|
||||
val picture = loadFullImage(imageList[0])
|
||||
miniatures.setMiniatures(pictureList)
|
||||
if (isMainImageEmpty()) {
|
||||
wrapPictureIntoMainImage(picture)
|
||||
} else {
|
||||
appliedFilters.add(MainImageWrapper.getFilters())
|
||||
currentImageIndex.value = MainImageWrapper.getId()
|
||||
}
|
||||
onContentReady()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showPopUpMessage(
|
||||
ResString.noInternet
|
||||
)
|
||||
onContentReady()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preview/fullscreen image managing
|
||||
fun isMainImageEmpty(): Boolean {
|
||||
return MainImageWrapper.isEmpty()
|
||||
}
|
||||
|
||||
fun fullscreen(picture: Picture) {
|
||||
isContentReady.value = false
|
||||
AppState.screenState(ScreenType.FullscreenImage)
|
||||
setMainImage(picture)
|
||||
}
|
||||
|
||||
fun setMainImage(picture: Picture) {
|
||||
if (MainImageWrapper.getId() == picture.id) {
|
||||
if (!isContentReady()) {
|
||||
onContentReady()
|
||||
}
|
||||
return
|
||||
}
|
||||
isContentReady.value = false
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
scale.reset()
|
||||
if (isInternetAvailable()) {
|
||||
val fullSizePicture = loadFullImage(picture.source)
|
||||
fullSizePicture.id = picture.id
|
||||
wrapPictureIntoMainImage(fullSizePicture)
|
||||
} else {
|
||||
showPopUpMessage(
|
||||
"${ResString.noInternet}\n${ResString.loadImageUnavailable}"
|
||||
)
|
||||
wrapPictureIntoMainImage(picture)
|
||||
}
|
||||
onContentReady()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContentReady() {
|
||||
isContentReady.value = true
|
||||
isAppReady.value = true
|
||||
}
|
||||
|
||||
private fun wrapPictureIntoMainImage(picture: Picture) {
|
||||
MainImageWrapper.wrapPicture(picture)
|
||||
MainImageWrapper.saveOrigin()
|
||||
mainImage.value = picture.image
|
||||
currentImageIndex.value = picture.id
|
||||
updateMainImage()
|
||||
}
|
||||
|
||||
fun updateMainImage() {
|
||||
MainImageWrapper.mainImageAsImageBitmap.value = Image.makeFromEncoded(
|
||||
toByteArray(
|
||||
cropBitmapByScale(
|
||||
mainImage.value,
|
||||
windowState.size,
|
||||
scale.factor.value,
|
||||
drag
|
||||
)
|
||||
)
|
||||
).toComposeImageBitmap()
|
||||
}
|
||||
|
||||
fun swipeNext() {
|
||||
if (currentImageIndex.value == miniatures.size() - 1) {
|
||||
showPopUpMessage(ResString.lastImage)
|
||||
return
|
||||
}
|
||||
|
||||
restoreFilters()
|
||||
setMainImage(miniatures.get(++currentImageIndex.value))
|
||||
}
|
||||
|
||||
fun swipePrevious() {
|
||||
if (currentImageIndex.value == 0) {
|
||||
showPopUpMessage(ResString.firstImage)
|
||||
return
|
||||
}
|
||||
|
||||
restoreFilters()
|
||||
setMainImage(miniatures.get(--currentImageIndex.value))
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (isInternetAvailable()) {
|
||||
clearCache()
|
||||
MainImageWrapper.clear()
|
||||
miniatures.clear()
|
||||
isContentReady.value = false
|
||||
initData()
|
||||
} else {
|
||||
showPopUpMessage(
|
||||
"${ResString.noInternet}\n${ResString.refreshUnavailable}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object MainImageWrapper {
|
||||
// origin image
|
||||
var origin: BufferedImage? = null
|
||||
private set
|
||||
|
||||
fun saveOrigin() {
|
||||
origin = copy(picture.value.image)
|
||||
}
|
||||
|
||||
fun restore(): BufferedImage {
|
||||
if (origin != null) {
|
||||
picture.value.image = copy(origin!!)
|
||||
filtersSet.clear()
|
||||
}
|
||||
return copy(picture.value.image)
|
||||
}
|
||||
|
||||
var mainImageAsImageBitmap = mutableStateOf(ImageBitmap(1, 1))
|
||||
|
||||
// picture adapter
|
||||
private var picture = mutableStateOf(
|
||||
Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
|
||||
)
|
||||
|
||||
fun wrapPicture(picture: Picture) {
|
||||
this.picture.value = picture
|
||||
}
|
||||
|
||||
fun setImage(bitmap: BufferedImage) {
|
||||
picture.value.image = bitmap
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return (picture.value.name == "")
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
picture.value = Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
|
||||
}
|
||||
|
||||
fun getName(): String {
|
||||
return picture.value.name
|
||||
}
|
||||
|
||||
fun getImage(): BufferedImage {
|
||||
return picture.value.image
|
||||
}
|
||||
|
||||
fun getId(): Int {
|
||||
return picture.value.id
|
||||
}
|
||||
|
||||
// applied filters
|
||||
private var filtersSet: MutableSet<FilterType> = LinkedHashSet()
|
||||
|
||||
fun addFilter(filter: FilterType) {
|
||||
filtersSet.add(filter)
|
||||
}
|
||||
|
||||
fun removeFilter(filter: FilterType) {
|
||||
filtersSet.remove(filter)
|
||||
}
|
||||
|
||||
fun getFilters(): Set<FilterType> {
|
||||
return filtersSet
|
||||
}
|
||||
|
||||
private fun copy(bitmap: BufferedImage) : BufferedImage {
|
||||
val result = BufferedImage(bitmap.width, bitmap.height, bitmap.type)
|
||||
val graphics = result.createGraphics()
|
||||
graphics.drawImage(bitmap, 0, 0, result.width, result.height, null)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package example.imageviewer.model
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import example.imageviewer.utils.cacheImage
|
||||
import example.imageviewer.utils.cacheImagePostfix
|
||||
import example.imageviewer.utils.scaleBitmapAspectRatio
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.BufferedReader
|
||||
import javax.imageio.ImageIO
|
||||
import java.lang.Exception
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
fun loadFullImage(source: String): Picture {
|
||||
try {
|
||||
val url = URL(source)
|
||||
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
|
||||
connection.connectTimeout = 5000
|
||||
connection.connect()
|
||||
|
||||
val input: InputStream = connection.inputStream
|
||||
val bitmap: BufferedImage? = ImageIO.read(input)
|
||||
if (bitmap != null) {
|
||||
return Picture(
|
||||
source = source,
|
||||
image = bitmap,
|
||||
name = getNameURL(source),
|
||||
width = bitmap.width,
|
||||
height = bitmap.height
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
|
||||
}
|
||||
|
||||
fun loadImages(cachePath: String, list: List<String>): MutableList<Picture> {
|
||||
val result: MutableList<Picture> = ArrayList()
|
||||
|
||||
for (source in list) {
|
||||
val name = getNameURL(source)
|
||||
val path = cachePath + File.separator + name
|
||||
|
||||
if (File(path + "info").exists()) {
|
||||
addCachedMiniature(filePath = path, outList = result)
|
||||
} else {
|
||||
addFreshMiniature(source = source, outList = result, path = cachePath)
|
||||
}
|
||||
|
||||
result.last().id = result.size - 1
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun addFreshMiniature(
|
||||
source: String,
|
||||
outList: MutableList<Picture>,
|
||||
path: String
|
||||
) {
|
||||
try {
|
||||
val url = URL(source)
|
||||
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
|
||||
connection.connectTimeout = 5000
|
||||
connection.connect()
|
||||
|
||||
val input: InputStream = connection.inputStream
|
||||
val result: BufferedImage? = ImageIO.read(input)
|
||||
|
||||
if (result != null) {
|
||||
val picture = Picture(
|
||||
source,
|
||||
getNameURL(source),
|
||||
scaleBitmapAspectRatio(result, 200, 164),
|
||||
result.width,
|
||||
result.height
|
||||
)
|
||||
|
||||
outList.add(picture)
|
||||
cacheImage(path + getNameURL(source), picture)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCachedMiniature(
|
||||
filePath: String,
|
||||
outList: MutableList<Picture>
|
||||
) {
|
||||
try {
|
||||
val read = BufferedReader(
|
||||
InputStreamReader(
|
||||
FileInputStream(filePath + cacheImagePostfix),
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
)
|
||||
|
||||
val source = read.readLine()
|
||||
val width = read.readLine().toInt()
|
||||
val height = read.readLine().toInt()
|
||||
|
||||
read.close()
|
||||
|
||||
val result: BufferedImage? = ImageIO.read(File(filePath))
|
||||
|
||||
if (result != null) {
|
||||
val picture = Picture(
|
||||
source,
|
||||
getNameURL(source),
|
||||
result,
|
||||
width,
|
||||
height
|
||||
)
|
||||
outList.add(picture)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNameURL(url: String): String {
|
||||
return url.substring(url.lastIndexOf('/') + 1, url.length)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package example.imageviewer.model
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
actual data class Picture(
|
||||
var source: String = "",
|
||||
var name: String = "",
|
||||
var image: BufferedImage,
|
||||
var width: Int = 0,
|
||||
var height: Int = 0,
|
||||
var id: Int = 0
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
import example.imageviewer.utils.applyBlurFilter
|
||||
|
||||
class BlurFilter : BitmapFilter {
|
||||
|
||||
override fun apply(bitmap: BufferedImage): BufferedImage {
|
||||
return applyBlurFilter(bitmap)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
|
||||
class EmptyFilter : BitmapFilter {
|
||||
|
||||
override fun apply(bitmap: BufferedImage): BufferedImage {
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
import example.imageviewer.core.FilterType
|
||||
|
||||
class FiltersManager {
|
||||
|
||||
private var filtersMap: MutableMap<FilterType, BitmapFilter> = LinkedHashMap()
|
||||
|
||||
fun clear() {
|
||||
filtersMap = LinkedHashMap()
|
||||
}
|
||||
|
||||
fun add(filters: Collection<FilterType>) {
|
||||
|
||||
for (filter in filters)
|
||||
add(filter)
|
||||
}
|
||||
|
||||
fun add(filter: FilterType) {
|
||||
|
||||
if (!filtersMap.containsKey(filter))
|
||||
filtersMap[filter] = getFilter(filter)
|
||||
}
|
||||
|
||||
fun remove(filter: FilterType) {
|
||||
filtersMap.remove(filter)
|
||||
}
|
||||
|
||||
fun contains(filter: FilterType): Boolean {
|
||||
return filtersMap.contains(filter)
|
||||
}
|
||||
|
||||
fun applyFilters(bitmap: BufferedImage): BufferedImage {
|
||||
|
||||
var result: BufferedImage = bitmap
|
||||
for (filter in filtersMap) {
|
||||
result = filter.value.apply(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFilter(type: FilterType): BitmapFilter {
|
||||
|
||||
return when (type) {
|
||||
FilterType.GrayScale -> GrayScaleFilter()
|
||||
FilterType.Pixel -> PixelFilter()
|
||||
FilterType.Blur -> BlurFilter()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
import example.imageviewer.utils.applyGrayScaleFilter
|
||||
|
||||
class GrayScaleFilter : BitmapFilter {
|
||||
|
||||
override fun apply(bitmap: BufferedImage) : BufferedImage {
|
||||
return applyGrayScaleFilter(bitmap)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package example.imageviewer.model.filtration
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import example.imageviewer.core.BitmapFilter
|
||||
import example.imageviewer.utils.applyPixelFilter
|
||||
|
||||
class PixelFilter : BitmapFilter {
|
||||
|
||||
override fun apply(bitmap: BufferedImage): BufferedImage {
|
||||
return applyPixelFilter(bitmap)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package example.imageviewer.style
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import java.awt.image.BufferedImage
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
@Composable
|
||||
fun icEmpty() = painterResource("images/empty.png")
|
||||
|
||||
@Composable
|
||||
fun icBack() = painterResource("images/back.png")
|
||||
|
||||
@Composable
|
||||
fun icRefresh() = painterResource("images/refresh.png")
|
||||
|
||||
@Composable
|
||||
fun icDots() = painterResource("images/dots.png")
|
||||
|
||||
@Composable
|
||||
fun icFilterGrayscaleOn() = painterResource("images/grayscale_on.png")
|
||||
|
||||
@Composable
|
||||
fun icFilterGrayscaleOff() = painterResource("images/grayscale_off.png")
|
||||
|
||||
@Composable
|
||||
fun icFilterPixelOn() = painterResource("images/pixel_on.png")
|
||||
|
||||
@Composable
|
||||
fun icFilterPixelOff() = painterResource("images/pixel_off.png")
|
||||
|
||||
@Composable
|
||||
fun icFilterBlurOn() = painterResource("images/blur_on.png")
|
||||
|
||||
@Composable
|
||||
fun icFilterBlurOff() = painterResource("images/blur_off.png")
|
||||
|
||||
@Composable
|
||||
fun icFilterUnknown() = painterResource("images/filter_unknown.png")
|
||||
|
||||
@Composable
|
||||
fun icAppRounded() = painterResource("images/ic_imageviewer_round.png")
|
||||
@@ -1,53 +0,0 @@
|
||||
package example.imageviewer.utils
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import example.imageviewer.model.Picture
|
||||
import javax.imageio.ImageIO
|
||||
import java.io.File
|
||||
import java.io.BufferedWriter
|
||||
import java.io.OutputStreamWriter
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
val cacheImagePostfix = "info"
|
||||
val cacheImagePath = System.getProperty("user.home")!! +
|
||||
File.separator + "Pictures/imageviewer" + File.separator
|
||||
|
||||
fun cacheImage(path: String, picture: Picture) {
|
||||
try {
|
||||
ImageIO.write(picture.image, "png", File(path))
|
||||
|
||||
val bw =
|
||||
BufferedWriter(
|
||||
OutputStreamWriter(
|
||||
FileOutputStream(path + cacheImagePostfix),
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
)
|
||||
|
||||
bw.write(picture.source)
|
||||
bw.write("\r\n${picture.width}")
|
||||
bw.write("\r\n${picture.height}")
|
||||
bw.close()
|
||||
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
|
||||
val directory = File(cacheImagePath)
|
||||
|
||||
val files: Array<File>? = directory.listFiles()
|
||||
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (file.isDirectory)
|
||||
continue
|
||||
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package example.imageviewer.utils
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
actual fun <T> runBlocking(
|
||||
context: CoroutineContext,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): T = kotlinx.coroutines.runBlocking(context, block)
|
||||
@@ -1,206 +0,0 @@
|
||||
package example.imageviewer.utils
|
||||
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.awt.Dimension
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.Rectangle
|
||||
import java.awt.Toolkit
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import javax.imageio.ImageIO
|
||||
import java.awt.image.BufferedImageOp
|
||||
import java.awt.image.ConvolveOp
|
||||
import java.awt.image.Kernel
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import example.imageviewer.view.DragHandler
|
||||
|
||||
fun scaleBitmapAspectRatio(
|
||||
bitmap: BufferedImage,
|
||||
width: Int,
|
||||
height: Int
|
||||
): BufferedImage {
|
||||
val boundW: Float = width.toFloat()
|
||||
val boundH: Float = height.toFloat()
|
||||
|
||||
val ratioX: Float = boundW / bitmap.width
|
||||
val ratioY: Float = boundH / bitmap.height
|
||||
val ratio: Float = if (ratioX < ratioY) ratioX else ratioY
|
||||
|
||||
val resultH = (bitmap.height * ratio).toInt()
|
||||
val resultW = (bitmap.width * ratio).toInt()
|
||||
|
||||
val result = BufferedImage(resultW, resultH, BufferedImage.TYPE_INT_ARGB)
|
||||
val graphics = result.createGraphics()
|
||||
graphics.drawImage(bitmap, 0, 0, resultW, resultH, null)
|
||||
graphics.dispose()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun getDisplayBounds(bitmap: BufferedImage, windowSize: DpSize): Rectangle {
|
||||
|
||||
val boundW: Float = windowSize.width.value.toFloat()
|
||||
val boundH: Float = windowSize.height.value.toFloat()
|
||||
|
||||
val ratioX: Float = bitmap.width / boundW
|
||||
val ratioY: Float = bitmap.height / boundH
|
||||
|
||||
val ratio: Float = if (ratioX > ratioY) ratioX else ratioY
|
||||
|
||||
val resultW = (boundW * ratio)
|
||||
val resultH = (boundH * ratio)
|
||||
|
||||
return Rectangle(0, 0, resultW.toInt(), resultH.toInt())
|
||||
}
|
||||
|
||||
fun applyGrayScaleFilter(bitmap: BufferedImage): BufferedImage {
|
||||
|
||||
val result = BufferedImage(
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight(),
|
||||
BufferedImage.TYPE_BYTE_GRAY)
|
||||
|
||||
val graphics = result.getGraphics()
|
||||
graphics.drawImage(bitmap, 0, 0, null)
|
||||
graphics.dispose()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun applyPixelFilter(bitmap: BufferedImage): BufferedImage {
|
||||
|
||||
val w: Int = bitmap.width
|
||||
val h: Int = bitmap.height
|
||||
|
||||
var result = scaleBitmapAspectRatio(bitmap, w / 20, h / 20)
|
||||
result = scaleBitmapAspectRatio(result, w, h)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun applyBlurFilter(bitmap: BufferedImage): BufferedImage {
|
||||
|
||||
var result = BufferedImage(bitmap.getWidth(), bitmap.getHeight(), bitmap.type)
|
||||
|
||||
val graphics = result.getGraphics()
|
||||
graphics.drawImage(bitmap, 0, 0, null)
|
||||
graphics.dispose()
|
||||
|
||||
val radius = 11
|
||||
val size = 11
|
||||
val weight: Float = 1.0f / (size * size)
|
||||
val matrix = FloatArray(size * size)
|
||||
|
||||
for (i in 0..matrix.size - 1) {
|
||||
matrix[i] = weight
|
||||
}
|
||||
|
||||
val kernel = Kernel(radius, size, matrix)
|
||||
val op = ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null)
|
||||
result = op.filter(result, null)
|
||||
|
||||
return result.getSubimage(
|
||||
radius,
|
||||
radius,
|
||||
result.width - radius * 2,
|
||||
result.height - radius * 2
|
||||
)
|
||||
}
|
||||
|
||||
fun toByteArray(bitmap: BufferedImage) : ByteArray {
|
||||
val baos = ByteArrayOutputStream()
|
||||
ImageIO.write(bitmap, "png", baos)
|
||||
return baos.toByteArray()
|
||||
}
|
||||
|
||||
fun cropImage(bitmap: BufferedImage, crop: Rectangle) : BufferedImage {
|
||||
return bitmap.getSubimage(crop.x, crop.y, crop.width, crop.height)
|
||||
}
|
||||
|
||||
fun cropBitmapByScale(
|
||||
bitmap: BufferedImage,
|
||||
size: DpSize,
|
||||
scale: Float,
|
||||
drag: DragHandler
|
||||
): BufferedImage {
|
||||
val crop = cropBitmapByBounds(
|
||||
bitmap,
|
||||
getDisplayBounds(bitmap, size),
|
||||
size,
|
||||
scale,
|
||||
drag
|
||||
)
|
||||
return cropImage(
|
||||
bitmap,
|
||||
Rectangle(crop.x, crop.y, crop.width - crop.x, crop.height - crop.y)
|
||||
)
|
||||
}
|
||||
|
||||
fun cropBitmapByBounds(
|
||||
bitmap: BufferedImage,
|
||||
bounds: Rectangle,
|
||||
size: DpSize,
|
||||
scaleFactor: Float,
|
||||
drag: DragHandler
|
||||
): Rectangle {
|
||||
|
||||
if (scaleFactor <= 1f) {
|
||||
return Rectangle(0, 0, bitmap.width, bitmap.height)
|
||||
}
|
||||
|
||||
var scale = scaleFactor.toDouble().pow(1.4)
|
||||
|
||||
var boundW = (bounds.width / scale).roundToInt()
|
||||
var boundH = (bounds.height / scale).roundToInt()
|
||||
|
||||
scale *= size.width.value / bounds.width.toDouble()
|
||||
|
||||
val offsetX = drag.getAmount().x / scale
|
||||
val offsetY = drag.getAmount().y / scale
|
||||
|
||||
if (boundW > bitmap.width) {
|
||||
boundW = bitmap.width
|
||||
}
|
||||
if (boundH > bitmap.height) {
|
||||
boundH = bitmap.height
|
||||
}
|
||||
|
||||
val invisibleW = bitmap.width - boundW
|
||||
var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt()
|
||||
|
||||
if (leftOffset > invisibleW) {
|
||||
leftOffset = invisibleW
|
||||
drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (leftOffset < 0) {
|
||||
drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
leftOffset = 0
|
||||
}
|
||||
|
||||
val invisibleH = bitmap.height - boundH
|
||||
var topOffset = (invisibleH / 2 - offsetY).roundToInt()
|
||||
|
||||
if (topOffset > invisibleH) {
|
||||
topOffset = invisibleH
|
||||
drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (topOffset < 0) {
|
||||
drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
topOffset = 0
|
||||
}
|
||||
|
||||
return Rectangle(leftOffset, topOffset, leftOffset + boundW, topOffset + boundH)
|
||||
}
|
||||
|
||||
fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): DpSize {
|
||||
val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize
|
||||
val preferredWidth: Int = (screenSize.width * 0.8f).toInt()
|
||||
val preferredHeight: Int = (screenSize.height * 0.8f).toInt()
|
||||
val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth
|
||||
val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight
|
||||
return DpSize(width.dp, height.dp)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import example.imageviewer.model.AppState
|
||||
import example.imageviewer.model.ScreenType
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.style.Gray
|
||||
|
||||
private val message: MutableState<String> = mutableStateOf("")
|
||||
private val state: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
@Composable
|
||||
fun AppUI(content: ContentState) {
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = Gray
|
||||
) {
|
||||
when (AppState.screenState()) {
|
||||
ScreenType.MainScreen -> {
|
||||
MainScreen(content)
|
||||
}
|
||||
ScreenType.FullscreenImage -> {
|
||||
FullscreenImage(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toast(message.value, state)
|
||||
}
|
||||
|
||||
fun showPopUpMessage(text: String) {
|
||||
message.value = text
|
||||
state.value = true
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import example.imageviewer.core.FilterType
|
||||
import example.imageviewer.model.AppState
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.model.ScreenType
|
||||
import example.imageviewer.ResString
|
||||
import example.imageviewer.style.DarkGray
|
||||
import example.imageviewer.style.Foreground
|
||||
import example.imageviewer.style.MiniatureColor
|
||||
import example.imageviewer.style.TranslucentBlack
|
||||
import example.imageviewer.style.Transparent
|
||||
import example.imageviewer.style.icBack
|
||||
import example.imageviewer.style.icFilterBlurOff
|
||||
import example.imageviewer.style.icFilterBlurOn
|
||||
import example.imageviewer.style.icFilterGrayscaleOff
|
||||
import example.imageviewer.style.icFilterGrayscaleOn
|
||||
import example.imageviewer.style.icFilterPixelOff
|
||||
import example.imageviewer.style.icFilterPixelOn
|
||||
|
||||
@Composable
|
||||
fun FullscreenImage(
|
||||
content: ContentState
|
||||
) {
|
||||
Column {
|
||||
ToolBar(content.getSelectedImageName(), content)
|
||||
Image(content)
|
||||
}
|
||||
if (!content.isContentReady()) {
|
||||
LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ToolBar(
|
||||
text: String,
|
||||
content: ContentState
|
||||
) {
|
||||
val backButtonInteractionSource = remember { MutableInteractionSource() }
|
||||
val backButtonHover by backButtonInteractionSource.collectIsHoveredAsState()
|
||||
Surface(
|
||||
color = MiniatureColor,
|
||||
modifier = Modifier.height(44.dp)
|
||||
) {
|
||||
Row(modifier = Modifier.padding(end = 30.dp)) {
|
||||
Surface(
|
||||
color = Transparent,
|
||||
modifier = Modifier.padding(start = 20.dp).align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Tooltip(ResString.back) {
|
||||
Clickable(
|
||||
modifier = Modifier
|
||||
.hoverable(backButtonInteractionSource)
|
||||
.background(color = if (backButtonHover) TranslucentBlack else Transparent),
|
||||
onClick = {
|
||||
if (content.isContentReady()) {
|
||||
content.restoreMainImage()
|
||||
AppState.screenState(ScreenType.MainScreen)
|
||||
}
|
||||
}) {
|
||||
Image(
|
||||
icBack(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(38.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text,
|
||||
color = Foreground,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.padding(start = 30.dp).weight(1f)
|
||||
.align(Alignment.CenterVertically),
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
|
||||
Surface(
|
||||
color = Color(255, 255, 255, 40),
|
||||
modifier = Modifier.size(154.dp, 38.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
val state = rememberScrollState(0)
|
||||
Row(modifier = Modifier.horizontalScroll(state)) {
|
||||
Row {
|
||||
for (type in FilterType.values()) {
|
||||
FilterButton(content, type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FilterButton(
|
||||
content: ContentState,
|
||||
type: FilterType,
|
||||
modifier: Modifier = Modifier.size(38.dp)
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val filterButtonHover by interactionSource.collectIsHoveredAsState()
|
||||
Box(
|
||||
modifier = Modifier.background(color = Transparent).clip(CircleShape)
|
||||
) {
|
||||
Tooltip("$type") {
|
||||
Clickable(
|
||||
modifier = Modifier
|
||||
.hoverable(interactionSource)
|
||||
.background(color = if (filterButtonHover) TranslucentBlack else Transparent),
|
||||
onClick = { content.toggleFilter(type)}
|
||||
) {
|
||||
Image(
|
||||
getFilterImage(type = type, content = content),
|
||||
contentDescription = null,
|
||||
modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(20.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getFilterImage(type: FilterType, content: ContentState): Painter {
|
||||
return when (type) {
|
||||
FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
|
||||
FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
|
||||
FilterType.Blur -> if (content.isFilterEnabled(type)) icFilterBlurOn() else icFilterBlurOff()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun Image(content: ContentState) {
|
||||
val onUpdate = remember { { content.updateMainImage() } }
|
||||
Surface(
|
||||
color = DarkGray,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Draggable(
|
||||
onUpdate = onUpdate,
|
||||
dragHandler = content.drag,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Zoomable(
|
||||
onUpdate = onUpdate,
|
||||
scaleHandler = content.scale,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.onPreviewKeyEvent {
|
||||
if (it.type == KeyEventType.KeyUp) {
|
||||
when (it.key) {
|
||||
Key.DirectionLeft -> {
|
||||
content.swipePrevious()
|
||||
}
|
||||
Key.DirectionRight -> {
|
||||
content.swipeNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
bitmap = content.getSelectedImage(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import example.imageviewer.ResString
|
||||
import example.imageviewer.model.AppState
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.model.Picture
|
||||
import example.imageviewer.model.ScreenType
|
||||
import example.imageviewer.style.DarkGray
|
||||
import example.imageviewer.style.DarkGreen
|
||||
import example.imageviewer.style.Foreground
|
||||
import example.imageviewer.style.LightGray
|
||||
import example.imageviewer.style.MiniatureColor
|
||||
import example.imageviewer.style.MiniatureHoverColor
|
||||
import example.imageviewer.style.TranslucentBlack
|
||||
import example.imageviewer.style.TranslucentWhite
|
||||
import example.imageviewer.style.Transparent
|
||||
import example.imageviewer.style.icDots
|
||||
import example.imageviewer.style.icEmpty
|
||||
import example.imageviewer.style.icRefresh
|
||||
import example.imageviewer.utils.toByteArray
|
||||
|
||||
@Composable
|
||||
fun MainScreen(content: ContentState) {
|
||||
Column {
|
||||
TopContent(content)
|
||||
ScrollableArea(content)
|
||||
}
|
||||
if (!content.isContentReady()) {
|
||||
LoadingScreen(ResString.loading)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopContent(content: ContentState) {
|
||||
TitleBar(text = ResString.appName, content = content)
|
||||
PreviewImage(content)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Divider()
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TitleBar(text: String, content: ContentState) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val refreshButtonHover by interactionSource.collectIsHoveredAsState()
|
||||
TopAppBar(
|
||||
backgroundColor = DarkGreen,
|
||||
title = {
|
||||
Row(Modifier.height(50.dp)) {
|
||||
Text(
|
||||
text,
|
||||
color = Foreground,
|
||||
modifier = Modifier.weight(1f).align(Alignment.CenterVertically)
|
||||
)
|
||||
Surface(
|
||||
color = Transparent,
|
||||
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Tooltip(ResString.refresh) {
|
||||
Clickable(
|
||||
modifier = Modifier
|
||||
.hoverable(interactionSource)
|
||||
.background(color = if (refreshButtonHover) TranslucentBlack else Transparent),
|
||||
onClick = {
|
||||
if (content.isContentReady()) {
|
||||
content.refresh()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
icRefresh(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(35.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreviewImage(content: ContentState) {
|
||||
Clickable(
|
||||
modifier = Modifier.background(color = DarkGray),
|
||||
onClick = {
|
||||
AppState.screenState(ScreenType.FullscreenImage)
|
||||
}
|
||||
) {
|
||||
Card(
|
||||
backgroundColor = Transparent,
|
||||
modifier = Modifier.height(250.dp),
|
||||
shape = RectangleShape,
|
||||
elevation = 1.dp
|
||||
) {
|
||||
Image(
|
||||
if (content.isMainImageEmpty())
|
||||
icEmpty()
|
||||
else
|
||||
BitmapPainter(content.getSelectedImage()),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Miniature(
|
||||
picture: Picture,
|
||||
content: ContentState
|
||||
) {
|
||||
val cardHoverInteractionSource = remember { MutableInteractionSource() }
|
||||
val cardHover by cardHoverInteractionSource.collectIsHoveredAsState()
|
||||
val infoButtonInteractionSource = remember { MutableInteractionSource() }
|
||||
val infoButtonHover by infoButtonInteractionSource.collectIsHoveredAsState()
|
||||
Card(
|
||||
backgroundColor = if (cardHover) MiniatureHoverColor else MiniatureColor,
|
||||
modifier = Modifier.padding(start = 10.dp, end = 18.dp).height(70.dp)
|
||||
.fillMaxWidth()
|
||||
.hoverable(cardHoverInteractionSource)
|
||||
.clickable {
|
||||
content.setMainImage(picture)
|
||||
},
|
||||
shape = RectangleShape
|
||||
) {
|
||||
Row(modifier = Modifier.padding(end = 30.dp)) {
|
||||
Clickable(
|
||||
onClick = {
|
||||
content.fullscreen(picture)
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
org.jetbrains.skia.Image.makeFromEncoded(
|
||||
toByteArray(picture.image)
|
||||
).toComposeImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(70.dp)
|
||||
.width(90.dp)
|
||||
.padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 1.dp),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = picture.name,
|
||||
color = Foreground,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(start = 16.dp),
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
|
||||
Clickable(
|
||||
modifier = Modifier.height(70.dp)
|
||||
.width(30.dp)
|
||||
.hoverable(infoButtonInteractionSource)
|
||||
.background(color = if (infoButtonHover) TranslucentWhite else Transparent),
|
||||
onClick = {
|
||||
showPopUpMessage(
|
||||
"${ResString.picture} " +
|
||||
"${picture.name} \n" +
|
||||
"${ResString.size} " +
|
||||
"${picture.width}x${picture.height} " +
|
||||
"${ResString.pixels}"
|
||||
)
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
icDots(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(70.dp)
|
||||
.width(30.dp)
|
||||
.padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp),
|
||||
contentScale = ContentScale.FillHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScrollableArea(content: ContentState) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
val stateVertical = rememberScrollState(0)
|
||||
Column(modifier = Modifier.verticalScroll(stateVertical)) {
|
||||
var index = 1
|
||||
Column {
|
||||
for (picture in content.getMiniatures()) {
|
||||
Miniature(
|
||||
picture = picture,
|
||||
content = content
|
||||
)
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalScrollbar(
|
||||
adapter = rememberScrollbarAdapter(stateVertical),
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
.fillMaxHeight()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Divider() {
|
||||
Divider(
|
||||
color = LightGray,
|
||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp)
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import example.imageviewer.style.Transparent
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun Zoomable(
|
||||
scaleHandler: ScaleHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
onUpdate: (() -> Unit)? = null,
|
||||
children: @Composable() () -> Unit
|
||||
) {
|
||||
val focusRequester = FocusRequester()
|
||||
|
||||
Surface(
|
||||
color = Transparent,
|
||||
modifier = modifier.onPreviewKeyEvent {
|
||||
if (it.type == KeyEventType.KeyUp) {
|
||||
when (it.key) {
|
||||
Key.I -> {
|
||||
scaleHandler.onScale(1.2f)
|
||||
onUpdate?.invoke()
|
||||
}
|
||||
Key.O -> {
|
||||
scaleHandler.onScale(0.8f)
|
||||
onUpdate?.invoke()
|
||||
}
|
||||
Key.R -> {
|
||||
scaleHandler.reset()
|
||||
onUpdate?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
.focusRequester(focusRequester)
|
||||
.focusable()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(onDoubleTap = { scaleHandler.reset() }) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
) {
|
||||
children()
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
onDispose { }
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
@@ -1,59 +0,0 @@
|
||||
package example.imageviewer
|
||||
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.window.WindowPosition
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.style.icAppRounded
|
||||
import example.imageviewer.utils.getPreferredWindowSize
|
||||
import example.imageviewer.view.AppUI
|
||||
import example.imageviewer.view.SplashUI
|
||||
|
||||
fun main() = application {
|
||||
val state = rememberWindowState()
|
||||
val content = remember {
|
||||
ContentState.applyContent(
|
||||
state,
|
||||
"https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list"
|
||||
)
|
||||
}
|
||||
|
||||
val icon = icAppRounded()
|
||||
|
||||
if (content.isAppReady()) {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Image Viewer",
|
||||
state = WindowState(
|
||||
position = WindowPosition.Aligned(Alignment.Center),
|
||||
size = getPreferredWindowSize(800, 1000)
|
||||
),
|
||||
icon = icon
|
||||
) {
|
||||
MaterialTheme {
|
||||
AppUI(content)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Image Viewer",
|
||||
state = WindowState(
|
||||
position = WindowPosition.Aligned(Alignment.Center),
|
||||
size = getPreferredWindowSize(800, 300)
|
||||
),
|
||||
undecorated = true,
|
||||
icon = icon,
|
||||
) {
|
||||
MaterialTheme {
|
||||
SplashUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22)
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ kotlin {
|
||||
withJava()
|
||||
}
|
||||
sourceSets {
|
||||
named("jvmMain") {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(project(":common"))
|
||||
implementation(project(":shared"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package example.imageviewer
|
||||
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.ui.window.application
|
||||
import example.imageviewer.view.ImageViewerDesktop
|
||||
|
||||
fun main() = application {
|
||||
MaterialTheme {
|
||||
ImageViewerDesktop()
|
||||
}
|
||||
}
|
||||
36
experimental/examples/imageviewer/gradle.properties
Executable file → Normal file
@@ -1,24 +1,18 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
xcodeproj=iosApp
|
||||
kotlin.native.cocoapods.generate.wrapper=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx3g
|
||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||
org.jetbrains.compose.experimental.macos.enabled=true
|
||||
org.jetbrains.compose.experimental.uikit.enabled=true
|
||||
kotlin.native.cacheKind=none
|
||||
kotlin.native.useEmbeddableCompilerJar=true
|
||||
kotlin.native.enableDependencyPropagation=false
|
||||
kotlin.mpp.enableGranularSourceSetsMetadata=true
|
||||
# Enable kotlin/native experimental memory model
|
||||
kotlin.native.binary.memoryModel=experimental
|
||||
kotlin.version=1.7.20
|
||||
agp.version=7.1.3
|
||||
compose.version=1.2.1
|
||||
compose.version=1.2.2
|
||||
ktor.version=2.2.1
|
||||
|
||||
275
experimental/examples/imageviewer/gradlew
vendored
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -17,67 +17,101 @@
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
@@ -106,80 +140,101 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
33
experimental/examples/imageviewer/gradlew.bat
vendored
@@ -14,7 +14,7 @@
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +25,7 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -54,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -64,21 +64,6 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
@@ -86,17 +71,19 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
TEAM_ID=
|
||||
@@ -0,0 +1,398 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2152FB042600AC8F00CF470E /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iosApp.swift */; };
|
||||
C1FC908188C4E8695729CB06 /* Pods_Imageviewer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DE96E47030356CE6AD9794A /* Pods_Imageviewer.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1EB65E27D2C0F884D0A1A133 /* Pods-Imageviewer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Imageviewer.debug.xcconfig"; path = "Target Support Files/Pods-Imageviewer/Pods-Imageviewer.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
2152FB032600AC8F00CF470E /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = "<group>"; };
|
||||
3D7A606AB0AD7636269BD9D0 /* Pods-Imageviewer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Imageviewer.release.xcconfig"; path = "Target Support Files/Pods-Imageviewer/Pods-Imageviewer.release.xcconfig"; sourceTree = "<group>"; };
|
||||
7555FF7B242A565900829871 /* Imageviewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Imageviewer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
8DE96E47030356CE6AD9794A /* Pods_Imageviewer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Imageviewer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AB3632DC29227652001CCB65 /* TeamId.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = TeamId.xcconfig; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
9964867F0862B4D9FB6ABFC7 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C1FC908188C4E8695729CB06 /* Pods_Imageviewer.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
7555FF72242A565900829871 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AB1DB47929225F7C00F7AF9C /* Configuration */,
|
||||
7555FF7D242A565900829871 /* iosApp */,
|
||||
7555FF7C242A565900829871 /* Products */,
|
||||
E1DAFBE8E1CFC0878361EF0E /* Pods */,
|
||||
B62309C7396AD7BF607A63B2 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7555FF7C242A565900829871 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7555FF7B242A565900829871 /* Imageviewer.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7555FF7D242A565900829871 /* iosApp */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7555FF8C242A565B00829871 /* Info.plist */,
|
||||
2152FB032600AC8F00CF470E /* iosApp.swift */,
|
||||
);
|
||||
path = iosApp;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AB1DB47929225F7C00F7AF9C /* Configuration */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AB3632DC29227652001CCB65 /* TeamId.xcconfig */,
|
||||
);
|
||||
path = Configuration;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B62309C7396AD7BF607A63B2 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8DE96E47030356CE6AD9794A /* Pods_Imageviewer.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1DAFBE8E1CFC0878361EF0E /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1EB65E27D2C0F884D0A1A133 /* Pods-Imageviewer.debug.xcconfig */,
|
||||
3D7A606AB0AD7636269BD9D0 /* Pods-Imageviewer.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
7555FF7A242A565900829871 /* Imageviewer */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "Imageviewer" */;
|
||||
buildPhases = (
|
||||
E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */,
|
||||
7555FF77242A565900829871 /* Sources */,
|
||||
7555FF79242A565900829871 /* Resources */,
|
||||
9964867F0862B4D9FB6ABFC7 /* Frameworks */,
|
||||
F34398AEB6C0D136D245A061 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Imageviewer;
|
||||
productName = iosApp;
|
||||
productReference = 7555FF7B242A565900829871 /* Imageviewer.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
7555FF73242A565900829871 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1130;
|
||||
LastUpgradeCheck = 1130;
|
||||
ORGANIZATIONNAME = org.jetbrains;
|
||||
TargetAttributes = {
|
||||
7555FF7A242A565900829871 = {
|
||||
CreatedOnToolsVersion = 11.3.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "Imageviewer" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 7555FF72242A565900829871;
|
||||
productRefGroup = 7555FF7C242A565900829871 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
7555FF7A242A565900829871 /* Imageviewer */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
7555FF79242A565900829871 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Imageviewer-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F34398AEB6C0D136D245A061 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Imageviewer/Pods-Imageviewer-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Imageviewer/Pods-Imageviewer-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Imageviewer/Pods-Imageviewer-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
7555FF77242A565900829871 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2152FB042600AC8F00CF470E /* iosApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
7555FFA3242A565B00829871 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AB3632DC29227652001CCB65 /* TeamId.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7555FFA4242A565B00829871 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AB3632DC29227652001CCB65 /* TeamId.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
7555FFA6242A565B00829871 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1EB65E27D2C0F884D0A1A133 /* Pods-Imageviewer.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "${TEAM_ID}";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = iosApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.Imageviewer${TEAM_ID}";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7555FFA7242A565B00829871 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3D7A606AB0AD7636269BD9D0 /* Pods-Imageviewer.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "${TEAM_ID}";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = iosApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.Imageviewer${TEAM_ID}";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
7555FF76242A565900829871 /* Build configuration list for PBXProject "Imageviewer" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7555FFA3242A565B00829871 /* Debug */,
|
||||
7555FFA4242A565B00829871 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "Imageviewer" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7555FFA6242A565B00829871 /* Debug */,
|
||||
7555FFA7242A565B00829871 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 7555FF73242A565900829871 /* Project object */;
|
||||
}
|
||||
5
experimental/examples/imageviewer/iosApp/Podfile
Normal file
@@ -0,0 +1,5 @@
|
||||
target 'Imageviewer' do
|
||||
use_frameworks!
|
||||
platform :ios, '14.1'
|
||||
pod 'shared', :path => '../shared'
|
||||
end
|
||||
48
experimental/examples/imageviewer/iosApp/iosApp/Info.plist
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
15
experimental/examples/imageviewer/iosApp/iosApp/iosApp.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import UIKit
|
||||
import shared
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
let mainViewController = Main_iosKt.MainViewController()
|
||||
window?.rootViewController = mainViewController
|
||||
window?.makeKeyAndVisible()
|
||||
return true
|
||||
}
|
||||
}
|
||||
BIN
experimental/examples/imageviewer/run-configurations.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
8
experimental/examples/imageviewer/settings.gradle.kts
Executable file → Normal file
@@ -12,11 +12,17 @@ pluginManagement {
|
||||
|
||||
kotlin("jvm").version(kotlinVersion)
|
||||
kotlin("multiplatform").version(kotlinVersion)
|
||||
kotlin("plugin.serialization").version(kotlinVersion)
|
||||
kotlin("android").version(kotlinVersion)
|
||||
id("com.android.base").version(agpVersion)
|
||||
id("com.android.application").version(agpVersion)
|
||||
id("com.android.library").version(agpVersion)
|
||||
id("org.jetbrains.compose").version(composeVersion)
|
||||
}
|
||||
}
|
||||
|
||||
include(":common", ":android", ":desktop")
|
||||
rootProject.name = "imageviewer"
|
||||
|
||||
include(":androidApp")
|
||||
include(":shared")
|
||||
include(":desktopApp")
|
||||
|
||||
84
experimental/examples/imageviewer/shared/build.gradle.kts
Executable file
@@ -0,0 +1,84 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
kotlin("native.cocoapods")
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.compose")
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
version = "1.0-SNAPSHOT"
|
||||
val ktorVersion = extra["ktor.version"]
|
||||
|
||||
kotlin {
|
||||
android()
|
||||
jvm("desktop")
|
||||
ios()
|
||||
iosSimulatorArm64()
|
||||
|
||||
cocoapods {
|
||||
summary = "Shared code for the sample"
|
||||
homepage = "https://github.com/JetBrains/compose-jb"
|
||||
ios.deploymentTarget = "14.1"
|
||||
podfile = project.file("../iosApp/Podfile")
|
||||
framework {
|
||||
baseName = "shared"
|
||||
isStatic = true
|
||||
}
|
||||
extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material)
|
||||
implementation("org.jetbrains.compose.components:components-resources:1.3.0-beta04-dev879")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
|
||||
}
|
||||
}
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
implementation("androidx.appcompat:appcompat:1.5.1")
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
|
||||
}
|
||||
}
|
||||
val iosMain by getting {
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
|
||||
}
|
||||
}
|
||||
val iosTest by getting
|
||||
val iosSimulatorArm64Main by getting {
|
||||
dependsOn(iosMain)
|
||||
}
|
||||
val iosSimulatorArm64Test by getting {
|
||||
dependsOn(iosTest)
|
||||
}
|
||||
|
||||
val desktopMain by getting {
|
||||
dependencies {
|
||||
implementation(compose.desktop.common)
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4")
|
||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
||||
sourceSets["main"].res.srcDirs("src/androidMain/res")
|
||||
sourceSets["main"].resources.srcDir("src/commonMain/resources")
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
targetSdk = 33
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||