Start adding iOS support to presentation

This commit is contained in:
Syer10
2022-11-17 19:49:27 -05:00
parent 7b2a54d2bc
commit 719a0720b8
51 changed files with 977 additions and 64 deletions

View File

@@ -7,7 +7,9 @@
package ca.gosyer.jui.core.io
import ca.gosyer.jui.core.lang.withIOContext
import okio.Buffer
import okio.BufferedSink
import okio.BufferedSource
import okio.FileSystem
import okio.Path
import okio.Source
@@ -32,3 +34,7 @@ suspend fun Source.copyTo(sink: BufferedSink) {
}
}
}
fun ByteArray.source(): BufferedSource {
return Buffer().write(this)
}

View File

@@ -25,6 +25,9 @@ kotlin {
}
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
all {
@@ -53,13 +56,16 @@ kotlin {
api(libs.dateTime)
api(libs.immutableCollections)
api(libs.aboutLibraries.core)
api(libs.aboutLibraries.ui)
api(projects.core)
api(projects.i18n)
api(projects.domain)
api(projects.data)
api(projects.uiCore)
api(compose.desktop.currentOs)
api(compose.runtime)
api(compose.foundation)
api(compose.ui)
api(compose.material)
api(compose("org.jetbrains.compose.ui:ui-util"))
api(compose.materialIconsExtended)
}
@@ -75,6 +81,8 @@ kotlin {
dependsOn(commonMain)
dependencies {
api(kotlin("stdlib-jdk8"))
api(libs.aboutLibraries.ui)
api(compose.desktop.currentOs)
}
}
val jvmTest by creating {
@@ -107,6 +115,22 @@ kotlin {
val androidTest by getting {
dependsOn(jvmTest)
}
val iosMain by creating {
dependsOn(commonMain)
}
val iosTest by creating {
dependsOn(commonTest)
}
listOf(
"iosX64",
"iosArm64",
"iosSimulatorArm64",
).forEach {
getByName(it + "Main").dependsOn(iosMain)
getByName(it + "Test").dependsOn(iosTest)
}
}
}

View File

@@ -18,6 +18,7 @@ import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
import com.mikepenz.aboutlibraries.ui.compose.Libraries
import com.mikepenz.aboutlibraries.ui.compose.LibraryColors
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
import com.mikepenz.aboutlibraries.util.withContext
import kotlinx.collections.immutable.ImmutableList
@@ -35,6 +36,10 @@ actual fun getLicenses(): Libs? {
return libs
}
actual typealias LibraryDefaults = LibraryDefaults
actual typealias LibraryColors = LibraryColors
@Composable
actual fun InternalAboutLibraries(
libraries: ImmutableList<Library>,

View File

@@ -12,8 +12,6 @@ import androidx.compose.ui.Modifier
actual fun Modifier.sourceSideMenuItem(
onSourceTabClick: () -> Unit,
onSourceCloseTabClick: () -> Unit
): Modifier = Modifier.clickable(
onClick = {
onSourceTabClick()
}
): Modifier = clickable(
onClick = onSourceTabClick
)

View File

@@ -7,7 +7,10 @@
package ca.gosyer.jui.ui.base.state
import ca.gosyer.jui.uicore.vm.ViewModel
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
@@ -17,14 +20,17 @@ fun <T> SavedStateHandle.getStateFlow(
return SavedStateHandleDelegate(this, initialValue)
}
@OptIn(InternalCoroutinesApi::class)
class SavedStateHandleDelegate<T>(
private val savedStateHandle: SavedStateHandle,
private val initialValue: () -> T
) : ReadOnlyProperty<ViewModel, SavedStateHandleStateFlow<T>> {
private val synchronizedObject = SynchronizedObject()
private var item: SavedStateHandleStateFlow<T>? = null
override fun getValue(thisRef: ViewModel, property: KProperty<*>): SavedStateHandleStateFlow<T> {
return item ?: synchronized(this) {
return item ?: synchronized(synchronizedObject) {
if (item == null) {
savedStateHandle.getSavedStateFlow(property.name, initialValue)
.also { item = it }

View File

@@ -13,6 +13,7 @@ import ca.gosyer.jui.ui.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import kotlin.jvm.Transient
expect class CategoriesLauncher {

View File

@@ -25,7 +25,7 @@ import ca.gosyer.jui.domain.updates.interactor.UpdateLibrary
import ca.gosyer.jui.i18n.MR
import ca.gosyer.jui.ui.base.state.SavedStateHandle
import ca.gosyer.jui.ui.base.state.getStateFlow
import ca.gosyer.jui.ui.util.lang.Collator
import ca.gosyer.jui.ui.util.lang.CollatorComparator
import ca.gosyer.jui.uicore.vm.ContextWrapper
import ca.gosyer.jui.uicore.vm.ViewModel
import kotlinx.collections.immutable.ImmutableList
@@ -191,7 +191,7 @@ class LibraryScreenViewModel @Inject constructor(
val sortFn: (Manga, Manga) -> Int = when (sortMode) {
Sort.ALPHABETICAL -> {
val locale = Locale.current
val collator = Collator(locale);
val collator = CollatorComparator(locale);
{ a, b ->
collator.compare(a.title.toLowerCase(locale), b.title.toLowerCase(locale))

View File

@@ -19,11 +19,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import ca.gosyer.jui.i18n.MR
@@ -38,8 +41,6 @@ import ca.gosyer.jui.uicore.insets.statusBars
import ca.gosyer.jui.uicore.resources.stringResource
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
import com.mikepenz.aboutlibraries.ui.compose.LibraryColors
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -60,6 +61,18 @@ internal expect fun InternalAboutLibraries(
onLibraryClick: ((Library) -> Unit)?
)
expect interface LibraryColors
expect object LibraryDefaults {
@Composable
fun libraryColors(
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
badgeBackgroundColor: Color = MaterialTheme.colors.primary,
badgeContentColor: Color = contentColorFor(badgeBackgroundColor),
): LibraryColors
val ContentPadding: PaddingValues
}
@Composable
fun AboutLibraries(
libraries: ImmutableList<Library>,

View File

@@ -6,6 +6,8 @@
package ca.gosyer.jui.ui.reader.loader
import ca.gosyer.jui.core.io.SYSTEM
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.core.lang.PriorityChannel
import ca.gosyer.jui.core.lang.throwIfCancellation
import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage
@@ -39,6 +41,7 @@ import kotlinx.coroutines.launch
import okio.BufferedSource
import okio.FileSystem
import okio.buffer
import okio.use
import org.lighthousegames.logging.logging
class TachideskPageLoader(

View File

@@ -36,8 +36,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import ca.gosyer.jui.core.io.SYSTEM
import ca.gosyer.jui.core.io.copyTo
import ca.gosyer.jui.core.io.saveTo
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.core.lang.throwIfCancellation
import ca.gosyer.jui.domain.backup.interactor.ExportBackupFile
import ca.gosyer.jui.domain.backup.interactor.ImportBackupFile

View File

@@ -7,6 +7,7 @@
package ca.gosyer.jui.ui.sources.globalsearch
import androidx.compose.runtime.snapshots.SnapshotStateMap
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.domain.manga.model.Manga
import ca.gosyer.jui.domain.source.interactor.GetSearchManga
import ca.gosyer.jui.domain.source.interactor.GetSourceList

View File

@@ -8,8 +8,6 @@ package ca.gosyer.jui.ui.util.lang
import androidx.compose.ui.text.intl.Locale
expect fun Collator(locale: Locale): Collator
expect class Collator {
fun compare(source: String, target: String): Int
expect class CollatorComparator(locale: Locale) : Comparator<String> {
override fun compare(source: String, target: String): Int
}

View File

@@ -9,4 +9,4 @@ package ca.gosyer.jui.ui.util.lang
import io.ktor.utils.io.ByteReadChannel
import okio.Source
expect fun ByteReadChannel.toSource(): Source
expect suspend fun ByteReadChannel.toSource(): Source

View File

@@ -18,6 +18,7 @@ import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
import com.mikepenz.aboutlibraries.ui.compose.Libraries
import com.mikepenz.aboutlibraries.ui.compose.LibraryColors
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
import kotlinx.collections.immutable.ImmutableList
@Composable
@@ -33,6 +34,10 @@ actual fun getLicenses(): Libs? {
return libs
}
actual typealias LibraryDefaults = LibraryDefaults
actual typealias LibraryColors = LibraryColors
@Composable
actual fun InternalAboutLibraries(
libraries: ImmutableList<Library>,

View File

@@ -0,0 +1,28 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.saveable.rememberSaveable
import ca.gosyer.jui.ui.base.LocalViewModels
import ca.gosyer.jui.ui.base.state.SavedStateHandle
import ca.gosyer.jui.uicore.vm.ViewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
actual interface ViewModelComponent : SharedViewModelComponent
@Composable
actual inline fun <reified VM : ViewModel> Screen.stateViewModel(
tag: String?,
crossinline factory: @DisallowComposableCalls ViewModelComponent.(SavedStateHandle) -> VM
): VM {
val viewModelFactory = LocalViewModels.current
val savedStateHandle = rememberSaveable { SavedStateHandle() }
return rememberScreenModel(tag) { viewModelFactory.factory(savedStateHandle) }
}

View File

@@ -0,0 +1,41 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.components
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
actual interface TooltipPlacement
actual class CursorPoint actual constructor(
offset: DpOffset,
alignment: Alignment,
windowMargin: Dp
) : TooltipPlacement
actual class ComponentRect actual constructor(
anchor: Alignment,
alignment: Alignment,
offset: DpOffset
) : TooltipPlacement
@Composable
actual fun TooltipArea(
tooltip: @Composable () -> Unit,
modifier: Modifier,
delayMillis: Int,
tooltipPlacement: TooltipPlacement,
content: @Composable () -> Unit
) {
Box(Modifier) {
content()
}
}

View File

@@ -0,0 +1,22 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.file
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import okio.Source
actual class FileChooser(private val onFileFound: (Source) -> Unit) {
actual fun launch(extension: String) {
TODO()
}
}
@Composable
actual fun rememberFileChooser(onFileFound: (Source) -> Unit): FileChooser {
return remember { FileChooser(onFileFound) }
}

View File

@@ -0,0 +1,30 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.file
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import okio.Sink
actual class FileSaver(
private val onFileSelected: (Sink) -> Unit,
private val onCancel: () -> Unit,
private val onError: () -> Unit,
) {
actual fun save(name: String) {
TODO()
}
}
@Composable
actual fun rememberFileSaver(
onFileSelected: (Sink) -> Unit,
onCancel: () -> Unit,
onError: () -> Unit
): FileSaver {
return remember { FileSaver(onFileSelected, onCancel, onError) }
}

View File

@@ -0,0 +1,14 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.image
import ca.gosyer.jui.uicore.vm.ContextWrapper
import com.seiko.imageloader.component.decoder.Decoder
import com.seiko.imageloader.component.decoder.SkiaImageDecoder
actual class BitmapDecoderFactory actual constructor(contextWrapper: ContextWrapper) :
Decoder.Factory by SkiaImageDecoder.Factory()

View File

@@ -0,0 +1,44 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.image
import ca.gosyer.jui.uicore.vm.ContextWrapper
import com.seiko.imageloader.ImageLoaderBuilder
import com.seiko.imageloader.cache.disk.DiskCache
import com.seiko.imageloader.cache.disk.DiskCacheBuilder
import com.seiko.imageloader.cache.memory.MemoryCache
import com.seiko.imageloader.cache.memory.MemoryCacheBuilder
import com.seiko.imageloader.request.Options
import okio.Path.Companion.toPath
import platform.Foundation.NSCachesDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask
actual val imageConfig: Options.ImageConfig = Options.ImageConfig.ARGB_8888
actual fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilder {
return ImageLoaderBuilder()
}
actual fun diskCache(contextWrapper: ContextWrapper, cacheDir: String): DiskCache {
return DiskCacheBuilder()
.directory(getCacheDir().toPath() / cacheDir)
.maxSizeBytes(1024 * 1024 * 150) // 150 MB
.build()
}
private fun getCacheDir(): String {
return NSFileManager.defaultManager.URLForDirectory(
NSCachesDirectory, NSUserDomainMask, null, true, null
)!!.path.orEmpty()
}
actual fun memoryCache(contextWrapper: ContextWrapper): MemoryCache {
return MemoryCacheBuilder()
.maxSizePercent(0.25)
.build()
}

View File

@@ -0,0 +1,20 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.navigation
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
// todo
@Composable
actual fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) {
IconButton(onClick = onClick) {
Icon(icon, contentDescription)
}
}

View File

@@ -0,0 +1,12 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.navigation
import androidx.compose.runtime.Composable
@Composable
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {}

View File

@@ -0,0 +1,37 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.prefs
import androidx.compose.ui.graphics.Color
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.value
import platform.CoreGraphics.CGFloatVar
import platform.UIKit.UIColor
fun Color.toUIColor() = UIColor(red.toDouble(), green.toDouble(), blue.toDouble(), 1.0)
internal actual fun Color.toHsv(): FloatArray = memScoped {
val uiColor = toUIColor()
val hue = alloc<CGFloatVar>()
val saturation = alloc<CGFloatVar>()
val brightness = alloc<CGFloatVar>()
val alpha = alloc<CGFloatVar>()
uiColor.getHue(hue.ptr, saturation.ptr, brightness.ptr, alpha.ptr)
floatArrayOf(hue.value.toFloat(), saturation.value.toFloat(), brightness.value.toFloat())
}
internal actual fun hexStringToColor(hex: String): Color? {
return try {
val i = hex.removePrefix("#").toInt(16)
Color(i shr 16 and 0xFF, i shr 8 and 0xFF, i and 0xFF)
} catch (e: Exception) {
null
}
}

View File

@@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.screen
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
actual abstract class BaseScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
}

View File

@@ -0,0 +1,50 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.state
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
actual class SavedStateHandle {
private val regular = mutableMapOf<String, Any?>()
private val flows = mutableMapOf<String, MutableStateFlow<Any?>>()
actual operator fun <T> get(key: String): T? {
@Suppress("UNCHECKED_CAST")
return regular[key] as T?
}
actual operator fun <T> set(key: String, value: T?) {
regular[key] = value
flows[key]?.value = value
}
actual fun <T> remove(key: String): T? {
@Suppress("UNCHECKED_CAST")
val latestValue = regular.remove(key) as T?
flows.remove(key)
return latestValue
}
actual fun <T> getStateFlow(
key: String,
initialValue: T
): StateFlow<T> {
@Suppress("UNCHECKED_CAST")
// If a flow exists we should just return it, and since it is a StateFlow and a value must
// always be set, we know a value must already be available
return flows.getOrPut(key) {
// If there is not a value associated with the key, add the initial value, otherwise,
// use the one we already have.
if (!regular.containsKey(key)) {
regular[key] = initialValue
}
MutableStateFlow(regular[key]).apply { flows[key] = this }
}.asStateFlow() as StateFlow<T>
}
}

View File

@@ -0,0 +1,21 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.base.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import ca.gosyer.jui.uicore.components.ScrollbarStyle
actual object ThemeScrollbarStyle {
private val defaultScrollbarStyle = ScrollbarStyle()
@Stable
@Composable
actual fun getScrollbarStyle(): ScrollbarStyle {
return defaultScrollbarStyle
}
}

View File

@@ -0,0 +1,29 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.categories
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
actual class CategoriesLauncher(private val navigator: Navigator?) {
actual fun open() {
navigator?.push(CategoriesScreen())
}
@Composable
actual fun CategoriesWindow() {
}
}
@Composable
actual fun rememberCategoriesLauncher(notifyFinished: () -> Unit): CategoriesLauncher {
val navigator = LocalNavigator.current
return remember(navigator) { CategoriesLauncher(navigator) }
}

View File

@@ -0,0 +1,19 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.downloads
import ca.gosyer.jui.domain.base.WebsocketService
import ca.gosyer.jui.domain.download.service.DownloadService
import ca.gosyer.jui.uicore.vm.ContextWrapper
internal actual fun startDownloadService(
contextWrapper: ContextWrapper,
downloadService: DownloadService,
actions: WebsocketService.Actions
) {
downloadService.init()
}

View File

@@ -0,0 +1,51 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import ca.gosyer.jui.i18n.MR
import ca.gosyer.jui.uicore.resources.stringResource
actual fun Modifier.libraryMangaModifier(
onClickManga: () -> Unit,
onClickRemoveManga: () -> Unit
): Modifier = composed {
var expanded by remember { mutableStateOf(false) }
DropdownMenu(
expanded,
onDismissRequest = { expanded = false }
) {
listOf(
stringResource(MR.strings.action_remove_favorite) to onClickRemoveManga
).forEach { (label, onClick) ->
DropdownMenuItem(
onClick = {
expanded = false
onClick()
}
) {
Text(text = label)
}
}
}
Modifier.combinedClickable(
onClick = { onClickManga() },
onLongClick = {
expanded = true
}
)
}

View File

@@ -0,0 +1,22 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.main.about.components
import ca.gosyer.jui.presentation.build.BuildKonfig
import platform.UIKit.UIDevice
actual fun getDebugInfo(): String {
val device = UIDevice.currentDevice
return """
App version: ${BuildKonfig.VERSION} (${ if (BuildKonfig.DEBUG) "Debug" else "Standard"}, ${BuildKonfig.MIGRATION_CODE})
Preview build: r${BuildKonfig.PREVIEW_BUILD}
Device name: ${device.name}
Device model: ${device.model}
System name: ${device.systemName}
System version: ${device.systemVersion}
""".trimIndent()
}

View File

@@ -0,0 +1,74 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.main.about.licenses.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import ca.gosyer.jui.core.lang.withIOContext
import ca.gosyer.jui.i18n.MR
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
import kotlinx.collections.immutable.ImmutableList
@Composable
actual fun getLicenses(): Libs? {
val libs by produceState<Libs?>(
null
) {
withIOContext {
val json = MR.files.aboutlibraries.readText()
value = Libs.Builder().withJson(json).build()
}
}
return libs
}
actual object LibraryDefaults {
@Composable
actual fun libraryColors(
backgroundColor: Color,
contentColor: Color,
badgeBackgroundColor: Color,
badgeContentColor: Color,
): LibraryColors = object : LibraryColors {}
actual val ContentPadding: PaddingValues get() = PaddingValues()
}
actual interface LibraryColors
@Composable
actual fun InternalAboutLibraries(
libraries: ImmutableList<Library>,
modifier: Modifier,
lazyListState: LazyListState,
contentPadding: PaddingValues,
showAuthor: Boolean,
showVersion: Boolean,
showLicenseBadges: Boolean,
colors: LibraryColors,
itemContentPadding: PaddingValues,
onLibraryClick: ((Library) -> Unit)?
) {
/*Libraries(
libraries = libraries,
modifier = modifier,
lazyListState = lazyListState,
contentPadding = contentPadding,
showAuthor = showAuthor,
showVersion = showVersion,
showLicenseBadges = showLicenseBadges,
colors = colors,
itemContentPadding = itemContentPadding,
onLibraryClick = onLibraryClick
)*/
}

View File

@@ -0,0 +1,18 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.main.components
import ca.gosyer.jui.uicore.vm.ContextWrapper
import ca.gosyer.jui.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import me.tatarka.inject.annotations.Inject
actual class DebugOverlayViewModel @Inject constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper) {
actual val maxMemory: String
get() = ""
actual val usedMemoryFlow: MutableStateFlow<String> = MutableStateFlow("")
}

View File

@@ -0,0 +1,19 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.main.components
import ca.gosyer.jui.domain.base.WebsocketService
import ca.gosyer.jui.domain.library.service.LibraryUpdateService
import ca.gosyer.jui.uicore.vm.ContextWrapper
internal actual fun startLibraryUpdatesService(
contextWrapper: ContextWrapper,
libraryUpdatesService: LibraryUpdateService,
actions: WebsocketService.Actions
) {
libraryUpdatesService.init()
}

View File

@@ -0,0 +1,24 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.manga.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.ui.Modifier
actual fun Modifier.chapterItemModifier(
onClick: () -> Unit,
markRead: (() -> Unit)?,
markUnread: (() -> Unit)?,
bookmarkChapter: (() -> Unit)?,
unBookmarkChapter: (() -> Unit)?,
markPreviousAsRead: () -> Unit,
onSelectChapter: (() -> Unit)?,
onUnselectChapter: (() -> Unit)?
): Modifier = combinedClickable(
onClick = onUnselectChapter ?: onClick,
onLongClick = onSelectChapter
)

View File

@@ -0,0 +1,50 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.reader
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import ca.gosyer.jui.ui.base.model.StableHolder
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import kotlinx.coroutines.flow.MutableSharedFlow
class ReaderScreen(val chapterIndex: Int, val mangaId: Long) : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
ReaderMenu(
chapterIndex,
mangaId,
remember { StableHolder(MutableSharedFlow()) },
navigator::pop
)
}
}
actual class ReaderLauncher(private val navigator: Navigator?) {
actual fun launch(
chapterIndex: Int,
mangaId: Long
) {
navigator?.push(ReaderScreen(chapterIndex, mangaId))
}
@Composable
actual fun Reader() {
}
}
@Composable
actual fun rememberReaderLauncher(): ReaderLauncher {
val navigator = LocalNavigator.current
return remember(navigator) { ReaderLauncher(navigator) }
}

View File

@@ -0,0 +1,9 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.settings
actual val showWindowDecorationsOption: Boolean = false

View File

@@ -0,0 +1,20 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.settings
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import ca.gosyer.jui.uicore.vm.ContextWrapper
import ca.gosyer.jui.uicore.vm.ViewModel
import me.tatarka.inject.annotations.Inject
@Composable
actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit {
return {}
}
actual class SettingsServerHostViewModel @Inject constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper)

View File

@@ -0,0 +1,17 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.sources.components
import androidx.compose.foundation.clickable
import androidx.compose.ui.Modifier
actual fun Modifier.sourceSideMenuItem(
onSourceTabClick: () -> Unit,
onSourceCloseTabClick: () -> Unit
): Modifier = clickable(
onClick = onSourceTabClick
)

View File

@@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.updates.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.ui.Modifier
actual fun Modifier.updatesItemModifier(
onClick: () -> Unit,
markRead: (() -> Unit)?,
markUnread: (() -> Unit)?,
bookmarkChapter: (() -> Unit)?,
unBookmarkChapter: (() -> Unit)?,
onSelectChapter: (() -> Unit)?,
onUnselectChapter: (() -> Unit)?
): Modifier = combinedClickable(
onClick = onUnselectChapter ?: onClick,
onLongClick = onSelectChapter
)

View File

@@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.util.compose
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asComposeImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import io.ktor.client.call.body
import io.ktor.client.statement.HttpResponse
import org.jetbrains.skia.Image
import com.seiko.imageloader.Image as ImageLoaderImage
actual suspend fun HttpResponse.toImageBitmap(): ImageBitmap {
return Image.makeFromEncoded(body<ByteArray>()).toComposeImageBitmap()
}
actual fun ImageLoaderImage.asImageBitmap(): ImageBitmap {
return asComposeImageBitmap()
}

View File

@@ -0,0 +1,18 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.util.compose
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import platform.Foundation.NSString
import platform.Foundation.stringWithFormat
actual fun Color.toHexString(): String {
return NSString.stringWithFormat("#%06X", (0xFFFFFF and toArgb()))
}
actual fun Color.toLong() = NSString.stringWithFormat("%06X", 0xFFFFFF and toArgb()).toLong(16)

View File

@@ -0,0 +1,21 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.util.lang
import androidx.compose.ui.text.intl.Locale
import platform.Foundation.NSString
import platform.Foundation.localizedCaseInsensitiveCompare
actual class CollatorComparator() : Comparator<String> {
actual constructor(locale: Locale) : this()
actual override fun compare(source: String, target: String): Int {
@Suppress("CAST_NEVER_SUCCEEDS")
return (source as NSString).localizedCaseInsensitiveCompare(target).toInt()
}
}

View File

@@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.util.lang
import ca.gosyer.jui.core.io.source
import io.ktor.util.toByteArray
import io.ktor.utils.io.ByteReadChannel
import okio.Source
actual suspend fun ByteReadChannel.toSource(): Source {
return this.toByteArray().source()
}

View File

@@ -0,0 +1,70 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.ui.util.lang
import platform.Foundation.NSString
import platform.Foundation.stringWithFormat
// Taken from https://github.com/icerockdev/moko-resources/blob/master/resources/src/appleMain/kotlin/dev/icerock/moko/resources/desc/Utils.kt
actual fun stringFormat(string: String, vararg args: Any?): String {
// NSString format works with NSObjects via %@, we should change standard format to %@
val objcFormat = string.replace(Regex("%((?:\\.|\\d|\\$)*)[abcdefs]"), "%$1@")
// bad but objc interop limited :(
// When calling variadic C functions spread operator is supported only for *arrayOf(...)
@Suppress("MagicNumber")
return when (args.size) {
0 -> NSString.stringWithFormat(objcFormat)
1 -> NSString.stringWithFormat(objcFormat, args[0])
2 -> NSString.stringWithFormat(objcFormat, args[0], args[1])
3 -> NSString.stringWithFormat(objcFormat, args[0], args[1], args[2])
4 -> NSString.stringWithFormat(objcFormat, args[0], args[1], args[2], args[3])
5 -> NSString.stringWithFormat(objcFormat, args[0], args[1], args[2], args[3], args[4])
6 -> NSString.stringWithFormat(
objcFormat,
args[0],
args[1],
args[2],
args[3],
args[4],
args[5]
)
7 -> NSString.stringWithFormat(
objcFormat,
args[0],
args[1],
args[2],
args[3],
args[4],
args[5],
args[6]
)
8 -> NSString.stringWithFormat(
objcFormat,
args[0],
args[1],
args[2],
args[3],
args[4],
args[5],
args[6],
args[7]
)
9 -> NSString.stringWithFormat(
objcFormat,
args[0],
args[1],
args[2],
args[3],
args[4],
args[5],
args[6],
args[7],
args[8]
)
else -> throw IllegalArgumentException("can't handle more then 9 arguments now")
}
}

View File

@@ -8,17 +8,17 @@ package ca.gosyer.jui.ui.util.lang
import androidx.compose.ui.text.intl.Locale
import ca.gosyer.jui.core.lang.toPlatform
import java.text.Collator as JvmCollator
import java.text.Collator
actual fun Collator(locale: Locale): Collator {
return Collator(JvmCollator.getInstance(locale.toPlatform()))
}
actual class CollatorComparator(private val collator: Collator) : Comparator<String> {
actual constructor(locale: Locale) : this(Collator.getInstance(locale.toPlatform()))
actual class Collator(private val jvmCollator: JvmCollator) {
init {
jvmCollator.strength = JvmCollator.PRIMARY
collator.strength = Collator.PRIMARY
}
actual fun compare(source: String, target: String): Int {
return jvmCollator.compare(source, target)
actual override fun compare(source: String, target: String): Int {
return collator.compare(source, target)
}
}

View File

@@ -11,6 +11,6 @@ import io.ktor.utils.io.jvm.javaio.toInputStream
import okio.Source
import okio.source
actual fun ByteReadChannel.toSource(): Source {
actual suspend fun ByteReadChannel.toSource(): Source {
return toInputStream().source()
}

View File

@@ -6,16 +6,8 @@
package ca.gosyer.jui.uicore.resources
import androidx.compose.runtime.Composable
import dev.icerock.moko.resources.PluralsResource
import dev.icerock.moko.resources.desc.Plural
import dev.icerock.moko.resources.desc.PluralFormatted
import dev.icerock.moko.resources.desc.StringDesc
import dev.icerock.moko.resources.desc.PluralFormattedStringDesc
import dev.icerock.moko.resources.desc.PluralStringDesc
@Composable
actual fun stringResource(resource: PluralsResource, quantity: Int): String =
StringDesc.Plural(resource, quantity).localized()
@Composable
actual fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String =
StringDesc.PluralFormatted(resource, quantity, *args).localized()
actual fun PluralStringDesc.localized(): String = localized()
actual fun PluralFormattedStringDesc.localized() = localized()

View File

@@ -7,11 +7,19 @@
package ca.gosyer.jui.uicore.resources
import androidx.compose.runtime.Composable
import dev.icerock.moko.resources.PluralsResource
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.Plural
import dev.icerock.moko.resources.desc.PluralFormatted
import dev.icerock.moko.resources.desc.PluralFormattedStringDesc
import dev.icerock.moko.resources.desc.PluralStringDesc
import dev.icerock.moko.resources.desc.Resource
import dev.icerock.moko.resources.desc.ResourceFormatted
import dev.icerock.moko.resources.desc.StringDesc
expect fun PluralStringDesc.localized(): String
expect fun PluralFormattedStringDesc.localized(): String
@Composable
actual fun stringResource(resource: StringResource): String =
StringDesc.Resource(resource).localized()
@@ -20,7 +28,6 @@ actual fun stringResource(resource: StringResource): String =
actual fun stringResource(resource: StringResource, vararg args: Any): String =
StringDesc.ResourceFormatted(resource, *args).localized()
/* TODO the commonizer chokes on .localized()
@Composable
actual fun stringResource(resource: PluralsResource, quantity: Int): String =
StringDesc.Plural(resource, quantity).localized()
@@ -28,4 +35,3 @@ actual fun stringResource(resource: PluralsResource, quantity: Int): String =
@Composable
actual fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String =
StringDesc.PluralFormatted(resource, quantity, *args).localized()
*/

View File

@@ -6,17 +6,28 @@
package ca.gosyer.jui.uicore.vm
import ca.gosyer.jui.core.lang.launchDefault
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc
import dev.icerock.moko.resources.format
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import me.tatarka.inject.annotations.Inject
actual class ContextWrapper @Inject constructor() {
private val _toasts = MutableSharedFlow<Pair<String, Length>>()
val toasts = _toasts.asSharedFlow()
actual fun toPlatformString(stringResource: StringResource): String {
return stringResource.localized()
return stringResource.desc().localized()
}
actual fun toPlatformString(stringResource: StringResource, vararg args: Any): String {
return stringResource.format(*args).localized()
}
actual fun toast(string: String, length: Length) {
GlobalScope.launchDefault {
_toasts.emit(string to length)
}
}
}

View File

@@ -6,16 +6,8 @@
package ca.gosyer.jui.uicore.resources
import androidx.compose.runtime.Composable
import dev.icerock.moko.resources.PluralsResource
import dev.icerock.moko.resources.desc.Plural
import dev.icerock.moko.resources.desc.PluralFormatted
import dev.icerock.moko.resources.desc.StringDesc
import dev.icerock.moko.resources.desc.PluralFormattedStringDesc
import dev.icerock.moko.resources.desc.PluralStringDesc
@Composable
actual fun stringResource(resource: PluralsResource, quantity: Int): String =
StringDesc.Plural(resource, quantity).localized()
@Composable
actual fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String =
StringDesc.PluralFormatted(resource, quantity, *args).localized()
actual fun PluralStringDesc.localized(): String = localized()
actual fun PluralFormattedStringDesc.localized() = localized()

View File

@@ -6,16 +6,8 @@
package ca.gosyer.jui.uicore.resources
import androidx.compose.runtime.Composable
import dev.icerock.moko.resources.PluralsResource
import dev.icerock.moko.resources.desc.Plural
import dev.icerock.moko.resources.desc.PluralFormatted
import dev.icerock.moko.resources.desc.StringDesc
import dev.icerock.moko.resources.desc.PluralFormattedStringDesc
import dev.icerock.moko.resources.desc.PluralStringDesc
@Composable
actual fun stringResource(resource: PluralsResource, quantity: Int): String =
StringDesc.Plural(resource, quantity).localized()
@Composable
actual fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String =
StringDesc.PluralFormatted(resource, quantity, *args).localized()
actual fun PluralStringDesc.localized(): String = localized()
actual fun PluralFormattedStringDesc.localized() = localized()