move models to commons

This commit is contained in:
nrobi144
2026-01-01 07:26:39 +02:00
parent f3b8589fea
commit eb47c19172
503 changed files with 1675 additions and 888 deletions
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.threading
import android.os.Looper
/**
* Android implementation of checkNotInMainThread.
* Uses Looper to detect if running on main thread.
*/
actual fun checkNotInMainThread() {
// BuildConfig check removed - commons doesn't have BuildConfig
// Enable this check in debug builds at the app level if needed
if (isMainThread()) {
throw OnMainThreadException("It should not be in the MainThread")
}
}
private fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.util
import java.text.NumberFormat
actual class PlatformNumberFormatter {
private val formatter = NumberFormat.getInstance()
actual fun format(value: Long): String = formatter.format(value)
}
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.model
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Request
import com.vitorpamplona.quartz.nip47WalletConnect.Response
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.utils.DualCase
/**
* Interface for NIP-47 wallet connect signer state.
* Used by Note.kt for checking NWC payment status.
*/
interface INwcSignerState {
suspend fun decryptResponse(event: LnZapPaymentResponseEvent): Response?
suspend fun decryptRequest(event: LnZapPaymentRequestEvent): Request?
fun isNIP47Author(pubKey: String?): Boolean
}
/**
* Interface for private zap decryption cache.
* Used by Note.kt for checking private zap status.
*/
interface IPrivateZapsDecryptionCache {
fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent?
suspend fun decryptPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent?
}
/**
* Hidden content settings for filtering notes.
* Used by Note.isHiddenFor() to check if content should be hidden.
*/
data class LiveHiddenUsers(
val showSensitiveContent: Boolean?,
val hiddenWordsCase: List<DualCase>,
val hiddenUsersHashCodes: Set<Int>,
val spammersHashCodes: Set<Int>,
)
/**
* Interface for account operations needed by Note.kt.
* Abstracts Android-specific Account class for use in commons.
*/
interface IAccount {
/** NIP-47 wallet connect state for payment verification */
val nip47SignerState: INwcSignerState
/** Private zaps decryption cache */
val privateZapsDecryptionCache: IPrivateZapsDecryptionCache
/** Current user's profile */
fun userProfile(): User
/** Whether account has write permissions */
fun isWriteable(): Boolean
/** Current user's public key */
val pubKey: String
/** Content filter settings */
val showSensitiveContent: Boolean?
val hiddenWordsCase: List<DualCase>
val hiddenUsersHashCodes: Set<Int>
val spammersHashCodes: Set<Int>
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.model.cache
import com.vitorpamplona.quartz.nip01Core.core.HexKey
/**
* Cache provider interface for accessing cached Notes, Users, and Channels.
*
* This abstraction allows Note and User models to access the cache without
* direct coupling to LocalCache singleton. Platform-specific implementations
* (Android LocalCache, Desktop DesktopLocalCache) implement this interface.
*
* Benefits:
* - Dependency injection instead of singleton coupling
* - Testable (can mock for unit tests)
* - Platform-agnostic model layer
*/
interface ICacheProvider {
/**
* Gets a channel by Note reference.
* Used for resolving relay hints for channel messages.
*
* @param note The note to look up channel for
* @return The channel if found, null otherwise
*/
fun getAnyChannel(note: Any?): IChannel?
/**
* Gets a User by public key hex.
* Used for updating follower counts and user relationships.
*
* @param pubkey The user's public key in hex format
* @return The User if exists in cache, null otherwise
*/
fun getUserIfExists(pubkey: HexKey): Any?
/**
* Counts users matching a predicate.
* Used for calculating follower counts.
*
* @param predicate Filter function for counting users
* @return Count of users matching the predicate
*/
fun countUsers(predicate: (String, Any) -> Boolean): Int
}
/**
* Minimal channel interface for relay resolution.
* Full channel implementations (PublicChatChannel, LiveActivitiesChannel)
* implement this interface.
*/
interface IChannel {
/**
* Gets the relay URLs for this channel.
* @return List of relay URLs or null if none configured
*/
fun relays(): List<Any>?
}
@@ -0,0 +1,109 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.model.nip56Reports
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.UserDependencies
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
class UserReportCache : UserDependencies {
val receivedReportsByAuthor = MutableStateFlow(mapOf<User, Set<Note>>())
fun addReport(note: Note) {
val author = note.author ?: return
val reportsBy = receivedReportsByAuthor.value[author]
// if it's already there, quick exit
if (reportsBy != null && reportsBy.contains(note)) return
receivedReportsByAuthor.update {
val author = note.author
if (author == null) {
it
} else {
val reportsByInner = it[author] ?: emptySet()
it + (author to reportsByInner + note)
}
}
}
fun removeReport(deleteNote: Note) {
val author = deleteNote.author ?: return
val reportsBy = receivedReportsByAuthor.value[author]
// if it's not already there, quick exit
if (reportsBy == null || !reportsBy.contains(deleteNote)) return
receivedReportsByAuthor.update {
val author = deleteNote.author
if (author == null) {
it
} else {
val reportsByInner = it[author]
if (reportsByInner == null) {
it
} else {
it + (author to reportsByInner - deleteNote)
}
}
}
}
fun reportsBy(user: User): Set<Note> = receivedReportsByAuthor.value[user] ?: emptySet()
fun count() = receivedReportsByAuthor.value.values.sumOf { it.size }
fun countFlow() =
receivedReportsByAuthor.map { reportPerAuthor ->
reportPerAuthor.values.sumOf { it.size }
}
fun countReportAuthorsBy(users: Set<HexKey>): Int = receivedReportsByAuthor.value.count { it.key.pubkeyHex in users }
fun all() = receivedReportsByAuthor.value.values.flatten()
fun reportsBy(users: Set<HexKey>): List<Note> =
receivedReportsByAuthor.value
.flatMap {
if (it.key.pubkeyHex in users) {
it.value
} else {
emptyList()
}
}
fun hasReport(
loggedIn: User,
type: ReportType,
): Boolean =
receivedReportsByAuthor.value[loggedIn]?.firstOrNull {
(it.event as? ReportEvent)?.reportedAuthor()?.any { it.type == type } ?: false
} != null
fun hasReportNewerThan(timeInSeconds: Long): Boolean = receivedReportsByAuthor.value.any { pair -> pair.value.firstOrNull { (it.createdAt() ?: 0L) > timeInSeconds } != null }
}
@@ -0,0 +1,150 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.model.trustedAssertions
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.UserDependencies
import com.vitorpamplona.amethyst.commons.util.PlatformNumberFormatter
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
class UserCardsCache : UserDependencies {
val receivedCards = MutableStateFlow(mapOf<User, AddressableNote>())
fun addCard(note: AddressableNote) {
val author = note.author ?: return
val cardBy = receivedCards.value[author]
// if it's already there, quick exit
if (cardBy != null && cardBy == note) return
receivedCards.update {
val author = note.author
if (author == null) {
it
} else {
it + (author to note)
}
}
}
fun removeCard(note: AddressableNote) {
val author = note.author ?: return
val cardBy = receivedCards.value[author]
// if it's not already there, quick exit
if (cardBy == null || cardBy != note) return
receivedCards.update {
val author = note.author
if (author == null) {
it
} else {
val reportsByInner = it[author]
if (reportsByInner == null) {
it
} else {
it - author
}
}
}
}
fun rankFlow(trustProviderList: TrustProviderListState) =
combineTransform(receivedCards, trustProviderList.liveUserRankProvider) { cards, provider ->
if (provider != null) {
val flow =
cards.firstNotNullOfOrNull {
if (it.key.pubkeyHex == provider.pubkey) {
it.value
.flow()
.metadata.stateFlow
} else {
null
}
}
if (flow != null) {
emitAll(flow)
} else {
emit(null)
}
} else {
emit(null)
}
}.map {
(it?.note?.event as? ContactCardEvent)?.rank()
}.flowOn(Dispatchers.IO)
private val formatter = PlatformNumberFormatter()
fun followerCountStrFlow(trustProviderList: TrustProviderListState) =
combineTransform(receivedCards, trustProviderList.liveUserFollowerCount) { cards, provider ->
if (provider != null) {
val flow =
cards.firstNotNullOfOrNull {
if (it.key.pubkeyHex == provider.pubkey) {
it.value
.flow()
.metadata.stateFlow
} else {
null
}
}
if (flow != null) {
emitAll(flow)
} else {
emit(null)
}
} else {
emit(null)
}
}.map {
val value = (it?.note?.event as? ContactCardEvent)?.followerCount()
if (value != null && value > 0) {
formatter.format(value.toLong())
} else {
"--"
}
}.flowOn(Dispatchers.IO)
}
// Placeholder for TrustProviderListState - will be properly extracted later
// For now, this allows UserCardsCache to compile
interface TrustProviderListState {
val liveUserRankProvider: kotlinx.coroutines.flow.StateFlow<ServiceProviderTag?>
val liveUserFollowerCount: kotlinx.coroutines.flow.StateFlow<ServiceProviderTag?>
}
// Placeholder for ServiceProviderTag
interface ServiceProviderTag {
val pubkey: String
}
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.threading
/**
* Checks that the current code is not running on the main thread.
* Throws an exception in debug builds if called from the main thread.
*
* Platform-specific: Android uses Looper, Desktop may use different mechanism or no-op.
*/
expect fun checkNotInMainThread()
/**
* Exception thrown when code expected to run on a background thread is executed on the main thread.
*/
class OnMainThreadException(
str: String,
) : RuntimeException(str)
@@ -0,0 +1,134 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.util
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2
fun String.firstFullCharOld(): String {
return when (this.length) {
0,
1,
-> return this
2,
3,
-> return if (isUTF16Char(0)) this.take(2) else this.take(1)
else -> {
val first = isUTF16Char(0)
val second = isUTF16Char(2)
if (first && second) {
this.take(4)
} else if (first) {
this.take(2)
} else {
this.take(1)
}
}
}
}
fun String.firstFullChar(): String {
var isInJoin = false
var hasHadSecondChance = false
var start = 0
var previousCharLength = 0
var next: Int
var codePoint: Int
var i = 0
while (i < this.length) {
codePoint = codePointAt(i)
// Skips if it starts with the join char 0x200D
if (codePoint == 0x200D && previousCharLength == 0) {
next = offsetByCodePoints(i, 1)
start = next
} else {
// If join, searches for the next char
if (codePoint == 0xFE0F) {
} else if (codePoint == 0x200D) {
isInJoin = true
} else {
// stops when two chars are not joined together
if (previousCharLength > 0 && !isInJoin) {
if (Character.charCount(codePoint) == 1 || hasHadSecondChance) {
break
} else {
hasHadSecondChance = true
}
} else {
hasHadSecondChance = false
}
isInJoin = false
}
// next char to evaluate
next = offsetByCodePoints(i, 1)
previousCharLength += (next - i)
}
i = next
}
// if ends in join, then seachers backwards until a char is found.
if (isInJoin) {
i = previousCharLength - 1
while (i > 0) {
if (this[i].code == 0x200D) {
previousCharLength -= 1
} else {
break
}
i -= 1
}
}
return substring(start, start + previousCharLength)
}
fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists<String>): String {
if (length <= 2) {
return firstFullChar()
}
if (this[0] == ':') {
// makes sure an emoji exists
val emojiParts = this.split(":", limit = 3)
if (emojiParts.size >= 2) {
val emojiName = emojiParts[1]
val emojiUrl = tags.lists.firstOrNull { it.size > 1 && it[1] == emojiName }?.getOrNull(2)
if (emojiUrl != null) {
return ":$emojiName:$emojiUrl"
}
}
}
if (EmojiCoder.isCoded(this)) {
return EmojiCoder.cropToFirstMessage(this)
}
return firstFullChar()
}
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.util
fun <T> Iterable<T>.replace(
old: T,
new: T,
): List<T> = map { if (it == old) new else it }
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.util
/**
* Platform-agnostic number formatter.
* Uses NumberFormat on JVM platforms, custom formatting elsewhere.
*/
expect class PlatformNumberFormatter() {
fun format(value: Long): String
}
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.threading
/**
* Desktop JVM implementation of checkNotInMainThread.
* Currently a no-op as Desktop Compose doesn't have the same main thread restrictions as Android.
* Could be enhanced to check Swing EDT if needed.
*/
actual fun checkNotInMainThread() {
// No-op for Desktop - different threading model
// Could check for Swing EDT with: javax.swing.SwingUtilities.isEventDispatchThread()
// but Compose Desktop doesn't have the same restrictions as Android
}
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.util
import java.text.NumberFormat
actual class PlatformNumberFormatter {
private val formatter = NumberFormat.getInstance()
actual fun format(value: Long): String = formatter.format(value)
}