@ -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"} |
||||||
|
] |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 26 KiB |
@ -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> |
@ -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,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) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -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 |
||||||
|
} |
||||||
|
} |
@ -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,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() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -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() |
||||||
|
} |
||||||
|
} |
@ -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 |
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 |
kotlin.version=1.7.20 |
||||||
agp.version=7.1.3 |
agp.version=7.1.3 |
||||||
compose.version=1.2.1 |
compose.version=1.2.2 |
||||||
|
ktor.version=2.2.1 |
||||||
|
@ -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 */; |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
target 'Imageviewer' do |
||||||
|
use_frameworks! |
||||||
|
platform :ios, '14.1' |
||||||
|
pod 'shared', :path => '../shared' |
||||||
|
end |
@ -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> |
@ -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 |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 15 KiB |
@ -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 |
||||||
|
} |
||||||
|
} |