@ -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 |
||||
xcodeproj=iosApp |
||||
kotlin.native.cocoapods.generate.wrapper=true |
||||
android.useAndroidX=true |
||||
org.gradle.jvmargs=-Xmx3g |
||||
org.jetbrains.compose.experimental.jscanvas.enabled=true |
||||
org.jetbrains.compose.experimental.macos.enabled=true |
||||
org.jetbrains.compose.experimental.uikit.enabled=true |
||||
kotlin.native.cacheKind=none |
||||
kotlin.native.useEmbeddableCompilerJar=true |
||||
kotlin.native.enableDependencyPropagation=false |
||||
kotlin.mpp.enableGranularSourceSetsMetadata=true |
||||
# Enable kotlin/native experimental memory model |
||||
kotlin.native.binary.memoryModel=experimental |
||||
kotlin.version=1.7.20 |
||||
agp.version=7.1.3 |
||||
compose.version=1.2.1 |
||||
compose.version=1.2.2 |
||||
ktor.version=2.2.1 |
||||
|
@ -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 |
||||
} |
||||
} |