mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f13a76870 | ||
|
|
adaab4ced8 | ||
|
|
5a2e149fa0 | ||
|
|
560c1f797c | ||
|
|
2d5be92f34 | ||
|
|
e9285fdac0 | ||
|
|
4f12c127af | ||
|
|
be7522382f | ||
|
|
987fb02be1 | ||
|
|
bbd8a34f44 | ||
|
|
720ebfd0ea | ||
|
|
159228972c | ||
|
|
2a65944eeb | ||
|
|
e7902c4565 | ||
|
|
f12588ef7c | ||
|
|
5fc197a40b | ||
|
|
64608f771e | ||
|
|
a9cebaf3d9 | ||
|
|
6c33dae498 | ||
|
|
e42d2dca04 | ||
|
|
35cd9cb6c5 | ||
|
|
2ce16325a1 | ||
|
|
275aeb7833 | ||
|
|
34737be5eb | ||
|
|
6fb5c873cc | ||
|
|
e0ffba5295 | ||
|
|
82f03e0769 | ||
|
|
b223cc4028 | ||
|
|
550a9abdbb | ||
|
|
a121d1b301 | ||
|
|
e794ff44a1 | ||
|
|
a22b6db130 | ||
|
|
1f7fc236db | ||
|
|
1edaea7905 | ||
|
|
e8cae710c7 | ||
|
|
489d92f9bb | ||
|
|
4d4c568afc | ||
|
|
e452c33a98 | ||
|
|
fdfd875580 | ||
|
|
a6e8ee20ae | ||
|
|
065dba2431 | ||
|
|
5f581c2f5e | ||
|
|
f7bfb68e7c | ||
|
|
2371e0678a | ||
|
|
97c78aaa3c | ||
|
|
9026c877ac | ||
|
|
238ade9ccb | ||
|
|
016bd1ed81 | ||
|
|
ed21d4cafb | ||
|
|
ecc0f39f7d | ||
|
|
26846fa91a | ||
|
|
fe3a0568f2 | ||
|
|
56252d33f1 | ||
|
|
fccfb2aaba | ||
|
|
7b55b69c12 | ||
|
|
23fad2a818 | ||
|
|
2dc23b601a | ||
|
|
504980d263 | ||
|
|
7cf7c9f082 | ||
|
|
dd8904de15 | ||
|
|
d1c43f8147 | ||
|
|
68506fd6af | ||
|
|
b795ca06d1 | ||
|
|
8695acb742 | ||
|
|
ed491f5e3a | ||
|
|
3a76c2a2de | ||
|
|
91b3b9e0e9 | ||
|
|
fb280650a5 | ||
|
|
e4baebf2f9 | ||
|
|
1a33da2537 | ||
|
|
3734ee58ea | ||
|
|
9554b36f80 | ||
|
|
5225c6ad07 | ||
|
|
c7be0e4a95 | ||
|
|
d4f060d509 | ||
|
|
9ba9ddee50 | ||
|
|
7bbdd5e877 | ||
|
|
6cc65ce84a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/androidTestResultsUserPreferences.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/appInsightsSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
137
README.md
137
README.md
@@ -18,87 +18,88 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
|
||||
# Current Features
|
||||
|
||||
- [x] Event Builders / WebSocket Subscriptions (NIP-01)
|
||||
- [x] Home Feed
|
||||
- [x] Notifications Feed
|
||||
- [x] Global Feed
|
||||
- [x] Replies and mentions (NIP-10)
|
||||
- [x] Reactions (NIP-25)
|
||||
- [x] Reposts (NIP-18)
|
||||
- [x] Image/Video/Url/LnInvoice Previews
|
||||
- [x] View Threads
|
||||
- [x] Events / Relay Subscriptions (NIP-01)
|
||||
- [x] Follow List (NIP-02)
|
||||
- [ ] OpenTimestamps Attestations (NIP-03)
|
||||
- [x] Private Messages (NIP-04)
|
||||
- [x] User Profiles (edit/follow/unfollow - NIP-02)
|
||||
- [x] Public Chats (NIP-28)
|
||||
- [x] Bech Encoding support (NIP-19)
|
||||
- [x] Notification Dots
|
||||
- [x] Reporting and Hide User capability (NIP-56)
|
||||
- [x] Automatic Translations
|
||||
- [x] Relay Sets (home, dms, public chats, global)
|
||||
- [x] User/Note Tagging (NIP-08, NIP-10)
|
||||
- [x] Lightning Tips
|
||||
- [x] Zaps (private, public, anon, non-zap) (NIP-57)
|
||||
- [x] URI Support (NIP-21)
|
||||
- [x] Created_at Limits (NIP-22)
|
||||
- [x] DNS Address (NIP-05)
|
||||
- [ ] Mnemonic seed phrase (NIP-06)
|
||||
- [ ] WebBrowser Signer (NIP-07, Not applicable)
|
||||
- [x] Old-style mentions (NIP-08)
|
||||
- [x] Event Deletion (NIP-09)
|
||||
- [x] Nostr Address (NIP-05)
|
||||
- [x] Replies, mentions, Threads and Notifications (NIP-10)
|
||||
- [x] Relay Information Document (NIP-11)
|
||||
- [x] Generic Tag Queries (NIP-12)
|
||||
- [x] Proof of Work Display (NIP-13)
|
||||
- [ ] Proof of Work Calculations (NIP-13)
|
||||
- [x] Events with a Subject (NIP-14)
|
||||
- [ ] Marketplace (NIP-15)
|
||||
- [x] Event Treatment (NIP-16)
|
||||
- [x] Image/Video/Url/LnInvoice Previews
|
||||
- [x] Reposts, Quotes, Generic Reposts (NIP-18)
|
||||
- [x] Bech Encoding support (NIP-19)
|
||||
- [x] Command Results (NIP-20)
|
||||
- [x] URI Support (NIP-21)
|
||||
- [x] Long-form Content (NIP-23)
|
||||
- [x] User Profile Fields / Relay list (NIP-24)
|
||||
- [x] Reactions (NIP-25)
|
||||
- [ ] Delegated Event Signing (NIP-26, Will not implement)
|
||||
- [x] Text Note References (NIP-27)
|
||||
- [x] Public Chats (NIP-28)
|
||||
- [x] Custom Emoji (NIP-30)
|
||||
- [x] Event kind summaries (NIP-31)
|
||||
- [ ] Labeling (NIP-32)
|
||||
- [x] Parameterized Replaceable Events (NIP-33)
|
||||
- [x] Online Relay Search (NIP-50)
|
||||
- [x] Internationalization
|
||||
- [x] Badges (NIP-58)
|
||||
- [x] Hashtag Following and Custom Hashtags
|
||||
- [x] Polls (NIP-69)
|
||||
- [x] Verifiable static content in URLs (NIP-94)
|
||||
- [x] Login with QR
|
||||
- [x] Sensitive Content (NIP-36)
|
||||
- [x] User Status Event (NIP-38)
|
||||
- [x] External Identities (NIP-39)
|
||||
- [x] Expiration Support (NIP-40)
|
||||
- [x] Relay Authentication (NIP-42)
|
||||
- [ ] Event Counts (NIP-45, Will not implement)
|
||||
- [ ] Nostr Connect (NIP-46)
|
||||
- [x] Wallet Connect API (NIP-47)
|
||||
- [x] Accessible uploads
|
||||
- [ ] Proxy Tags (NIP-48, Not applicable)
|
||||
- [x] Online Relay Search (NIP-50)
|
||||
- [x] Lists (NIP-51)
|
||||
- [ ] Calendar Events (NIP-52)
|
||||
- [x] Live Activities & Live Chats (NIP-53)
|
||||
- [x] Inline Metadata (NIP-55 - Draft)
|
||||
- [x] Reporting (NIP-56)
|
||||
- [x] Lightning Tips
|
||||
- [x] Zaps (NIP-57)
|
||||
- [x] Private Zaps
|
||||
- [x] Zap Splits (NIP-57)
|
||||
- [x] Zapraiser (NIP-TBD)
|
||||
- [x] Badges (NIP-58)
|
||||
- [ ] Relay List Metadata (NIP-65)
|
||||
- [x] Polls (NIP-69)
|
||||
- [x] Moderated Communities (NIP-72)
|
||||
- [ ] Zap Goals (NIP-75)
|
||||
- [ ] Arbitrary Custom App Data (NIP-78)
|
||||
- [x] Highlights (NIP-84)
|
||||
- [x] Recommended Application Handlers (NIP-89)
|
||||
- [ ] Data Vending Machine (NIP-90)
|
||||
- [x] Verifiable file URLs (NIP-94)
|
||||
- [x] Binary Blobs (NIP-95)
|
||||
- [x] HTTP File Storage Integration (NIP-96 Draft)
|
||||
- [x] HTTP Auth (NIP-98)
|
||||
- [x] Classifieds (NIP-99)
|
||||
- [x] Private Messages and Small Groups (NIP-24/Draft)
|
||||
- [x] Gift Wraps & Seals (NIP-59/Draft)
|
||||
- [x] Versioned Encrypted Payloads (NIP-44/Draft)
|
||||
- [x] Audio Tracks (zapstr.live) (Kind:31337)
|
||||
- [x] Push Notifications (Google and Unified Push)
|
||||
- [x] In-Device Automatic Translations
|
||||
- [x] Hashtag Following and Custom Hashtags
|
||||
- [x] Login with QR
|
||||
- [x] Bounty support (nostrbounties.com)
|
||||
- [x] De-googled F-Droid flavor
|
||||
- [x] External Identity Support (NIP-39)
|
||||
- [x] Multiple Accounts
|
||||
- [x] Markdown Support
|
||||
- [x] Relay Authentication (NIP-42)
|
||||
- [x] Content stored in relays themselves (NIP-95)
|
||||
- [x] Custom Emoji (NIP-30)
|
||||
- [x] Zap Forwarding (NIP-57)
|
||||
- [x] Text Note References (NIP-27)
|
||||
- [x] Audio Tracks (zapstr.live) (Kind:31337)
|
||||
- [x] Push Notifications (Zaps and Messages)
|
||||
- [x] Generic Tags (NIP-12)
|
||||
- [x] Sensitive Content (NIP-36)
|
||||
- [x] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
|
||||
- [x] Recommended Application Handlers (NIP-89)
|
||||
- [x] Events with a Subject (NIP-14)
|
||||
- [x] Generic Reposts (kind:16)
|
||||
- [x] Live Activities & Live Chats (NIP-53)
|
||||
- [x] Relay Pages (NIP-11)
|
||||
- [x] HTTP Auth (NIP-98)
|
||||
- [x] Zapraiser (NIP-TBD)
|
||||
- [x] Moderated Communities (NIP-72)
|
||||
- [x] Emoji Packs (Kind:30030)
|
||||
- [x] Personal Emoji Lists (Kind:10030)
|
||||
- [x] Classifieds (Kind:30402)
|
||||
- [x] Private Messages and Small Groups (NIP-24)
|
||||
- [x] Gift Wraps & Seals (NIP-59)
|
||||
- [x] Versioned Encrypted Payloads (NIP-44)
|
||||
- [x] Expiration Support (NIP-40)
|
||||
- [x] Status Event (NIP-38)
|
||||
- [ ] Marketplace (NIP-15)
|
||||
- [ ] Image/Video Capture in the app
|
||||
- [ ] Local Database
|
||||
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
|
||||
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
|
||||
- [ ] Workspaces
|
||||
- [ ] Infinity Scroll
|
||||
- [ ] Relay List Metadata (NIP-65)
|
||||
- [ ] Signing Requests (NIP-46)
|
||||
- [ ] Delegated Event Signing (NIP-26)
|
||||
- [ ] Account Creation / Backup Guidance
|
||||
- [ ] Mnemonic seed phrase (NIP-06)
|
||||
- [ ] Message Sent feedback (NIP-20)
|
||||
- [ ] OpenTimestamps Attestations (NIP-03)
|
||||
|
||||
|
||||
# Development Overview
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 339
|
||||
versionName "0.82.2"
|
||||
versionCode 344
|
||||
versionName "0.83.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -134,6 +134,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
@@ -158,7 +159,7 @@ android {
|
||||
dependencies {
|
||||
implementation project(path: ':quartz')
|
||||
implementation "androidx.core:core-ktx:$core_ktx_version"
|
||||
implementation 'androidx.activity:activity-compose:1.8.1'
|
||||
implementation 'androidx.activity:activity-compose:1.8.2'
|
||||
implementation "androidx.compose.ui:ui:$compose_ui_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
|
||||
|
||||
@@ -193,7 +194,7 @@ dependencies {
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
|
||||
// Websockets API
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
||||
|
||||
// HTML Parsing for Link Preview
|
||||
implementation 'org.jsoup:jsoup:1.17.1'
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.Nip96Retriever
|
||||
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
|
||||
import com.vitorpamplona.amethyst.ui.actions.Nip96Uploader
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertTrue
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -9,8 +10,13 @@ import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ThreadAssemblerTest {
|
||||
val db = """
|
||||
[
|
||||
@@ -85,7 +91,7 @@ class ThreadAssemblerTest {
|
||||
""".trimIndent()
|
||||
|
||||
@Test
|
||||
fun threadOrderTest() {
|
||||
fun threadOrderTest() = runBlocking {
|
||||
val eventArray = Event.mapper.readValue<ArrayList<Event>>(db) as List<Event> + Event.fromJson(header)
|
||||
|
||||
var counter = 0
|
||||
@@ -97,7 +103,12 @@ class ThreadAssemblerTest {
|
||||
|
||||
val naddr = ATag(30023, "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", "1680612926599", null)
|
||||
|
||||
val filter = ThreadFeedFilter(Account(KeyPair()), naddr.toTag())
|
||||
val account = Account(KeyPair())
|
||||
withContext(Dispatchers.Main) {
|
||||
val user = account.userProfile().live()
|
||||
}
|
||||
|
||||
val filter = ThreadFeedFilter(account, naddr.toTag())
|
||||
val calculatedFeed = filter.feed()
|
||||
|
||||
val expecteedOrder = listOf(
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
object OptOutFromFilters {
|
||||
var warnAboutPostsWithReports: Boolean = true
|
||||
var filterSpamFromStrangers: Boolean = true
|
||||
|
||||
fun start(warnAboutReports: Boolean, filterSpam: Boolean) {
|
||||
warnAboutPostsWithReports = warnAboutReports
|
||||
filterSpamFromStrangers = filterSpam
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class ServiceManager {
|
||||
|
||||
// Resets Proxy Use
|
||||
HttpClient.start(account?.proxy)
|
||||
OptOutFromFilters.start(account?.warnAboutPostsWithReports ?: true, account?.filterSpamFromStrangers ?: true)
|
||||
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
|
||||
Coil.setImageLoader {
|
||||
Amethyst.instance.imageLoaderBuilder().components {
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.switchMap
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.OptOutFromFilters
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||
@@ -103,7 +102,7 @@ val DefaultChannels = setOf(
|
||||
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group
|
||||
)
|
||||
|
||||
val DefaultReactions = listOf("\uD83D\uDE80", "\uD83E\uDEC2", "\uD83D\uDC40", "\uD83D\uDE02")
|
||||
val DefaultReactions = listOf("\uD83D\uDE80", "\uD83E\uDEC2", "\uD83D\uDC40", "\uD83D\uDE02", "\uD83C\uDF89", "\uD83E\uDD14", "\uD83D\uDE31")
|
||||
|
||||
val DefaultZapAmounts = listOf(500L, 1000L, 5000L)
|
||||
|
||||
@@ -426,7 +425,7 @@ class Account(
|
||||
fun updateOptOutOptions(warnReports: Boolean, filterSpam: Boolean) {
|
||||
warnAboutPostsWithReports = warnReports
|
||||
filterSpamFromStrangers = filterSpam
|
||||
OptOutFromFilters.start(warnAboutPostsWithReports, filterSpamFromStrangers)
|
||||
LocalCache.antiSpam.active = filterSpamFromStrangers
|
||||
if (!filterSpamFromStrangers) {
|
||||
transientHiddenUsers = persistentSetOf()
|
||||
}
|
||||
@@ -477,10 +476,10 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendNewUserMetadata(toString: String, identities: List<IdentityClaim>) {
|
||||
suspend fun sendNewUserMetadata(toString: String, newName: String, identities: List<IdentityClaim>) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
MetadataEvent.create(toString, identities, signer) {
|
||||
MetadataEvent.create(toString, newName, identities, signer) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.OptOutFromFilters
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
@@ -19,10 +18,12 @@ class AntiSpamFilter {
|
||||
val recentMessages = LruCache<Int, String>(1000)
|
||||
val spamMessages = LruCache<Int, Spammer>(1000)
|
||||
|
||||
var active: Boolean = true
|
||||
|
||||
fun isSpam(event: Event, relay: Relay?): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (!OptOutFromFilters.filterSpamFromStrangers) return false
|
||||
if (!active) return false
|
||||
|
||||
val idHex = event.id
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
@@ -41,7 +42,6 @@ import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.WrappedEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.math.BigDecimal
|
||||
@@ -93,7 +93,7 @@ open class Note(val idHex: String) {
|
||||
var zapPayments = mapOf<Note, Note?>()
|
||||
private set
|
||||
|
||||
var relays = listOf<String>()
|
||||
var relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
||||
private set
|
||||
|
||||
var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()
|
||||
@@ -110,13 +110,13 @@ open class Note(val idHex: String) {
|
||||
host.id,
|
||||
host.pubKey,
|
||||
host.kind(),
|
||||
relays.firstOrNull()
|
||||
relays.firstOrNull()?.url
|
||||
)
|
||||
} else {
|
||||
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
|
||||
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
|
||||
}
|
||||
} else {
|
||||
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
|
||||
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ open class Note(val idHex: String) {
|
||||
zaps = mapOf<Note, Note?>()
|
||||
zapPayments = mapOf<Note, Note?>()
|
||||
zapsAmount = BigDecimal.ZERO
|
||||
relays = listOf<String>()
|
||||
relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
||||
lastReactionsDownloadTime = emptyMap()
|
||||
|
||||
liveSet?.innerReplies?.invalidateData()
|
||||
@@ -287,7 +287,7 @@ open class Note(val idHex: String) {
|
||||
val tags = note.event?.tags() ?: emptyArray()
|
||||
val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"
|
||||
|
||||
if (reaction in reactions.keys && reactions[reaction]?.contains(note) == true) {
|
||||
if (reactions[reaction]?.contains(note) == true) {
|
||||
reactions[reaction]?.let {
|
||||
if (note in it) {
|
||||
val newList = it.minus(note)
|
||||
@@ -306,7 +306,7 @@ open class Note(val idHex: String) {
|
||||
fun removeReport(deleteNote: Note) {
|
||||
val author = deleteNote.author ?: return
|
||||
|
||||
if (author in reports.keys && reports[author]?.contains(deleteNote) == true) {
|
||||
if (reports[author]?.contains(deleteNote) == true) {
|
||||
reports[author]?.let {
|
||||
reports = reports + Pair(author, it.minus(deleteNote))
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
@@ -327,12 +327,11 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun removeZapPayment(note: Note) {
|
||||
if (zapPayments[note] != null) {
|
||||
if (zapPayments.containsKey(note)) {
|
||||
zapPayments = zapPayments.minus(note)
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
} else if (zapPayments.containsValue(note)) {
|
||||
val toRemove = zapPayments.filterValues { it == note }
|
||||
zapPayments = zapPayments.minus(toRemove.keys)
|
||||
zapPayments = zapPayments.filterValues { it != note }
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -346,10 +345,7 @@ open class Note(val idHex: String) {
|
||||
|
||||
@Synchronized
|
||||
private fun innerAddZap(zapRequest: Note, zap: Note?): Boolean {
|
||||
if (zapRequest !in zaps.keys) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
return true
|
||||
} else if (zaps[zapRequest] == null) {
|
||||
if (zaps[zapRequest] == null) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
return true
|
||||
}
|
||||
@@ -359,13 +355,8 @@ open class Note(val idHex: String) {
|
||||
|
||||
fun addZap(zapRequest: Note, zap: Note?) {
|
||||
checkNotInMainThread()
|
||||
if (zapRequest !in zaps.keys) {
|
||||
val inserted = innerAddZap(zapRequest, zap)
|
||||
if (inserted) {
|
||||
updateZapTotal()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
} else if (zaps[zapRequest] == null) {
|
||||
|
||||
if (zaps[zapRequest] == null) {
|
||||
val inserted = innerAddZap(zapRequest, zap)
|
||||
if (inserted) {
|
||||
updateZapTotal()
|
||||
@@ -376,10 +367,7 @@ open class Note(val idHex: String) {
|
||||
|
||||
@Synchronized
|
||||
private fun innerAddZapPayment(zapPaymentRequest: Note, zapPayment: Note?): Boolean {
|
||||
if (zapPaymentRequest !in zapPayments.keys) {
|
||||
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
|
||||
return true
|
||||
} else if (zapPayments[zapPaymentRequest] == null) {
|
||||
if (zapPayments[zapPaymentRequest] == null) {
|
||||
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
|
||||
return true
|
||||
}
|
||||
@@ -389,12 +377,7 @@ open class Note(val idHex: String) {
|
||||
|
||||
fun addZapPayment(zapPaymentRequest: Note, zapPayment: Note?) {
|
||||
checkNotInMainThread()
|
||||
if (zapPaymentRequest !in zapPayments.keys) {
|
||||
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
|
||||
if (inserted) {
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
} else if (zapPayments[zapPaymentRequest] == null) {
|
||||
if (zapPayments[zapPaymentRequest] == null) {
|
||||
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
|
||||
if (inserted) {
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
@@ -406,11 +389,12 @@ open class Note(val idHex: String) {
|
||||
val tags = note.event?.tags() ?: emptyArray()
|
||||
val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"
|
||||
|
||||
if (reaction !in reactions.keys) {
|
||||
val listOfAuthors = reactions[reaction]
|
||||
if (listOfAuthors == null) {
|
||||
reactions = reactions + Pair(reaction, listOf(note))
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
} else if (reactions[reaction]?.contains(note) == false) {
|
||||
reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note)
|
||||
} else if (!listOfAuthors.contains(note)) {
|
||||
reactions = reactions + Pair(reaction, listOfAuthors + note)
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -418,25 +402,27 @@ open class Note(val idHex: String) {
|
||||
fun addReport(note: Note) {
|
||||
val author = note.author ?: return
|
||||
|
||||
if (author !in reports.keys) {
|
||||
val reportsByAuthor = reports[author]
|
||||
|
||||
if (reportsByAuthor == null) {
|
||||
reports = reports + Pair(author, listOf(note))
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
} else if (reports[author]?.contains(note) == false) {
|
||||
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
|
||||
} else if (!reportsByAuthor.contains(note)) {
|
||||
reports = reports + Pair(author, reportsByAuthor + note)
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addRelaySync(url: String) {
|
||||
if (url !in relays) {
|
||||
relays = relays + url
|
||||
fun addRelaySync(briefInfo: RelayBriefInfoCache.RelayBriefInfo) {
|
||||
if (briefInfo !in relays) {
|
||||
relays = relays + briefInfo
|
||||
}
|
||||
}
|
||||
|
||||
fun addRelay(relay: Relay) {
|
||||
if (relay.url !in relays) {
|
||||
addRelaySync(relay.url)
|
||||
if (relay.brief !in relays) {
|
||||
addRelaySync(relay.brief)
|
||||
liveSet?.innerRelays?.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -523,17 +509,17 @@ open class Note(val idHex: String) {
|
||||
return reports[user]?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
fun reportAuthorsBy(users: Set<HexKey>): List<User> {
|
||||
return reports.keys.filter { it.pubkeyHex in users }
|
||||
}
|
||||
|
||||
fun countReportAuthorsBy(users: Set<HexKey>): Int {
|
||||
return reports.keys.count { it.pubkeyHex in users }
|
||||
return reports.count { it.key.pubkeyHex in users }
|
||||
}
|
||||
|
||||
fun reportsBy(users: Set<HexKey>): List<Note> {
|
||||
return reportAuthorsBy(users).mapNotNull {
|
||||
reports[it]
|
||||
return reports.mapNotNull {
|
||||
if (it.key.pubkeyHex in users) {
|
||||
it.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
@@ -541,8 +527,8 @@ open class Note(val idHex: String) {
|
||||
var sumOfAmounts = BigDecimal.ZERO
|
||||
|
||||
// Regular Zap Receipts
|
||||
zaps.values.forEach {
|
||||
val noteEvent = it?.event
|
||||
zaps.forEach {
|
||||
val noteEvent = it?.value?.event
|
||||
if (noteEvent is LnZapEvent) {
|
||||
sumOfAmounts += noteEvent.amount ?: BigDecimal.ZERO
|
||||
}
|
||||
@@ -647,8 +633,8 @@ open class Note(val idHex: String) {
|
||||
val dayAgo = TimeUtils.oneDayAgo()
|
||||
return reports.isNotEmpty() ||
|
||||
(
|
||||
author?.reports?.values?.any {
|
||||
it.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null
|
||||
author?.reports?.any {
|
||||
it.value.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null
|
||||
} ?: false
|
||||
)
|
||||
}
|
||||
@@ -863,21 +849,17 @@ class NoteLiveSet(u: Note) {
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val reactionCount = innerReactions.map {
|
||||
it.note.reactions.values.sumOf { it.size }
|
||||
var total = 0
|
||||
it.note.reactions.forEach { total += it.value.size }
|
||||
total
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val boostCount = innerBoosts.map {
|
||||
it.note.boosts.size
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val boostList = innerBoosts.map {
|
||||
it.note.boosts.toImmutableList()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val relayInfo = innerRelays.map {
|
||||
it.note.relays.map {
|
||||
RelayBriefInfo(it)
|
||||
}.toImmutableList()
|
||||
it.note.relays
|
||||
}
|
||||
|
||||
val content = innerMetadata.map {
|
||||
@@ -897,8 +879,7 @@ class NoteLiveSet(u: Note) {
|
||||
hasReactions.hasObservers() ||
|
||||
replyCount.hasObservers() ||
|
||||
reactionCount.hasObservers() ||
|
||||
boostCount.hasObservers() ||
|
||||
boostList.hasObservers()
|
||||
boostCount.hasObservers()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
@@ -986,9 +967,22 @@ class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveDat
|
||||
@Immutable
|
||||
class NoteState(val note: Note)
|
||||
|
||||
@Immutable
|
||||
data class RelayBriefInfo(
|
||||
val url: String,
|
||||
val displayUrl: String = url.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/").intern(),
|
||||
val favIcon: String = "https://$displayUrl/favicon.ico".intern()
|
||||
)
|
||||
object RelayBriefInfoCache {
|
||||
val cache = LruCache<String, RelayBriefInfo?>(50)
|
||||
|
||||
@Immutable
|
||||
data class RelayBriefInfo(
|
||||
val url: String,
|
||||
val displayUrl: String = url.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/").intern(),
|
||||
val favIcon: String = "https://$displayUrl/favicon.ico".intern()
|
||||
)
|
||||
|
||||
fun get(url: String): RelayBriefInfo {
|
||||
val info = cache[url]
|
||||
if (info != null) return info
|
||||
|
||||
val newInfo = RelayBriefInfo(url)
|
||||
cache.put(url, newInfo)
|
||||
return newInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ data class RelaySetupInfo(
|
||||
val feedTypes: Set<FeedType>,
|
||||
val paidRelay: Boolean = false
|
||||
) {
|
||||
val briefInfo: RelayBriefInfo = RelayBriefInfo(url)
|
||||
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ object UrlCachedPreviewer {
|
||||
}
|
||||
}
|
||||
|
||||
val state = if (urlInfo.allFetchComplete() && urlInfo.url == url) {
|
||||
val state = if (urlInfo.fetchComplete() && urlInfo.url == url) {
|
||||
UrlPreviewState.Loaded(urlInfo)
|
||||
} else {
|
||||
UrlPreviewState.Empty
|
||||
|
||||
@@ -130,11 +130,12 @@ class User(val pubkeyHex: String) {
|
||||
fun addReport(note: Note) {
|
||||
val author = note.author ?: return
|
||||
|
||||
if (author !in reports.keys) {
|
||||
val reportsBy = reports[author]
|
||||
if (reportsBy == null) {
|
||||
reports = reports + Pair(author, setOf(note))
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
} else if (reports[author]?.contains(note) == false) {
|
||||
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
|
||||
} else if (!reportsBy.contains(note)) {
|
||||
reports = reports + Pair(author, reportsBy + note)
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -142,7 +143,7 @@ class User(val pubkeyHex: String) {
|
||||
fun removeReport(deleteNote: Note) {
|
||||
val author = deleteNote.author ?: return
|
||||
|
||||
if (author in reports.keys && reports[author]?.contains(deleteNote) == true) {
|
||||
if (reports[author]?.contains(deleteNote) == true) {
|
||||
reports[author]?.let {
|
||||
reports = reports + Pair(author, it.minus(deleteNote))
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
@@ -151,48 +152,49 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
|
||||
fun addZap(zapRequest: Note, zap: Note?) {
|
||||
if (zapRequest !in zaps.keys) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
} else if (zapRequest in zaps.keys && zaps[zapRequest] == null) {
|
||||
if (zaps[zapRequest] == null) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeZap(zapRequestOrZapEvent: Note) {
|
||||
if (zapRequestOrZapEvent in zaps.keys) {
|
||||
if (zaps.containsKey(zapRequestOrZapEvent)) {
|
||||
zaps = zaps.minus(zapRequestOrZapEvent)
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
} else if (zapRequestOrZapEvent in zaps.values) {
|
||||
} else if (zaps.containsValue(zapRequestOrZapEvent)) {
|
||||
zaps = zaps.filter { it.value != zapRequestOrZapEvent }
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun zappedAmount(): BigDecimal {
|
||||
return zaps.mapNotNull { it.value?.event }
|
||||
.filterIsInstance<LnZapEvent>()
|
||||
.mapNotNull {
|
||||
it.amount
|
||||
}.sumOf { it }
|
||||
var amount = BigDecimal.ZERO
|
||||
zaps.forEach {
|
||||
val itemValue = (it.value?.event as? LnZapEvent)?.amount
|
||||
if (itemValue != null) {
|
||||
amount += itemValue
|
||||
}
|
||||
}
|
||||
|
||||
return amount
|
||||
}
|
||||
|
||||
fun reportsBy(user: User): Set<Note> {
|
||||
return reports[user] ?: emptySet()
|
||||
}
|
||||
|
||||
fun reportAuthorsBy(users: Set<HexKey>): List<User> {
|
||||
return reports.keys.filter { it.pubkeyHex in users }
|
||||
}
|
||||
|
||||
fun countReportAuthorsBy(users: Set<HexKey>): Int {
|
||||
return reports.keys.count { it.pubkeyHex in users }
|
||||
return reports.count { it.key.pubkeyHex in users }
|
||||
}
|
||||
|
||||
fun reportsBy(users: Set<HexKey>): List<Note> {
|
||||
return reportAuthorsBy(users).mapNotNull {
|
||||
reports[it]
|
||||
return reports.mapNotNull {
|
||||
if (it.key.pubkeyHex in users) {
|
||||
it.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
@@ -314,7 +316,7 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
|
||||
suspend fun transientFollowerCount(): Int {
|
||||
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
return LocalCache.users.count { it.value.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
}
|
||||
|
||||
fun cachedFollowingKeySet(): Set<HexKey> {
|
||||
@@ -338,7 +340,7 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
|
||||
suspend fun cachedFollowerCount(): Int {
|
||||
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
return LocalCache.users.count { it.value.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
}
|
||||
|
||||
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
|
||||
|
||||
@@ -28,7 +28,7 @@ class FileHeader(
|
||||
mimeType: String?,
|
||||
dimPrecomputed: String?,
|
||||
onReady: (FileHeader) -> Unit,
|
||||
onError: () -> Unit
|
||||
onError: (String?) -> Unit
|
||||
) {
|
||||
try {
|
||||
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
|
||||
@@ -36,11 +36,11 @@ class FileHeader(
|
||||
if (imageData != null) {
|
||||
prepare(imageData, mimeType, dimPrecomputed, onReady, onError)
|
||||
} else {
|
||||
onError()
|
||||
onError(null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
|
||||
onError()
|
||||
onError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class FileHeader(
|
||||
mimeType: String?,
|
||||
dimPrecomputed: String?,
|
||||
onReady: (FileHeader) -> Unit,
|
||||
onError: () -> Unit
|
||||
onError: (String?) -> Unit
|
||||
) {
|
||||
try {
|
||||
val hash = CryptoUtils.sha256(data).toHexKey()
|
||||
@@ -123,7 +123,7 @@ class FileHeader(
|
||||
onReady(FileHeader(mimeType, hash, size, dim, blurHash))
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||
onError()
|
||||
onError(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
|
||||
private val clientListener = object : Client.Listener() {
|
||||
override fun onEvent(event: Event, subscriptionId: String, relay: Relay, afterEOSE: Boolean) {
|
||||
if (subscriptionId in subscriptions.keys) {
|
||||
if (subscriptions.containsKey(subscriptionId)) {
|
||||
val key = "$debugName $subscriptionId ${event.kind}"
|
||||
val keyValue = eventCounter.get(key)
|
||||
if (keyValue != null) {
|
||||
@@ -64,7 +64,7 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
// Relay.Type.EOSE -> "sent all events it had stored."
|
||||
// }}")
|
||||
|
||||
if (type == Relay.StateType.EOSE && subscriptionId != null && subscriptionId in subscriptions.keys) {
|
||||
if (type == Relay.StateType.EOSE && subscriptionId != null && subscriptions.containsKey(subscriptionId)) {
|
||||
markAsEOSE(subscriptionId, relay)
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
subscriptions.values.forEach { updatedSubscription ->
|
||||
val updatedSubscriptionNewFilters = updatedSubscription.typedFilters
|
||||
|
||||
if (updatedSubscription.id in currentFilters.keys) {
|
||||
if (currentFilters.containsKey(updatedSubscription.id)) {
|
||||
if (updatedSubscriptionNewFilters == null) {
|
||||
// was active and is not active anymore, just close.
|
||||
Client.close(updatedSubscription.id)
|
||||
|
||||
@@ -4,11 +4,14 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
|
||||
object PlaybackClientController {
|
||||
val cache = LruCache<Int, SessionToken>(1)
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
fun prepareController(
|
||||
controllerID: String,
|
||||
@@ -25,9 +28,14 @@ object PlaybackClientController {
|
||||
bundle.putString("uri", videoUri)
|
||||
bundle.putString("callbackUri", callbackUri)
|
||||
|
||||
val sessionTokenLocal = SessionToken(context, ComponentName(context, PlaybackService::class.java))
|
||||
var session = cache.get(context.hashCode())
|
||||
if (session == null) {
|
||||
session = SessionToken(context, ComponentName(context, PlaybackService::class.java))
|
||||
cache.put(context.hashCode(), session)
|
||||
}
|
||||
|
||||
val controllerFuture = MediaController
|
||||
.Builder(context, sessionTokenLocal)
|
||||
.Builder(context, session)
|
||||
.setConnectionHints(bundle)
|
||||
.buildAsync()
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) {
|
||||
private val imageExtensionArray = arrayOf(".gif", ".png", ".jpg", ".jpeg", ".bmp", ".webp")
|
||||
|
||||
suspend fun fetchUrlPreview(timeOut: Int = 30000) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
fetch(timeOut)
|
||||
@@ -17,26 +13,7 @@ class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) {
|
||||
}
|
||||
|
||||
private suspend fun fetch(timeOut: Int = 30000) {
|
||||
lateinit var urlInfoItem: UrlInfoItem
|
||||
if (checkIsImageUrl()) {
|
||||
urlInfoItem = UrlInfoItem(url = url, image = url)
|
||||
} else {
|
||||
val document = getDocument(url, timeOut)
|
||||
urlInfoItem = parseHtml(url, document)
|
||||
}
|
||||
callback?.onComplete(urlInfoItem)
|
||||
}
|
||||
|
||||
private fun checkIsImageUrl(): Boolean {
|
||||
val uri = Uri.parse(url)
|
||||
var isImage = false
|
||||
for (imageExtension in imageExtensionArray) {
|
||||
if (uri.path != null && uri.path!!.toLowerCase(Locale.getDefault()).endsWith(imageExtension)) {
|
||||
isImage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return isImage
|
||||
callback?.onComplete(getDocument(url, timeOut))
|
||||
}
|
||||
|
||||
fun cleanUp() {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import okhttp3.MediaType
|
||||
import java.net.URL
|
||||
|
||||
@Immutable
|
||||
data class UrlInfoItem(
|
||||
class UrlInfoItem(
|
||||
val url: String = "",
|
||||
val title: String = "",
|
||||
val description: String = "",
|
||||
val image: String = ""
|
||||
val image: String = "",
|
||||
val mimeType: MediaType
|
||||
) {
|
||||
val verifiedUrl = kotlin.runCatching { URL(url) }.getOrNull()
|
||||
val imageUrlFullPath =
|
||||
@@ -18,6 +20,10 @@ data class UrlInfoItem(
|
||||
image
|
||||
}
|
||||
|
||||
fun fetchComplete(): Boolean {
|
||||
return url.isNotEmpty() && image.isNotEmpty()
|
||||
}
|
||||
|
||||
fun allFetchComplete(): Boolean {
|
||||
return title.isNotEmpty() && description.isNotEmpty() && image.isNotEmpty()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.service.previews
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
@@ -46,21 +48,31 @@ private val META_X_IMAGE = arrayOf(
|
||||
|
||||
private const val CONTENT = "content"
|
||||
|
||||
suspend fun getDocument(url: String, timeOut: Int = 30000): Document =
|
||||
suspend fun getDocument(url: String, timeOut: Int = 30000): UrlInfoItem =
|
||||
withContext(Dispatchers.IO) {
|
||||
val request: Request = Request.Builder().url(url).get().build()
|
||||
val html = HttpClient.getHttpClient().newCall(request).execute().use {
|
||||
HttpClient.getHttpClient().newCall(request).execute().use {
|
||||
if (it.isSuccessful) {
|
||||
it.body.string()
|
||||
val mimeType = it.headers.get("Content-Type")?.toMediaType()
|
||||
?: throw IllegalArgumentException("Website returned unknown mimetype: ${it.headers.get("Content-Type")}")
|
||||
|
||||
if (mimeType.type == "text" && mimeType.subtype == "html") {
|
||||
val document = Jsoup.parse(it.body.string())
|
||||
parseHtml(url, document, mimeType)
|
||||
} else if (mimeType.type == "image") {
|
||||
UrlInfoItem(url, image = url, mimeType = mimeType)
|
||||
} else if (mimeType.type == "video") {
|
||||
UrlInfoItem(url, image = url, mimeType = mimeType)
|
||||
} else {
|
||||
throw IllegalArgumentException("Website returned unknown encoding for previews: $mimeType")
|
||||
}
|
||||
} else {
|
||||
throw IllegalArgumentException("Website returned: " + it.code)
|
||||
}
|
||||
}
|
||||
|
||||
Jsoup.parse(html)
|
||||
}
|
||||
|
||||
suspend fun parseHtml(url: String, document: Document): UrlInfoItem =
|
||||
suspend fun parseHtml(url: String, document: Document, type: MediaType): UrlInfoItem =
|
||||
withContext(Dispatchers.IO) {
|
||||
val metaTags = document.getElementsByTag(ELEMENT_TAG_META)
|
||||
|
||||
@@ -106,8 +118,8 @@ suspend fun parseHtml(url: String, document: Document): UrlInfoItem =
|
||||
}
|
||||
|
||||
if (title.isNotEmpty() && description.isNotEmpty() && image.isNotEmpty()) {
|
||||
return@withContext UrlInfoItem(url, title, description, image)
|
||||
return@withContext UrlInfoItem(url, title, description, image, type)
|
||||
}
|
||||
}
|
||||
return@withContext UrlInfoItem(url, title, description, image)
|
||||
return@withContext UrlInfoItem(url, title, description, image, type)
|
||||
}
|
||||
|
||||
@@ -213,8 +213,8 @@ object Client : RelayPool.Listener {
|
||||
listeners = listeners.minus(listener)
|
||||
}
|
||||
|
||||
fun allSubscriptions(): List<String> {
|
||||
return subscriptions.keys.toList()
|
||||
fun allSubscriptions(): Set<String> {
|
||||
return subscriptions.keys
|
||||
}
|
||||
|
||||
fun getSubscriptionFilters(subId: String): List<TypedFilter> {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service.relays
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
@@ -14,6 +15,7 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.lang.StringBuilder
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
enum class FeedType {
|
||||
@@ -23,11 +25,13 @@ enum class FeedType {
|
||||
val COMMON_FEED_TYPES = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
|
||||
|
||||
class Relay(
|
||||
var url: String,
|
||||
var read: Boolean = true,
|
||||
var write: Boolean = true,
|
||||
var activeTypes: Set<FeedType> = FeedType.values().toSet()
|
||||
val url: String,
|
||||
val read: Boolean = true,
|
||||
val write: Boolean = true,
|
||||
val activeTypes: Set<FeedType> = FeedType.values().toSet()
|
||||
) {
|
||||
val brief = RelayBriefInfoCache.get(url)
|
||||
|
||||
companion object {
|
||||
// waits 3 minutes to reconnect once things fail
|
||||
const val RECONNECTING_IN_SECONDS = 60 * 3
|
||||
@@ -180,7 +184,7 @@ class Relay(
|
||||
}
|
||||
|
||||
fun markConnectionAsReady(pingInMs: Long, usingCompression: Boolean) {
|
||||
this.afterEOSEPerSubscription.clear()
|
||||
this.resetEOSEStatuses()
|
||||
this.isReady = true
|
||||
this.pingInMs = pingInMs
|
||||
this.usingCompression = usingCompression
|
||||
@@ -190,7 +194,7 @@ class Relay(
|
||||
this.socket = null
|
||||
this.isReady = false
|
||||
this.usingCompression = false
|
||||
this.afterEOSEPerSubscription.clear()
|
||||
this.resetEOSEStatuses()
|
||||
this.closingTimeInSeconds = TimeUtils.now()
|
||||
}
|
||||
|
||||
@@ -267,7 +271,11 @@ class Relay(
|
||||
socket = null
|
||||
isReady = false
|
||||
usingCompression = false
|
||||
afterEOSEPerSubscription.clear()
|
||||
resetEOSEStatuses()
|
||||
}
|
||||
|
||||
fun resetEOSEStatuses() {
|
||||
afterEOSEPerSubscription = LinkedHashMap(afterEOSEPerSubscription.size)
|
||||
}
|
||||
|
||||
fun sendFilter(requestId: String) {
|
||||
@@ -276,16 +284,22 @@ class Relay(
|
||||
if (read) {
|
||||
if (isConnected()) {
|
||||
if (isReady) {
|
||||
val filters = Client.getSubscriptionFilters(requestId).filter { activeTypes.intersect(it.types).isNotEmpty() }
|
||||
val filters = Client.getSubscriptionFilters(requestId).filter { filter ->
|
||||
activeTypes.any { it in filter.types }
|
||||
}
|
||||
if (filters.isNotEmpty()) {
|
||||
val request =
|
||||
"""["REQ","$requestId",${filters.take(10).joinToString(",") { it.filter.toJson(url) }}]"""
|
||||
val request = filters.joinToStringLimited(
|
||||
separator = ",",
|
||||
limit = 10,
|
||||
prefix = """["REQ","$requestId",""",
|
||||
postfix = "]"
|
||||
) { it.filter.toJson(url) }
|
||||
|
||||
// Log.d("Relay", "onFilterSent $url $requestId $request")
|
||||
|
||||
socket?.send(request)
|
||||
eventUploadCounterInBytes += request.bytesUsedInMemory()
|
||||
afterEOSEPerSubscription.clear()
|
||||
resetEOSEStatuses()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -298,6 +312,33 @@ class Relay(
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Iterable<T>.joinToStringLimited(
|
||||
separator: CharSequence = ", ",
|
||||
prefix: CharSequence = "",
|
||||
postfix: CharSequence = "",
|
||||
limit: Int = -1,
|
||||
transform: ((T) -> CharSequence)? = null
|
||||
): String {
|
||||
val buffer = StringBuilder()
|
||||
buffer.append(prefix)
|
||||
var count = 0
|
||||
for (element in this) {
|
||||
if (limit < 0 || count <= limit) {
|
||||
if (++count > 1) buffer.append(separator)
|
||||
when {
|
||||
transform != null -> buffer.append(transform(element))
|
||||
element is CharSequence? -> buffer.append(element)
|
||||
element is Char -> buffer.append(element)
|
||||
else -> buffer.append(element.toString())
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
buffer.append(postfix)
|
||||
return buffer.toString()
|
||||
}
|
||||
|
||||
fun sendFilterOnlyIfDisconnected() {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -356,7 +397,7 @@ class Relay(
|
||||
checkNotInMainThread()
|
||||
|
||||
val msg = """["CLOSE","$subscriptionId"]"""
|
||||
Log.d("Relay", "Close Subscription $url $msg")
|
||||
// Log.d("Relay", "Close Subscription $url $msg")
|
||||
socket?.send(msg)
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onDestroy() {
|
||||
Log.d("Lifetime Event", "MainActivity.onDestroy")
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
keepPlayingMutex?.stop()
|
||||
keepPlayingMutex?.release()
|
||||
keepPlayingMutex = null
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
@@ -16,8 +17,6 @@ import com.vitorpamplona.amethyst.service.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class ServerOption(
|
||||
@@ -30,7 +29,6 @@ open class NewMediaModel : ViewModel() {
|
||||
var account: Account? = null
|
||||
|
||||
var isUploadingImage by mutableStateOf(false)
|
||||
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
var mediaType by mutableStateOf<String?>(null)
|
||||
|
||||
var selectedServer by mutableStateOf<ServerOption?>(null)
|
||||
@@ -44,12 +42,14 @@ open class NewMediaModel : ViewModel() {
|
||||
var uploadingDescription = mutableStateOf<String?>(null)
|
||||
|
||||
var onceUploaded: () -> Unit = {}
|
||||
var onError: (String) -> Unit = {}
|
||||
|
||||
open fun load(account: Account, uri: Uri, contentType: String?) {
|
||||
open fun load(account: Account, uri: Uri, contentType: String?, onError: (String) -> Unit) {
|
||||
this.account = account
|
||||
this.galleryUri = uri
|
||||
this.mediaType = contentType
|
||||
this.selectedServer = ServerOption(defaultServer(), false)
|
||||
this.onError = onError
|
||||
}
|
||||
|
||||
fun upload(context: Context, relayList: List<Relay>? = null) {
|
||||
@@ -73,11 +73,11 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingPercentage.value = 0.2f
|
||||
uploadingDescription.value = "Loading"
|
||||
contentResolver.openInputStream(fileUri)?.use {
|
||||
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
|
||||
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList, context)
|
||||
}
|
||||
?: run {
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
onError(context.getString(R.string.could_not_open_the_compressed_file))
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
@@ -106,15 +106,14 @@ open class NewMediaModel : ViewModel() {
|
||||
localContentType = contentType,
|
||||
alt = alt,
|
||||
sensitiveContent = sensitiveContent,
|
||||
relayList = relayList
|
||||
relayList = relayList,
|
||||
context
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload: ${e.message}")
|
||||
}
|
||||
onError(context.getString(R.string.failed_to_upload_media, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,9 +122,7 @@ open class NewMediaModel : ViewModel() {
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
onError(context.getString(R.string.error_when_compressing_media, it))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -151,7 +148,8 @@ open class NewMediaModel : ViewModel() {
|
||||
localContentType: String?,
|
||||
alt: String,
|
||||
sensitiveContent: Boolean,
|
||||
relayList: List<Relay>? = null
|
||||
relayList: List<Relay>? = null,
|
||||
context: Context
|
||||
) {
|
||||
uploadingPercentage.value = 0.40f
|
||||
uploadingDescription.value = "Server Processing"
|
||||
@@ -169,9 +167,7 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
isUploadingImage = false
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
onError(context.getString(R.string.server_did_not_provide_a_url_after_uploading))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -203,9 +199,7 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
isUploadingImage = false
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
onError(context.getString(R.string.could_not_prepare_local_file_to_upload, it))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@@ -214,13 +208,28 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
isUploadingImage = false
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
onError(context.getString(R.string.could_not_download_from_the_server))
|
||||
}
|
||||
}
|
||||
|
||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
fun createNIP95Record(
|
||||
bytes: ByteArray,
|
||||
mimeType: String?,
|
||||
alt: String,
|
||||
sensitiveContent: Boolean,
|
||||
relayList: List<Relay>? = null,
|
||||
context: Context
|
||||
) {
|
||||
if (bytes.size > 80000) {
|
||||
viewModelScope.launch {
|
||||
onError("Media is too big for NIP-95")
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
uploadingPercentage.value = 0.30f
|
||||
uploadingDescription.value = "Hashing"
|
||||
|
||||
@@ -248,9 +257,7 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingPercentage.value = 0.00f
|
||||
isUploadingImage = false
|
||||
cancel()
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
onError(context.getString(R.string.could_not_prepare_local_file_to_upload, it))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -55,7 +54,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
@@ -67,14 +65,8 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
|
||||
|
||||
LaunchedEffect(uri) {
|
||||
val mediaType = resolver.getType(uri) ?: ""
|
||||
postViewModel.load(account, uri, mediaType)
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
postViewModel.imageUploadingError.collect { error ->
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
postViewModel.load(account, uri, mediaType) {
|
||||
accountViewModel.toast(context.getString(R.string.failed_to_upload_media_no_details), it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +138,11 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
|
||||
onPost = {
|
||||
onClose()
|
||||
postViewModel.upload(context, relayList)
|
||||
postViewModel.selectedServer?.let { account.changeDefaultFileServer(it.server) }
|
||||
postViewModel.selectedServer?.let {
|
||||
if (!it.isNip95) {
|
||||
account.changeDefaultFileServer(it.server)
|
||||
}
|
||||
}
|
||||
},
|
||||
isActive = postViewModel.canPost()
|
||||
)
|
||||
@@ -211,7 +207,6 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
try {
|
||||
bitmap = resolver.loadThumbnail(it, Size(1200, 1000), null)
|
||||
} catch (e: Exception) {
|
||||
postViewModel.imageUploadingError.emit("Unable to load thumbnail, but the video can be uploaded")
|
||||
Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,8 +405,10 @@ fun NewPostView(
|
||||
url,
|
||||
accountViewModel.account.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent ->
|
||||
postViewModel.upload(url, alt, sensitiveContent, false, server, context, relayList)
|
||||
accountViewModel.account.changeDefaultFileServer(server.server)
|
||||
postViewModel.upload(url, alt, sensitiveContent, false, server, context)
|
||||
if (!server.isNip95) {
|
||||
accountViewModel.account.changeDefaultFileServer(server.server)
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
postViewModel.contentToAddUrl = null
|
||||
|
||||
@@ -44,6 +44,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.events.findURLs
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -206,7 +207,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
val dmUsers = toUsersTagger.mentions
|
||||
|
||||
val zapReceiver = if (wantsForwardZapTo) {
|
||||
forwardZapTo?.items?.map {
|
||||
forwardZapTo.items.map {
|
||||
ZapSplitSetup(
|
||||
lnAddressOrPubKeyHex = it.key.pubkeyHex,
|
||||
relay = it.key.relaysBeingUsed.keys.firstOrNull(),
|
||||
@@ -361,8 +362,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
sensitiveContent: Boolean,
|
||||
isPrivate: Boolean = false,
|
||||
server: ServerOption,
|
||||
context: Context,
|
||||
relayList: List<Relay>? = null
|
||||
context: Context
|
||||
) {
|
||||
isUploadingImage = true
|
||||
contentToAddUrl = null
|
||||
@@ -569,7 +569,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
|
||||
)
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo?.addItem(item)
|
||||
forwardZapTo.addItem(item)
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
/*
|
||||
val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
|
||||
@@ -740,6 +740,14 @@ open class NewPostViewModel() : ViewModel() {
|
||||
alt: String?,
|
||||
sensitiveContent: Boolean
|
||||
) {
|
||||
if (bytes.size > 80000) {
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Media is too big for NIP-95")
|
||||
isUploadingImage = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
FileHeader.prepare(
|
||||
bytes,
|
||||
@@ -773,6 +781,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
contentToAddUrl = uri
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun startLocation(context: Context) {
|
||||
locUtil = LocationUtil(context)
|
||||
locUtil?.let {
|
||||
|
||||
@@ -57,7 +57,7 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.service.relays.Constants
|
||||
@@ -74,11 +74,11 @@ import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.WarningColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.warningColor
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -329,7 +329,7 @@ fun ServerConfig(
|
||||
accountViewModel.retrieveRelayDocument(
|
||||
item.url,
|
||||
onInfo = {
|
||||
relayInfo = RelayInfoDialog(RelayBriefInfo(item.url), it)
|
||||
relayInfo = RelayInfoDialog(RelayBriefInfoCache.RelayBriefInfo(item.url), it)
|
||||
},
|
||||
onError = { url, errorCode, exceptionMessage ->
|
||||
val msg = when (errorCode) {
|
||||
@@ -369,7 +369,12 @@ fun ServerConfigClickableLine(
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
) {
|
||||
Column(Modifier.clickable(onClick = onClick)) {
|
||||
RenderRelayIcon(iconUrl = item.briefInfo.favIcon, loadProfilePicture, Size55dp)
|
||||
RenderRelayIcon(
|
||||
item.briefInfo.displayUrl,
|
||||
item.briefInfo.favIcon,
|
||||
loadProfilePicture,
|
||||
MaterialTheme.colorScheme.largeRelayIconModifier
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = HalfHorzPadding)
|
||||
|
||||
@@ -124,7 +124,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
ObjectMapper().writeValue(writer, currentJson)
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.sendNewUserMetadata(writer.buffer.toString(), newClaims)
|
||||
account.sendNewUserMetadata(writer.buffer.toString(), displayName.value.trim(), newClaims)
|
||||
}
|
||||
clear()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -26,7 +27,7 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
@@ -36,14 +37,14 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RelayInformationDialog(
|
||||
onClose: () -> Unit,
|
||||
relayBriefInfo: RelayBriefInfo,
|
||||
relayBriefInfo: RelayBriefInfoCache.RelayBriefInfo,
|
||||
relayInfo: RelayInformation,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -81,9 +82,10 @@ fun RelayInformationDialog(
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = StdPadding.fillMaxWidth()) {
|
||||
Column() {
|
||||
RenderRelayIcon(
|
||||
relayBriefInfo.displayUrl,
|
||||
relayBriefInfo.favIcon,
|
||||
automaticallyShowProfilePicture,
|
||||
Size55dp
|
||||
MaterialTheme.colorScheme.largeRelayIconModifier
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
@@ -37,12 +37,12 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
data class RelayList(
|
||||
val relay: Relay,
|
||||
val relayInfo: RelayBriefInfo,
|
||||
val relayInfo: RelayBriefInfoCache.RelayBriefInfo,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
|
||||
data class RelayInfoDialog(
|
||||
val relayBriefInfo: RelayBriefInfo,
|
||||
val relayBriefInfo: RelayBriefInfoCache.RelayBriefInfo,
|
||||
val relayInfo: RelayInformation
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ fun RelaySelectionDialog(
|
||||
accountViewModel.account.activeWriteRelays().map {
|
||||
RelayList(
|
||||
relay = it,
|
||||
relayInfo = RelayBriefInfo(it.url),
|
||||
relayInfo = RelayBriefInfoCache.RelayBriefInfo(it.url),
|
||||
isSelected = preSelectedList.any { relay -> it.url == relay.url }
|
||||
)
|
||||
}
|
||||
@@ -167,7 +167,12 @@ fun RelaySelectionDialog(
|
||||
accountViewModel.retrieveRelayDocument(
|
||||
item.relay.url,
|
||||
onInfo = {
|
||||
relayInfo = RelayInfoDialog(RelayBriefInfo(item.relay.url), it)
|
||||
relayInfo = RelayInfoDialog(
|
||||
RelayBriefInfoCache.RelayBriefInfo(
|
||||
item.relay.url
|
||||
),
|
||||
it
|
||||
)
|
||||
},
|
||||
onError = { url, errorCode, exceptionMessage ->
|
||||
val msg = when (errorCode) {
|
||||
|
||||
@@ -60,7 +60,6 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallishBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
@@ -158,7 +157,7 @@ fun CashuPreview(
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.cashu),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Size20Modifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
|
||||
@@ -206,7 +205,7 @@ fun CashuPreview(
|
||||
if (isRedeeming) {
|
||||
LoadingAnimation()
|
||||
} else {
|
||||
ZapIcon(Size20dp, tint = Color.White)
|
||||
ZapIcon(Size20Modifier, tint = Color.White)
|
||||
}
|
||||
Spacer(DoubleHorzSpacer)
|
||||
|
||||
@@ -320,7 +319,7 @@ fun CashuPreviewNew(
|
||||
if (isRedeeming) {
|
||||
LoadingAnimation()
|
||||
} else {
|
||||
ZapIcon(Size20dp, tint = MaterialTheme.colorScheme.onBackground)
|
||||
ZapIcon(Size20Modifier, tint = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
Spacer(StdHorzSpacer)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
@@ -36,6 +35,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaIntent
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -119,7 +119,7 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.lightning),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Size20Modifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
@@ -37,6 +36,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
@@ -94,7 +94,7 @@ fun InvoiceRequest(
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.lightning),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Size20Modifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -10,6 +11,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun LoadUrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) {
|
||||
@@ -42,7 +45,17 @@ fun LoadUrlPreview(url: String, urlText: String, accountViewModel: AccountViewMo
|
||||
) { state ->
|
||||
when (state) {
|
||||
is UrlPreviewState.Loaded -> {
|
||||
UrlPreviewCard(url, state.previewInfo)
|
||||
if (state.previewInfo.mimeType.type == "image") {
|
||||
Box(modifier = HalfVertPadding) {
|
||||
ZoomableContentView(ZoomableUrlImage(url), persistentListOf(), roundedCorner = true, accountViewModel)
|
||||
}
|
||||
} else if (state.previewInfo.mimeType.type == "video") {
|
||||
Box(modifier = HalfVertPadding) {
|
||||
ZoomableContentView(ZoomableUrlVideo(url), persistentListOf(), roundedCorner = true, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
UrlPreviewCard(url, state.previewInfo)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
@@ -2,10 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
@@ -14,308 +11,48 @@ import coil.fetch.SourceResult
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.Options
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.utils.Robohash
|
||||
import okio.Buffer
|
||||
|
||||
private fun toHex(color: Color): String {
|
||||
val argb = color.toArgb()
|
||||
val rgb = argb and 0x00FFFFFF // Mask out the alpha channel
|
||||
return String.format("#%06X", rgb)
|
||||
}
|
||||
|
||||
private fun byteMod10(byte: Byte): Int {
|
||||
val ub = byte.toUByte().toInt()
|
||||
return ub % 10
|
||||
}
|
||||
|
||||
private fun bytesToRGB(b1: Byte, b2: Byte, b3: Byte): Color {
|
||||
return Color(b1.toUByte().toInt(), b2.toUByte().toInt(), b3.toUByte().toInt())
|
||||
}
|
||||
|
||||
private fun svgString(msg: String): String {
|
||||
checkNotInMainThread()
|
||||
|
||||
val hash = CryptoUtils.sha256(msg.toByteArray())
|
||||
val hashHex = hash.toHexKey()
|
||||
val bgColor = bytesToRGB(hash[0], hash[1], hash[2])
|
||||
val fgColor = bytesToRGB(hash[3], hash[4], hash[5])
|
||||
val bodyIndex = byteMod10(hash[6])
|
||||
val faceIndex = byteMod10(hash[7])
|
||||
val eyesIndex = byteMod10(hash[8])
|
||||
val mouthIndex = byteMod10(hash[9])
|
||||
val accIndex = byteMod10(hash[10])
|
||||
val body = bodies[bodyIndex]
|
||||
val face = faces[faceIndex]
|
||||
val eye = eyes[eyesIndex]
|
||||
val mouth = mouths[mouthIndex]
|
||||
val accessory = accessories[accIndex]
|
||||
|
||||
return """<svg id="$hashHex" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-bg{fill:${toHex(bgColor)}}.cls-fill-1{fill:${toHex(fgColor)};}.cls-fill-2{fill:${toHex(fgColor)};}
|
||||
${body.style}${face.style}${eye.style}${mouth.style}${accessory.style}
|
||||
</style>
|
||||
</defs>
|
||||
<title>Robohash $hashHex</title>
|
||||
${background}${body.paths}${face.paths}${eye.paths}${mouth.paths}${accessory.paths}
|
||||
</svg>"""
|
||||
}
|
||||
import java.nio.charset.Charset
|
||||
|
||||
@Stable
|
||||
class HashImageFetcher(
|
||||
private val context: Context,
|
||||
private val isLightTheme: Boolean,
|
||||
private val data: Uri
|
||||
) : Fetcher {
|
||||
|
||||
override suspend fun fetch(): SourceResult {
|
||||
checkNotInMainThread()
|
||||
val source = try {
|
||||
Buffer().apply { write(svgString(data.toString()).toByteArray()) }
|
||||
val buffer = Buffer()
|
||||
buffer.writeString(Robohash.assemble(data.toString(), isLightTheme), Charset.defaultCharset())
|
||||
buffer
|
||||
} finally {
|
||||
}
|
||||
|
||||
return SourceResult(
|
||||
source = ImageSource(source, context),
|
||||
mimeType = null,
|
||||
mimeType = "image/svg+xml",
|
||||
dataSource = DataSource.MEMORY
|
||||
)
|
||||
}
|
||||
|
||||
object Factory : Fetcher.Factory<Uri> {
|
||||
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return HashImageFetcher(options.context, data)
|
||||
return HashImageFetcher(options.context, options.parameters.value("isLightTheme") ?: true, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
object Robohash {
|
||||
fun imageRequest(context: Context, message: String): ImageRequest {
|
||||
|
||||
object RobohashImageRequest {
|
||||
fun build(context: Context, message: String, isLightTheme: Boolean): ImageRequest {
|
||||
return ImageRequest
|
||||
.Builder(context)
|
||||
.data("robohash:$message")
|
||||
.data(message)
|
||||
.fetcherFactory(HashImageFetcher.Factory)
|
||||
.setParameter("isLightTheme", isLightTheme)
|
||||
.addHeader("Cache-Control", "max-age=31536000")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class Part(val style: String, val paths: String)
|
||||
|
||||
private const val background = """<polyline class="cls-bg" points="150.3 7.4 55.1 97.9 55.1 203.1 150.3 293.6 245.9 203.1 245.9 97.9 150.3 7.4"/>"""
|
||||
|
||||
private val accessories: List<Part> = listOf(
|
||||
Part(
|
||||
""".cls-00-2{fill:none;}.cls-00-2,.cls-00-3,.cls-00-4{stroke:#000;}.cls-00-2,.cls-00-3{stroke-linecap:round;stroke-linejoin:round;}.cls-00-3{fill-opacity:0.4;stroke-width:0.75px;}.cls-00-4{fill-opacity:0.2;stroke-miterlimit:10;stroke-width:0.5px;}""",
|
||||
"""<g id="accessory-01"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M141.5,79.5s-1,11-1,17v8s-11,3-15,3h-10s-1-11-1-15-2-10,6-13a138,138,0,0,1,14-1C140.5,78.5,141.5,79.5,141.5,79.5Z"/><path class="cls-00-2" d="M141.5,79.5s-1,11-1,17v8s-11,3-15,3h-10s-1-11-1-15-2-10,6-13a138,138,0,0,1,14-1C140.5,78.5,141.5,79.5,141.5,79.5Z"/><path class="cls-00-3" d="M116.5,106.5s22-2.67,24-3.33v1.33s-11.42,2.74-13.21,2.87-11.79.13-11.79.13v-.94l1-.06"/><circle cx="137.5" cy="100.5" r="1"/><circle cx="139" cy="81" r="0.75"/><circle cx="117" cy="103" r="0.75"/><path class="cls-00-4" d="M117,88h3a3.49,3.49,0,0,1,2-1c1,0,9.5-1.5,9.5-1.5s-9,7-10,13v4l7-1s-1-6,2-10a22.28,22.28,0,0,1,7-6v-3l-1-1-18,2-1,1Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-01-2{fill:#fff;fill-opacity:0.2;}.cls-01-3,.cls-01-4{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-01-4{stroke-width:0.75px;}""",
|
||||
"""<g id="accessory-02"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M103.5,165.5s11-4,15-6a59.46,59.46,0,0,1,7-3s5,0,6,3,0,5-1,6-23,8-23,8a4.45,4.45,0,0,1-5-2C99.5,166.5,103.5,165.5,103.5,165.5Z"/><path class="cls-01-2" d="M102,167c-1.07,1.3,1.29,6.38,2.64,6.19s6.07.81,4.36-4.12C107.18,165,103.05,165.72,102,167Z"/><path class="cls-01-3" d="M103.5,165.5s11-4,15-6a59.46,59.46,0,0,1,7-3s5,0,6,3,0,5-1,6-23,8-23,8a4.45,4.45,0,0,1-5-2C99.5,166.5,103.5,165.5,103.5,165.5Z"/><path class="cls-01-4" d="M106.5,173.5c3-1,3-3,2-5-3-4-6-2-6-2.24"/><path class="cls-01-4" d="M109.1,163.4s4.4.1,5.4,3.1a5.22,5.22,0,0,1-.78,5"/><circle cx="128" cy="161" r="1"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-02-2{fill:#be1e2d;}.cls-02-11,.cls-02-2,.cls-02-5,.cls-02-6,.cls-02-7,.cls-02-9{stroke:#000;}.cls-02-11,.cls-02-2,.cls-02-5,.cls-02-9{stroke-miterlimit:10;}.cls-02-3{fill:#561317;}.cls-02-4{fill:#ed293b;}.cls-02-11,.cls-02-5,.cls-02-7{fill:none;}.cls-02-5,.cls-02-6{stroke-width:0.75px;}.cls-02-6{fill:#fff;}.cls-02-6,.cls-02-8{fill-opacity:0.2;}.cls-02-6,.cls-02-7{stroke-linecap:round;stroke-linejoin:round;}.cls-02-9{fill:#e6e7e8;}.cls-02-10{fill:#d0d2d3;}""",
|
||||
"""<g id="accessory-03"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M122.5,94.5s4,3,13,3,16-6,16-6a26.39,26.39,0,0,0-1-7c-1-3-10.49-2.87-15.74-1.93s-11.26.93-12.26,3.93Z"/><ellipse class="cls-02-2" cx="133" cy="26" rx="7.5" ry="6.5"/><path class="cls-02-3" d="M140,25c.19.68-5.51,8.12-11.57,6.27a8.65,8.65,0,0,0,7.91.53C141,30,140,25,140,25Z"/><ellipse class="cls-02-4" cx="130.22" cy="22.93" rx="4.46" ry="2.23" transform="matrix(0.87, -0.5, 0.5, 0.87, 5.98, 68.18)"/><ellipse class="cls-02-5" cx="133" cy="26" rx="7.5" ry="6.5"/><path class="cls-02-6" d="M135.5,82.5s-13,1-13,4,9,4,13,4,15-2,15-6S135.5,82.5,135.5,82.5Z"/><path class="cls-02-7" d="M122.5,94.5s4,3,13,3,16-6,16-6a26.39,26.39,0,0,0-1-7c-1-3-10.49-2.87-15.74-1.93s-11.26.93-12.26,3.93Z"/><path class="cls-02-8" d="M148.19,93.77c.31-1.27-.69-6.27-.69-6.27s-7.41-2.59-8.2-2.3,1.86-.8,2-1.75.23-1.43.23-1.43,3.12,0,5.53.25,3.41,1.47,3.41,2.35l.64,2.58.36,4.29-3,2"/><path class="cls-02-9" d="M133,29.24s-7.5,13.21-6.5,28.88a245.78,245.78,0,0,0,3,26.43s0,2,4,2a14,14,0,0,0,7-2,3.4,3.4,0,0,0,1-2c0-1-11-18.6-8-48.94C133.5,33.64,134.5,27.77,133,29.24Z"/><path class="cls-02-10" d="M130.5,46.5a94.41,94.41,0,0,0,2,18,145.29,145.29,0,0,0,6,19l.52,1.8s2.48-.8,2.48-2.8c0,0-6.45-14.41-7.23-22.71S132.5,39.5,133.5,33.5c0,0-.08-3.33.21-2.91S130,38,130.5,46.5Z"/><path class="cls-02-11" d="M133,29.24s-7.5,13.21-6.5,28.88a245.78,245.78,0,0,0,3,26.43s0,2,4,2a14,14,0,0,0,7-2,3.4,3.4,0,0,0,1-2c0-1-11-18.6-8-48.94C133.5,33.64,134.5,27.77,133,29.24Z"/><path class="cls-02-7" d="M126,46s-9,1-9,6,10,5,13,5,12-1,12-6-8-5-8-5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-03-2,.cls-03-3,.cls-03-8{fill:#fff;}.cls-03-2,.cls-03-3,.cls-03-7{fill-opacity:0.2;}.cls-03-2,.cls-03-4,.cls-03-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-03-2{stroke-width:0.75px;}.cls-03-4{fill:none;}.cls-03-5{fill:#ec1c24;}.cls-03-6{fill-opacity:0.1;}.cls-03-8{fill-opacity:0.4;}""",
|
||||
"""<g id="accessory-04"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M136.5,83.5c-3.39.31-11,2-13,4a4.38,4.38,0,0,0-1,3v5a18.26,18.26,0,0,0,13,3c8-1,16-6,16-6s0-6-1-7-1-2-5-2S138.74,83.3,136.5,83.5Z"/><path class="cls-03-2" d="M125.7,86.16s-2.2,1.34-1.2,3.34,7,3,12,2a53.37,53.37,0,0,0,11-4s3.57-2.09,1.29-3.54S131.89,82.83,125.7,86.16Z"/><path class="cls-03-3" d="M125.7,86.16s-2.2,1.34-1.2,3.34,7,3,12,2A55.23,55.23,0,0,0,145.11,89c-1.11-2-8.42-5.89-8.42-5.89S129.47,84.13,125.7,86.16Z"/><path class="cls-03-4" d="M136.5,83.5c-3.39.31-11,2-13,4a4.38,4.38,0,0,0-1,3v5a18.26,18.26,0,0,0,13,3c8-1,16-6,16-6s0-6-1-7-1-2-5-2S138.74,83.3,136.5,83.5Z"/><path class="cls-03-5" d="M134.5,73.5s-5,1-5,6,1,7,1,7a18.58,18.58,0,0,0,8,0c4-1,5-3,5-3S142.5,73.5,134.5,73.5Z"/><path class="cls-03-6" d="M139.5,75.5s2,3,1,6-6,5-6,5,8-1,9-3S140.5,76.5,139.5,75.5Z"/><path class="cls-03-4" d="M134.5,73.5s-5,1-5,6,1,7,1,7a18.58,18.58,0,0,0,8,0c4-1,5-3,5-3S142.5,73.5,134.5,73.5Z"/><path class="cls-03-7" d="M144.5,88.5c1,1,0,7.3,0,7.3l7-3.3s0-6.14-1.5-7.57C148,88,147,88,144.5,88.5Z"/><path class="cls-03-8" d="M133.5,75.5a1,1,0,0,1,1,1c0,1,0,6-2,6s-3,0-2-4S133.5,75.5,133.5,75.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-04-2,.cls-04-3{fill:none;stroke-linecap:round;stroke-linejoin:round;}.cls-04-2,.cls-04-3,.cls-04-5{stroke:#000;}.cls-04-3,.cls-04-5{stroke-width:0.75px;}.cls-04-4,.cls-04-6{fill:#fff;}.cls-04-4{fill-opacity:0.4;}.cls-04-5{fill:#ec1c24;stroke-miterlimit:10;}.cls-04-6,.cls-04-7{fill-opacity:0.2;}""",
|
||||
"""<g id="accessory_05"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M139.5,51.5s-24-2-32,33c0,0-6,25,20,25s32-17,34-32S149.5,51.5,139.5,51.5Z"/><path class="cls-04-2" d="M139.5,51.5c-15.88.08-26.77,10.62-32,33,0,0-5,26,20,25s34-18,34-32C161.5,60.5,149.5,51.5,139.5,51.5Z"/><path class="cls-04-3" d="M143.5,53.5s14,7,10,29-26,25-33,24-18-7-10.34-30.84C114.5,59.5,130.5,47.5,143.5,53.5Z"/><path class="cls-04-4" d="M121.5,63.5c-3,1.87-8,6-9,11s-1,14,0,16,4,1,4-2,0-17,5-21S123.4,62.32,121.5,63.5Z"/><path class="cls-04-4" d="M128.5,57.5s-6,4-3,5,6-3,6-3S133.5,56.5,128.5,57.5Z"/><path class="cls-04-5" d="M125.5,71.5s-7-2-10,1c-1.88,2.07,0,8,5,10s8-1,9-4S127.5,71.5,125.5,71.5Z"/><polygon class="cls-04-6" points="131 79 135 81 134.62 78.67 130.38 76.63 130.06 78.6 131 79"/><path class="cls-04-3" d="M130.09,76.5l4.46,2s.89,2,0,3a4.07,4.07,0,0,1-2.67,1L129,80.79A5.22,5.22,0,0,0,130.09,76.5Z"/><ellipse class="cls-04-4" cx="120" cy="75.5" rx="2.5" ry="2"/><path class="cls-04-7" d="M123.5,71.5s5,2,4,6-6.49,4.82-8.25,4.41,7.54,3.33,9.89-2.54S127,72,123.5,71.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-05-2{fill:#fff;fill-opacity:0.4;}.cls-05-2,.cls-05-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-05-3{fill:none;stroke-width:0.75px;}""",
|
||||
"""<g id="accessory-06"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M125.5,157.5s8-4,15-3,20,8,26,5l8-4s-3,13-14,14-29-3-35-2-25,7-30,6-10-1-12-11c0,0-1-3,3,0s11,4,18-1S119.5,153.5,125.5,157.5Z"/><path class="cls-05-2" d="M125.5,157.5s8-4,15-3,20,8,26,5l8-4s-3,13-14,14-29-3-35-2-25,7-30,6-10-1-12-11c0,0-1-3,3,0s11,4,18-1S119.5,153.5,125.5,157.5Z"/><ellipse cx="107.5" cy="165.25" rx="1" ry="1.25"/><ellipse cx="99.5" cy="168.25" rx="1" ry="1.25"/><ellipse cx="91.75" cy="168.94" rx="0.75" ry="0.94"/><ellipse cx="115.5" cy="163.25" rx="1" ry="1.25"/><ellipse cx="134.5" cy="162.25" rx="1" ry="1.25"/><ellipse cx="145.5" cy="163.25" rx="1" ry="1.25"/><ellipse cx="154.5" cy="164.25" rx="1" ry="1.25"/><ellipse cx="161.5" cy="163.25" rx="1" ry="1.25"/><circle class="cls-05-3" cx="125" cy="162" r="2.5"/><circle cx="125" cy="162" r="0.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-06-2,.cls-06-4{fill-opacity:0.2;}.cls-06-3,.cls-06-6{fill:none;}.cls-06-3,.cls-06-5,.cls-06-6{stroke:#000;}.cls-06-3,.cls-06-5{stroke-miterlimit:10;}.cls-06-4{fill:#fff;}.cls-06-5{fill:#ec1c24;stroke-width:0.75px;}.cls-06-6{stroke-linecap:round;stroke-linejoin:round;}""",
|
||||
"""<g id="accessory-07"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M131,59s-13,1-14,6a2.62,2.62,0,0,0,.5,2.5c1,1,3,5,4,8s2,5,0,5-8,2-10,4-1,5-1,5l1,9a33.15,33.15,0,0,0,18,5c16.5.5,34-9,35-10s-1-11-1-11c.16-.17-2.84-5.17-4-5-1,.14-12,0-12,0s0-15-1-16S141.5,58.5,131,59Z"/><path class="cls-06-2" d="M151.5,88.5s1.14,9.54.57,10.77c0,0,12.43-4.77,12.43-5.77s0-9-1-11a27.36,27.36,0,0,0-4-5s1.59,1.44-.21,3.72-5.36,6.31-8.08,6.3Z"/><path class="cls-06-3" d="M131,59s-13,1-14,6a2.62,2.62,0,0,0,.5,2.5c1,1,3,5,4,8s2,5,0,5-8,2-10,4-1,5-1,5l1,9s6,5,18,5c18,0,34-9,35-10s-1-11-1-11-3-4.79-4-5-12-.18-12,0c0,0,0-15-1-16S141.5,58.5,131,59Z"/><path class="cls-06-4" d="M121.5,80.5s-7.3,1.22-8.65,3.11.21,5.68,3.93,6.29,12.31,2.13,27-.63l7.42-1.75-10.71-4A38.39,38.39,0,0,0,121.5,80.5Z"/><path class="cls-06-3" d="M132.5,67.5s15-1,14-5-14-3.56-14-3.56S118,60,117.26,64.25,132.5,67.5,132.5,67.5Z"/><path class="cls-06-5" d="M121.5,75.5s0,2,6,3,13-2,16-4,3.33-3.41,3.33-3.41l.67,6.41a10.67,10.67,0,0,1-8,6c-13,3-16.91-3.58-16.91-3.58Z"/><path class="cls-06-2" d="M137,67l3.87,16.18S147,80,147,77s0-13.73,0-13.73S145.09,67,137,67Z"/><path class="cls-06-4" d="M136.18,59l.82,8s-18,2-20-2S130.36,58,136.18,59Z"/><path class="cls-06-6" d="M113.16,83.3s-6.66,9.2,23.18,7c25.17-1.75,23.8-12.39,23.8-12.39l3.36,4.64s2,10,1,11-18,10-33.68,10a45.47,45.47,0,0,1-16.46-3.35,7.75,7.75,0,0,1-2.86-1.61l-1.2-10A5.34,5.34,0,0,1,113.16,83.3Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-07-10,.cls-07-2,.cls-07-4,.cls-07-8,.cls-07-9{fill:none;}.cls-07-10,.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8,.cls-07-9{stroke:#000;}.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8{stroke-linecap:round;}.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8,.cls-07-9{stroke-linejoin:round;}.cls-07-3,.cls-07-5{fill:#fff;}.cls-07-3,.cls-07-5,.cls-07-6,.cls-07-7{fill-opacity:0.2;}.cls-07-3,.cls-07-4{stroke-width:0.75px;}.cls-07-10,.cls-07-8,.cls-07-9{stroke-width:1.5px;}.cls-07-10{stroke-miterlimit:10;}""",
|
||||
"""<g id="accessory-08"><g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M128.5,86.5a3.49,3.49,0,0,0-1,2v3s3,3,9,3,12-4,12-4v-5s-1-1-6-1S131.5,84.5,128.5,86.5Z"/><path class="cls-fill-1" d="M173.5,21.5l-35,17s-.57,2.41.22,3.71l.78,1.29,37-18S178.5,20.5,173.5,21.5Z"/><path class="cls-fill-1" d="M96.5,58.5s-2,2-1,3,3,0,3,0l33.66-15.9a6.44,6.44,0,0,1-.66-3.1Z"/><path class="cls-fill-1" d="M134.5,47.5l-1,37a4.33,4.33,0,0,0,4,2,7.65,7.65,0,0,0,5-2l-2-40S135.5,44.5,134.5,47.5Z"/><path class="cls-fill-1" d="M136.5,38.5l-4,2a3.76,3.76,0,0,0-.81,1.55,5.34,5.34,0,0,0-.19,1.45c0,2,2,4,3,4,0,0-.17-.54,1.42-1.77a8.26,8.26,0,0,1,4.32-1.22h.26s-2-2-2-3v-3Z"/></g><path class="cls-07-2" d="M134.5,47.5l-1,37a4.33,4.33,0,0,0,4,2,7.65,7.65,0,0,0,5-2l-2-40S135.5,44.5,134.5,47.5Z"/><path class="cls-07-3" d="M128.5,87.5s0,2,6,2,11-1,13-3c.48-.55.5-1.75-2.25-1.87-1,0-3.15,0-3.15,0a6.77,6.77,0,0,1-4.1,1.8c-3.5.1-4.31-1.6-4.31-1.6S128.5,85.5,128.5,87.5Z"/><path class="cls-07-2" d="M128.5,86.5a3.49,3.49,0,0,0-1,2v3s3,3,9,3,12-4,12-4v-5s-.8-1-5.8-1a7.15,7.15,0,0,1-4.57,2c-1.63,0-4.17,0-4.63-1.72A10.71,10.71,0,0,0,128.5,86.5Z"/><path class="cls-07-4" d="M128.5,86.5a3.49,3.49,0,0,0-1,2v3s3,3,9,3,12-4,12-4v-5c-.63-.65-1.9-1.15-6-1a6.16,6.16,0,0,1-4.17,1.8c-3.83.2-4.41-1.46-4.41-1.46S129.5,85.5,128.5,86.5Z"/><path class="cls-07-2" d="M136.5,38.5l-4,2a3.76,3.76,0,0,0-.81,1.55,5.34,5.34,0,0,0-.19,1.45c0,2,2,4,3,4,0,0-.17-.54,1.42-1.77a8.26,8.26,0,0,1,4.32-1.22h.26s-2-2-2-3v-3Z"/><path class="cls-07-5" d="M96.5,58.5s-2,2-1,3,3,0,3,0l33.8-15.6a7.45,7.45,0,0,1-.8-3.4Z"/><path class="cls-07-5" d="M173.5,21.5l-35,17s-.57,2.41.22,3.71l.78,1.29,37-18C177.52,25.52,179.49,20.53,173.5,21.5Z"/><circle class="cls-07-6" cx="175.5" cy="23.5" r="2"/><path d="M137.5,5.5s-2,0,1,2,32,15,32,15l2-1-33-15Z"/><path d="M56.72,44a1.7,1.7,0,0,0,1.2,1.19L96.29,58.83l1.89-1.2L58.81,44.1S56.44,43,56.72,44Z"/><path d="M114.5,17.5s-2,0,1,2,32,15,32,15l2-1-33-15Z"/><path d="M102,26.53s-2,.12,1.12,1.94S133,40.2,133,40.2l1.91-.77-30.83-12Z"/><path d="M177.5,24.19,212,41c1,1,.39,1.37-2,1L176.5,25.5S177,25.38,177.5,24.19Z"/><path d="M154.91,35.41l35.6,14.34S192,51,188.58,50.88L154,36.79Z"/><polygon points="102.89 59.02 134.34 68.69 133.65 69.98 101 59.98 102.89 59.02"/><path d="M142,42.54l35.55,13c2.4.9.47,2.47-2,1.19L140.2,43.79Z"/><path class="cls-07-7" d="M136,38.75a5.37,5.37,0,0,0-.5,2.75c0,2,2.22,3.37,2.22,3.37l1.8,40.84L144,88.34v4l4.46-1.89v-5a35.36,35.36,0,0,0-6-1l-2-40a3.7,3.7,0,0,1-2-3v-3Z"/><path class="cls-07-8" d="M119.5,47.5l-32-12s-3-1-4,1,0,3,2,4,27,10,27,10"/><line class="cls-07-8" x1="116.05" y1="49.09" x2="83.95" y2="37.51"/><line class="cls-07-9" x1="125.5" y1="49.2" x2="134.36" y2="52.57"/><line class="cls-07-10" x1="117.9" y1="52.66" x2="134" y2="59"/><line class="cls-07-10" x1="121.42" y1="50.93" x2="134.27" y2="55.83"/><path class="cls-07-10" d="M141,54.57,158,62s4,2,3,4-5,1-6,1-13.65-5.5-13.65-5.5"/><line class="cls-07-9" x1="141.18" y1="58.12" x2="160.5" y2="66.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-08-2{fill:none;}.cls-08-2,.cls-08-3,.cls-08-5,.cls-08-6,.cls-08-7{stroke:#000;stroke-miterlimit:10;}.cls-08-3,.cls-08-4{fill:#fff;}.cls-08-3,.cls-08-4,.cls-08-8{fill-opacity:0.2;}.cls-08-5{fill:#716558;}.cls-08-6{fill:#9a8479;}.cls-08-7{fill:#c1b49a;}""",
|
||||
"""<g id="accessory-09"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M135,83s-13,2-13,5a54.33,54.33,0,0,0,.5,6.5s4,4,13,3a37.83,37.83,0,0,0,16-6v-5l-3-4S143.5,81.5,135,83Z"/><path class="cls-08-2" d="M135,83s-13,2-13,5a54.33,54.33,0,0,0,.5,6.5s4,4,13,3a37.83,37.83,0,0,0,16-6v-5l-3-4S143.5,81.5,135,83Z"/><path class="cls-08-3" d="M123.5,88.5s2,3,10,2,14-3,15-5,1.19-2.37-1.41-3.18-13.22.86-16.41,1.52S122.5,85.5,123.5,88.5Z"/><path class="cls-08-4" d="M123.5,88.5s2,3,10,2a47.41,47.41,0,0,0,11.86-2.78c-.38.16-4.11-2.4-4.11-2.4s1.43-3.14,2-3.18a105.65,105.65,0,0,0-12.56,1.69C127.5,84.5,122.5,85.5,123.5,88.5Z"/><path class="cls-08-5" d="M139.5,66.5s-4-16,3-27c0,0-2-7-6-3,0,0-4,3-5,15a66,66,0,0,0,2,22S138.5,73.5,139.5,66.5Z"/><path class="cls-08-6" d="M119.5,36.5s1,13,3,19,4,11,6,16,5,7,6,5,0-3-2-10-5-18-5-25v-7a10.34,10.34,0,0,0-4-1C121.5,33.5,119.5,34.5,119.5,36.5Z"/><path class="cls-08-5" d="M110.5,50.5s8,8,10,13,5,13,6,10-3-18-6-22a61.22,61.22,0,0,0-5-6S110.5,45.5,110.5,50.5Z"/><path class="cls-08-7" d="M136.5,86.5s-5-19-19-28-20-7-20-7-4,3,1,6,12,5,18,10,12,14,12,16S131.5,88.5,136.5,86.5Z"/><path class="cls-08-7" d="M134.5,73.5s6-25,16-32,9,2,9,2v4s-4-2-8,5-9,19-9,23v8s-4,5-6,3c0,0-3.24-7.59-3.12-8.3S134.5,73.5,134.5,73.5Z"/><path class="cls-08-8" d="M144.36,88.12v7.11l7.14-3.73.07-4.64L150,84A11.59,11.59,0,0,1,144.36,88.12Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-09-2{fill:none;}.cls-09-2,.cls-09-4{stroke:#000;stroke-miterlimit:10;}.cls-09-3,.cls-09-4{fill:#fff;}.cls-09-3{fill-opacity:0.2;}.cls-09-5{fill:#d0d2d3;}.cls-09-6{fill-opacity:0.4;}""",
|
||||
"""<path id="fill_color" data-name="fill color" class="cls-fill-1" d="M122.5,88.5v6s3,4,13,3c12.5-.5,16-5,16-5v-6l-2-3a63.26,63.26,0,0,0-15,0C126.5,84.5,122.5,85.5,122.5,88.5Z"/><path class="cls-09-2" d="M122.5,88.5v6s2,3,13,3,16-5,16-5v-6l-2-3a63.26,63.26,0,0,0-15,0C126.5,84.5,122.5,85.5,122.5,88.5Z"/><path class="cls-09-3" d="M142.5,89.5c.2-.05,1.28-.44,1.5-.5-.63-1.83-4.53-5.64-5.5-5.7-7-.41-11.66,1-13.22,2-1.78,1.16-3.78,2.16,1.22,4.16C129.5,90.5,138.5,90.5,142.5,89.5Z"/><path class="cls-09-2" d="M142.5,89.5c4-1,10.83-4.83,3.92-5.92s-19.36.59-21.14,1.75-3.78,2.16,1.22,4.16C129.5,90.5,138.5,90.5,142.5,89.5Z"/><path class="cls-09-4" d="M130.5,52.5,128.24,84s1.26,3.47,7.26,2.47,7.33-3.44,7.33-3.44L132.5,54.5S131.5,51.5,130.5,52.5Z"/><path class="cls-09-5" d="M131.5,55.5l7.06,30.26s3.94-.26,3.94-2.26-10-29-10-29S130.5,50.5,131.5,55.5Z"/><path class="cls-09-2" d="M130.5,52.5,128.24,84s1.26,3.47,7.26,2.47,7.33-3.44,7.33-3.44L132.5,54.5S131.5,51.5,130.5,52.5Z"/><path class="cls-09-6" d="M144.09,88.72v8l7.41-4.26v-6l-2-3C150,85.88,147.82,87.51,144.09,88.72Z"/>"""
|
||||
)
|
||||
)
|
||||
|
||||
private val bodies: List<Part> = listOf(
|
||||
Part(
|
||||
""".cls-10-2{fill:#fff;fill-opacity:0.4;}.cls-10-3,.cls-10-5,.cls-10-7{fill:none;}.cls-10-3,.cls-10-4,.cls-10-5,.cls-10-6,.cls-10-7,.cls-10-8{stroke:#000;stroke-miterlimit:10;}.cls-10-3{stroke-width:1.2px;}.cls-10-4,.cls-10-6,.cls-10-9{fill-opacity:0.2;}.cls-10-5{stroke-width:1.25px;}.cls-10-6{stroke-width:0.75px;}.cls-10-8{fill:#6d6e70;}""",
|
||||
"""<g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M106,247.52s-34.47,3-50.47,32c0,0-5,9-6,23h27S77.43,272.54,106,247.52Z"/><path class="cls-fill-1" d="M226.5,236.5s-6-10-26-12-56-2-81,13-43,44-43,65h115s3-31,22-53c0,0,8.88-7.45,14.44-9.22C227.94,240.28,228.5,239.5,226.5,236.5Z"/><path class="cls-fill-1" d="M286.5,302.5s4-50-16-59-44-8-57,6-22,43-22,53Z"/></g><path class="cls-10-2" d="M91.18,264s5.32.48,3.32,9.48-5,7-7,17-2.5,12-2.5,12H76.5S75.85,285.54,91.18,264Z"/><path class="cls-10-3" d="M286.5,302.5s4-50-16-59-44-8-57,6-22,43-22,53Z"/><path class="cls-10-4" d="M268.5,247.5s-8,8-11,26-2.25,29-2.25,29H286.5s3.84-41.79-12.58-56.9C273.92,245.6,271.5,244.5,268.5,247.5Z"/><path class="cls-10-5" d="M226.5,236.5s-6-10-26-12-56-2-81,13-43,44-43,65h115s3-31,22-53c0,0,8.88-7.45,14.44-9.22C227.94,240.28,228.5,239.5,226.5,236.5Z"/><path class="cls-10-6" d="M218.22,229.55s-18.72.95-31.72,20.95-17,37-17.5,52h22.5c-1.42.12,8.08-36.84,16-45.5,10-15.5,21-16.5,21-16.5S228.93,235.6,218.22,229.55Z"/><path class="cls-10-7" d="M106,247.52s-34.47,3-50.47,32c0,0-5,9-6,23h27S77.43,272.54,106,247.52Z"/><path class="cls-10-2" d="M80.61,255.48s2.89,2-6.11,12a94,94,0,0,0-17,26c-3,7-1.75,9-1.75,9H49.5S50.73,270.47,80.61,255.48Z"/><path class="cls-10-3" d="M106,247.52s-34.47,3-50.47,32c0,0-5,9-6,23h27S77.43,272.54,106,247.52Z"/><path class="cls-10-7" d="M153.5,251.5c7.6-.12,26-2,27-14s-27-8-27-8-22,1-22,11S147.35,251.6,153.5,251.5Z"/><path class="cls-10-8" d="M138.5,166.5s12,33,4,72c0,0,1,6,12,5s12-5,12-5,6-37-10-75C156.5,163.5,145.5,166.5,138.5,166.5Z"/><path class="cls-10-7" d="M161,176a15.59,15.59,0,0,1-7.53,3.51c-5,1-9,1-11.52,0"/><path class="cls-10-7" d="M163.66,185.38c-1.18,2.51-2.36,7.71-20,5.42"/><path class="cls-10-7" d="M166.32,200.74s-4.63,7.77-21.23,3.27"/><path class="cls-10-7" d="M167.42,213.41s-3.73,9.13-22.33,4.11"/><path class="cls-10-7" d="M167.42,227.17s-7.75,9.36-23.34,1.85"/><circle cx="154.5" cy="247.5" r="1.5"/><circle cx="166.5" cy="244.5" r="1.5"/><circle cx="174.5" cy="237.5" r="1.5"/><circle cx="168.5" cy="232.5" r="1.5"/><circle cx="142.5" cy="245.5" r="1.5"/><circle cx="137.5" cy="240.5" r="1.5"/><path class="cls-10-9" d="M150.5,164.5s10.5,29.5,11,45a328.75,328.75,0,0,1-1,33l6-3c3.29-1.87.5-61.5-10-76Z"/>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-11-2{fill:#fff;}.cls-11-2,.cls-11-6{fill-opacity:0.2;}.cls-11-3{fill:none;}.cls-11-3,.cls-11-4{stroke:#000;stroke-miterlimit:10;}.cls-11-4{fill:#6d6e70;}.cls-11-5{opacity:0.2;}""",
|
||||
"""<g id="body-02"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M160.5,246.5s-14-3-27,13-22,45-22,45h108s-20-35-25-40S176.5,247.5,160.5,246.5Z"/><path class="cls-11-2" d="M121.5,303.5s5-24,10-33,3.28-8.07,7.64-8.53,15.13-1.22,23.75,2.66,8.18,3.25,8.18,3.25l13.12-12.15s-12.68-9.66-26.18-9.44-23.5,12.22-26.5,15.22-14.17,23.45-19.09,40.22Z"/><path class="cls-11-3" d="M145.5,261.5s28,1,38,17,13,26,13,26h-86s13.77-34.15,18.39-38.57S144.5,261.5,145.5,261.5Z"/><path class="cls-11-2" d="M121.5,303.5s5-24,10-33,3.28-8.07,7.64-8.53c22.36-1.47,31.93,5.9,31.93,5.9l13.12-12.15s-12.68-9.66-26.18-9.44-23.5,12.22-26.5,15.22-14.17,23.45-19.09,40.22Z"/><path class="cls-11-3" d="M160.5,246.5s-14-3-27,13-22,45-22,45h108s-20-35-25-40S176.5,247.5,160.5,246.5Z"/><path class="cls-11-4" d="M149,192s1,62,2,64a11.16,11.16,0,0,0,9.06,3.21c5.44-.71,6.44-2.71,6.94-5.21,0,0-2.5-48.5-2.5-62.5Z"/><path class="cls-11-5" d="M159.5,191.5s3,36,3,40,1,27,1,27l3-1-2-66Z"/><path class="cls-11-3" d="M164.5,203.5a8.76,8.76,0,0,1-6,2c-4,0-8,0-9-1"/><path class="cls-11-3" d="M165.5,214.5a12.68,12.68,0,0,1-6,2c-3,0-7,.25-10-1.37"/><path class="cls-11-3" d="M165.8,225.5a10.11,10.11,0,0,1-6.3,2c-4,0-7.65-.2-9.82-2.1"/><path class="cls-11-3" d="M166,234.5s-.5,3-5.5,3a64.39,64.39,0,0,1-10.3-1"/><path class="cls-11-3" d="M166.5,245.5s-1,3-6,3a21.51,21.51,0,0,1-10-2"/><path class="cls-11-6" d="M195.58,301.91H218S203,275.78,198.73,270.14,186.5,256.5,184.5,255.5l-13,12s8,4.69,10.48,8.84S192.66,293.32,195.58,301.91Z"/><path class="cls-11-4" d="M193.5,272.5a6.85,6.85,0,0,0-2,8c2,5,8,6,8,6s7-6,15-2,9,7,9,15,14,3,14,3l2-2s4-21-19-31C220.5,269.5,209.5,265.5,193.5,272.5Z"/><path class="cls-11-6" d="M196.5,284.5a25.68,25.68,0,0,1,18-5c11,1,16,9,18,17s-4,10-4,10l-5-7s.05-13.88-10-15.44-14,2.44-14,2.44S195.5,286.5,196.5,284.5Z"/><path class="cls-11-3" d="M206,268.82a2.89,2.89,0,0,0-1.5,2.68c0,2,.1,8.87,7,11.94"/><path class="cls-11-3" d="M229.44,274.89s-3.94-1.39-5.94,1.61-3.62,9.25-2.81,12.13"/><path class="cls-11-3" d="M223.56,300.34c-.06.16.94-2.84,5.94-2.84a21.66,21.66,0,0,1,10.09,2.37"/><path class="cls-11-4" d="M127.07,268.36S108.5,263.5,98.5,279.5c0,0-5,9,1,20s7,4,7,4l7-5.25s-6-7.75-2-13.75c0,0,2.75-4.25,8.88-3.12Z"/><path class="cls-11-3" d="M119.5,281.5s2-7,1-10a5.45,5.45,0,0,0-3.56-3.62"/><path class="cls-11-3" d="M111.5,284.5a14.54,14.54,0,0,0-8-5c-5-1-5.44.94-5.44.94"/><path class="cls-11-3" d="M111,293.56a10.89,10.89,0,0,0-7.48.94c-4,2-4,5-4,5"/><path class="cls-11-6" d="M123.53,274.89s-7-2.39-13,.61-9,7-9,12,5,12,7,14,4.83-3.86,4.83-3.86-3.83-5.14-2.83-11.14c0,0,1-6,10-5Z"/><circle cx="145" cy="266" r="1.5"/><circle cx="160.5" cy="268.5" r="1.5"/><circle cx="173.5" cy="274.5" r="1.5"/><circle cx="182.5" cy="285.5" r="1.5"/><circle cx="188.5" cy="296.5" r="1.5"/><circle cx="133.5" cy="266.5" r="1.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-12-2{fill:#fff;fill-opacity:0.4;}.cls-12-3,.cls-12-7{fill:none;}.cls-12-3,.cls-12-5,.cls-12-6,.cls-12-7{stroke:#000;}.cls-12-3,.cls-12-5,.cls-12-6{stroke-miterlimit:10;}.cls-12-4,.cls-12-6{fill-opacity:0.2;}.cls-12-5{fill:#6d6e70;}.cls-12-7{stroke-linecap:round;stroke-linejoin:round;}""",
|
||||
"""<g id="body-03"><g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M180.45,252.21s-17.95-2.71-29.95,2.29-15,8-16,24c0,0,1,27,1,28s49,1,49,1v-9a27.15,27.15,0,0,1-3-13c0-8,7-6.16,7-6.16S177,279,177.24,263.25A16.2,16.2,0,0,1,180.45,252.21Z"/><path class="cls-fill-1" d="M133.5,277.5s-6.92-1.5-8-5.25l-.68-1.62c-.37-6.66,1.07-12,5.72-15,0,0,5.17-4.37,11.55-4.24,0,0,5.37.13,7.37,3.13,0,0-9,3.6-12,9.3s-3.09,13.32-3,14Z"/><path class="cls-fill-1" d="M188.5,247.5s-8,2-10,8-3,19,8,23,19-7,19-17S201.5,245.5,188.5,247.5Z"/></g><path class="cls-12-2" d="M134.5,254.5s3-1,3,0-6,5-7,10,0,8-2,8-4.67.09-2.84-10.45A13,13,0,0,1,134.5,254.5Z"/><path class="cls-12-3" d="M133.5,277.5s-6.92-1.5-8-5.25l-.68-1.62c-.37-6.66,1.07-12,5.72-15,0,0,5.17-4.37,11.55-4.24,0,0,5.37.13,7.37,3.13,0,0-9,3.6-12,9.3s-3.09,13.32-3,14Z"/><path class="cls-12-4" d="M189.5,248.5s9,0,9,10-4,16-9,17-7,.77-7,.77a12.77,12.77,0,0,0,8,3.07c5,.16,15-4.84,15-17.84s-7.26-14.64-14.13-14.32S188.5,248.5,189.5,248.5Z"/><path class="cls-12-3" d="M188.5,247.5s-8,2-10,8-3,19,8,23,19-7,19-17S201.5,245.5,188.5,247.5Z"/><path class="cls-12-5" d="M201.86,273.17s9.64,10.33,9.64,28.33h-13s2.74-14.31-7.13-22.15A11.59,11.59,0,0,0,201.86,273.17Z"/><path class="cls-12-4" d="M197.5,279.5s5,8,6,15,0,9,0,9h8S213,293,206.76,280.74c0,0-3.26-6.24-5.26-7.24a22,22,0,0,1-4.74,4.65Z"/><path class="cls-12-3" d="M180.45,252.21s-17.95-2.71-29.95,2.29-15,8-16,24c0,0,1,27,1,28s49,1,49,1v-9a27.15,27.15,0,0,1-3-13c0-8,7-6.16,7-6.16S177,279,177.24,263.25A16.2,16.2,0,0,1,180.45,252.21Z"/><path class="cls-12-6" d="M169.5,304.5s-7-21,0-42c0,0,3.39-7.79,10.2-10.39l.8.39s-3.64,3.11-3.32,10.06,2.16,14.4,9.74,16.17a5.19,5.19,0,0,0-5,3.77c-1.44,4,.38,11.28,1.47,13.64A13.05,13.05,0,0,1,184.5,301Z"/><path class="cls-12-2" d="M142.5,259.5a2.19,2.19,0,0,1,3,1q1.5,3-3,9c-3,4-3,14-2,19s2,10,2,13h-7.16S134,284,134.75,275.26s2.49-12.37,6.12-15.56Z"/><path class="cls-12-3" d="M150,254.71s-4.5-4.21-10.5-3.21-14,6-15,15,9,12,10,12C134.5,278.5,132.5,259.92,150,254.71Z"/><path class="cls-12-5" d="M130.17,276.81a64.62,64.62,0,0,1-1.51,10.87c-1.2,5.22-3.35,11-7.16,13.82l13.79-.59s-.79-21.41-.79-22.41Z"/><path class="cls-12-3" d="M134.5,290.5s-2.86-2.88-5.93-2.44"/><path class="cls-12-3" d="M124,299s5.55-1.48,9,2"/><path class="cls-12-5" d="M150,192s1,61,2,64c0,0,2,5,10,3,0,0,6-1,6-7,0,0-3.5-40.5-2.5-60.5C165.5,191.5,152.5,191.5,150,192Z"/><path class="cls-12-7" d="M164.5,202.5s0,3-5,3-8.56-1-9.28-1.5"/><path class="cls-12-7" d="M165,214.5s-1,2-5.18,2a39.35,39.35,0,0,1-9.38-1.58"/><path class="cls-12-3" d="M166,224.5s0,2-4.62,3-10.38-1-10.38-1"/><path class="cls-12-3" d="M166,233.5s0,2-3.21,3a16.53,16.53,0,0,1-11.79-1"/><path class="cls-12-3" d="M167,244.5a7,7,0,0,1-5.17,4c-4.13,1-10.48-1.51-10.48-1.51"/><path class="cls-12-3" d="M207,281.6s-4-2.4-12,2.4"/><path class="cls-12-3" d="M211.42,296.16s-2.24-4.33-12.58,1.51"/><path class="cls-12-4" d="M159,191.5h6.5v10s1,20,.89,29.33A182.26,182.26,0,0,0,168,252.75h0c-.5,4.25-5.5,5.75-5.5,5.75v-2c0-2,1-15,1-23s-4-40-4-40Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-13-2{fill:none;}.cls-13-2,.cls-13-5{stroke:#000;stroke-miterlimit:10;}.cls-13-3{fill:#fff;fill-opacity:0.4;}.cls-13-4{fill-opacity:0.2;}.cls-13-5{fill:#6d6e70;}""",
|
||||
"""<g id="body-04"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M148.46,242.82s-35,5.68-33,23.68c0,0-1,7,9,20s13,20,13,20l51-2s1-7,9-18,11-13,11-24-13.09-25.36-60-19.68"/><path class="cls-13-2" d="M148.46,242.82s-35,5.68-33,23.68c0,0-1,7,9,20s13,20,13,20l51-2s1-7,9-18,11-13,11-24-13.09-25.36-60-19.68"/><path class="cls-13-3" d="M131.83,247.83s-6.33,4.67-7.33,12.67c0,0-3,7,4,14s13,18,14,25-7.56,2.2-7.56,2.2-5.72-8.89-11.08-16-10.81-14.18-7.58-25.17C116.28,260.49,118.17,254.17,131.83,247.83Z"/><circle cx="155.5" cy="267.5" r="1"/><circle cx="144" cy="268" r="1"/><circle cx="132" cy="270" r="1"/><circle cx="122" cy="274" r="1"/><circle cx="170" cy="267" r="1"/><circle cx="183" cy="270" r="1"/><circle cx="194" cy="275" r="1"/><path class="cls-13-4" d="M193.33,245.17s-1.83,5.33,1.17,8.33,4,4,2,8a21.05,21.05,0,0,0-3.72,4c-1.28,2,.72,4-1.28,9s-15,28-15,28l12.74-.81s-.74-2.19,8.26-15.19,13-17.33,10-29.17C207.5,257.33,204.17,249.83,193.33,245.17Z"/><path class="cls-13-2" d="M116.5,272.83s-2-6.33,6-8.33,19-5,40-5,41,6,40,18a10,10,0,0,1-2.17,5"/><path class="cls-13-5" d="M166.5,205.5h-14a12.13,12.13,0,0,0-5,1l1,38s0,11,10,11,11-9,11-9-3-17-3-27Z"/><path class="cls-13-2" d="M166,212.5s-2.06,3-9.25,3-9.25-1.5-9.25-1.5"/><path class="cls-13-2" d="M166,224.5s-2.06,3-9.25,3-9.25-1.5-9.25-1.5"/><path class="cls-13-2" d="M167,238.5s-2.06,3-9.25,3-9.25-1.5-9.25-1.5"/><path class="cls-13-4" d="M159.17,205.5s-.67,9,1.33,15,4,15,3,21-1,12.74-3,13.87,8-.87,9-8.87a168.89,168.89,0,0,1-3-28.92V205.5Z"/><path class="cls-13-5" d="M197.5,286.5s12,3,14,13,16,4,16,4,4-1-2-15A31.19,31.19,0,0,0,207.19,271a29.16,29.16,0,0,1-5.19,9.56A32.56,32.56,0,0,0,197.5,286.5Z"/><path class="cls-13-2" d="M219.5,279.5s-11.86,4.53-11.93,12.76"/><path class="cls-13-2" d="M228.5,296.5a15.7,15.7,0,0,0-17,4"/><path class="cls-13-4" d="M224,304.64l4.82-4.14s-2.14-16.5-11-23-10.33-6-10.33-6l-3.27,6S222.47,286.77,224,304.64Z"/><path class="cls-13-5" d="M117.31,275.57S103.5,282.5,100.5,301.5s17,0,17,0,1.85-8.67,8.92-12.34C126.42,289.16,118.13,278.64,117.31,275.57Z"/><path class="cls-13-2" d="M109,283s7.48,11.61,11.58,11.55"/><path class="cls-13-2" d="M102,296s6.9,7.76,11.5,6.47"/><path class="cls-13-4" d="M119.5,283.5s-13,7-12,21,10.64-5.12,10.64-5.12,6.36-8.88,8.36-9.88l-5.5-7Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-14-2{fill:none;}.cls-14-2,.cls-14-4{stroke:#000;stroke-miterlimit:10;}.cls-14-3,.cls-14-6{fill-opacity:0.2;}.cls-14-4{fill:#6d6e70;}.cls-14-5,.cls-14-6{fill:#fff;}.cls-14-5{fill-opacity:0.4;}""",
|
||||
"""<g id="body-05"><g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M137.41,252.15s-.91-4.65-11.91-3.65-13,12-13,18,4,13,12,14c0,0,3,0,3-1,0,0-3.17-6.44.41-16.72C127.91,262.78,133.33,254.8,137.41,252.15Z"/><path class="cls-fill-1" d="M194.5,244.5s-19,1-19,17,11,21,17,21,20-2,19-20S194.5,244.5,194.5,244.5Z"/><path class="cls-fill-1" d="M181.5,247.5a39.32,39.32,0,0,0-24-4c-14,2-28,14-30,19s-3,9,0,17,3,20,1,22h68s.68-18.41-.66-19.2c0,0-21.34,1.2-20.34-19.8a13.88,13.88,0,0,1,6.67-13.88Z"/></g><path class="cls-14-2" d="M194.5,244.5s-19,1-19,17,11,21,17,21,20-2,19-20S194.5,244.5,194.5,244.5Z"/><path class="cls-14-3" d="M195.5,245.5s8,5,6,18-9.78,17.77-14.39,17.89c0,0,21.41,6.8,24.4-15.54C211.51,265.84,212.5,245.5,195.5,245.5Z"/><path class="cls-14-2" d="M181.5,247.5a39.32,39.32,0,0,0-24-4c-14,2-28,14-30,19s-3,9,0,17,3,20,1,22h68s.68-18.41-.66-19.2c0,0-21.34,1.2-20.34-19.8a13.88,13.88,0,0,1,6.67-13.88Z"/><path class="cls-14-4" d="M195.5,282.5s7,11,6,19h12s2.5-15-7.25-23.5C206.25,278,203.5,282.5,195.5,282.5Z"/><path class="cls-14-2" d="M211.55,285.39s.73,7.21-11.66,6.16"/><path class="cls-14-5" d="M136.5,260.5a3.1,3.1,0,0,0-2,1c-1,1-3,5,0,5s4-1,4-3S137.5,260.5,136.5,260.5Z"/><path class="cls-14-5" d="M133.5,270.5a4.33,4.33,0,0,0-2,4c0,3,1,6,2,12s.67,14-1.67,15,6.67,0,6.67,0,4-16,0-25C138.5,276.5,136.5,269.5,133.5,270.5Z"/><path class="cls-14-3" d="M161.5,253.5s9,2,11,10,2,28,2,28v12l22,1v-19a4.38,4.38,0,0,0-1-3s-22-1-20-21c0,0,0-9,7-13a35,35,0,0,0-14.06-5.07l.06,5.07A10.39,10.39,0,0,1,161.5,253.5Z"/><path class="cls-14-2" d="M141.34,249.41s3.16,11.09,16.16,11.09,19.07-10,19-15"/><path class="cls-14-2" d="M144.5,301.5s2-12,1-18l23-1s1,17,0,19"/><path class="cls-14-2" d="M137.41,252.15s-.91-4.65-11.91-3.65-13,12-13,18,4,13,12,14c0,0,3,0,3-1C123.29,273.33,124.5,260.5,137.41,252.15Z"/><path class="cls-14-6" d="M118.5,252.5s5-3,3,1-5,5-6,11,4,11,4,11,2,2-1,2-7.7-8-5.35-17A16.51,16.51,0,0,1,118.5,252.5Z"/><path class="cls-14-4" d="M119.36,278.79s-3.86,5.71-4.86,10.71,1,12,1,12h10s-1-7,1-12,2.48-4.68,2.48-4.68l-1.48-5.32s-1.27,1.4-4.63.7-3.87-1.4-3.87-1.4"/><path class="cls-14-2" d="M114.92,286.75s3.58,4.75,11.58,2.75"/><circle cx="158.5" cy="257.5" r="1"/><circle cx="150" cy="255" r="1"/><circle cx="146" cy="251" r="1"/><circle cx="168" cy="254" r="1"/><circle cx="172" cy="247" r="1"/><path class="cls-14-3" d="M125,280.39s-4.48,3.61-3.22,21.11H125s-1-11.5,4-16.5l-1.58-5.08Z"/><path class="cls-14-3" d="M202,281.28S209,293,209,302s4.79-1.37,4.79-1.37S215,286,206,278A6.19,6.19,0,0,1,202,281.28Z"/><path class="cls-14-4" d="M147,200s-1,42,2,50c0,0,12,9,20-2,0,0-3.5-26.5-3.5-48.5A77.11,77.11,0,0,0,147,200Z"/><path class="cls-14-2" d="M166,211.5s-1.08,3-8.68,3-10.48-1.37-10.48-1.37"/><path class="cls-14-2" d="M166,223.5s-1.08,3-8.68,3-10.48-1.37-10.48-1.37"/><path class="cls-14-2" d="M147.5,238.5a15.39,15.39,0,0,0,8,2c5,0,11.32-1.58,12.16-4.29"/><path class="cls-14-3" d="M158.67,199.17s.83,11.33,1.83,18.33,2,15,2,20,.33,13.93-1.33,16c0,0,4.33,0,7.33-6,0,0-3-26-3-33v-15Z"/><circle cx="148" cy="286" r="0.5"/><circle cx="165.5" cy="285.5" r="0.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-15-2{fill:#fff;}.cls-15-2,.cls-15-4{fill-opacity:0.2;}.cls-15-2,.cls-15-3,.cls-15-5{stroke:#000;stroke-miterlimit:10;}.cls-15-3{fill:none;}.cls-15-5{fill:#6d6e70;}""",
|
||||
"""<g id="body-06"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M145.5,301.5s-1-18-1-23c0,0,.37-3.41,15.68-2.2s10.15-.26,10.15-.26,5.17-.54,5.17,2.46,0,19,3,23Z"/><path class="cls-15-2" d="M159.5,286.5s16-1,16-8c0,0,4-3-14-2h-12s-5,0-5,2S143.5,285.5,159.5,286.5Z"/><path class="cls-15-3" d="M145.5,301.5s-1-18-1-23c0,0,.37-3.41,15.68-2.2s10.15-.26,10.15-.26,5.17-.54,5.17,2.46,0,19,3,23Z"/><path class="cls-15-4" d="M167.5,282.5l4.79.67.55,18.33h5.66s-2.64-3-2.82-15l-.18-8s-1-3-4-2.5v3.5A6.93,6.93,0,0,1,167.5,282.5Z"/><circle cx="164" cy="291" r="1.5"/><circle cx="155.5" cy="291.5" r="1.5"/><circle cx="148.5" cy="288.5" r="1.5"/><circle cx="171.5" cy="288.5" r="1.5"/><path class="cls-15-5" d="M146,200s3,68,2,79c0,0,2,6,12,5s11.5-4.5,11.5-4.5-3-75-2-80C169.5,199.5,151.5,199.5,146,200Z"/><path class="cls-15-4" d="M161.5,200.5s0,20,1,27,3,24,4,35a166.5,166.5,0,0,1,.61,20s4.39-2,4.39-3-1.38-41.67-1.69-48.33-.31-31.67-.31-31.67h-8Z"/><path class="cls-15-3" d="M146.43,210.28a31.28,31.28,0,0,0,13.07,3.22c7,0,9.83-3.22,9.83-3.22"/><path class="cls-15-3" d="M147.43,222.28a31.28,31.28,0,0,0,13.07,3.22c7,0,9.83-3.22,9.83-3.22"/><path class="cls-15-3" d="M147.43,234.28a31.28,31.28,0,0,0,13.07,3.22c7,0,9.83-3.22,9.83-3.22"/><path class="cls-15-3" d="M147.43,246.28a31.28,31.28,0,0,0,13.07,3.22c7,0,9.83-3.22,9.83-3.22"/><path class="cls-15-3" d="M148.43,259.28a31.28,31.28,0,0,0,13.07,3.22c7,0,9.83-3.22,9.83-3.22"/><path class="cls-15-3" d="M148.43,271.28a31.28,31.28,0,0,0,13.07,3.22c7,0,9.83-3.22,9.83-3.22"/><path class="cls-15-4" d="M144.5,281.5l2,21,33-1s-4-6-4-23c0,0-1,8-15,8S145.5,282.5,144.5,281.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-16-2,.cls-16-5{fill:none;}.cls-16-2,.cls-16-5,.cls-16-6,.cls-16-7,.cls-16-8,.cls-16-9{stroke:#000;}.cls-16-2{stroke-linecap:round;stroke-linejoin:round;}.cls-16-3,.cls-16-6,.cls-16-7{fill-opacity:0.2;}.cls-16-4,.cls-16-6{fill:#fff;}.cls-16-4{fill-opacity:0.4;}.cls-16-5,.cls-16-6,.cls-16-7,.cls-16-8,.cls-16-9{stroke-miterlimit:10;}.cls-16-8{fill:#f9ec31;}.cls-16-9{fill:#6d6e70;}""",
|
||||
"""<g id="body-07"><g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M158.5,234.5s-59,1-84,42,23,42,23,42l132-13s-5-16,5-27,19-6,19-6S219.5,231.5,158.5,234.5Z"/><path class="cls-fill-1" d="M277.5,301.5c-.33-7.3-4.07-15.56-12-25,0,0-11.5-7.5-25.28-2.69-7.72,4.69-13.72,11.69-11.72,28.69Z"/><path class="cls-fill-1" d="M74.5,276.5a29.05,29.05,0,0,0-26,16c-9,17,10,17,10,17l8.79-9.5S64.5,293.5,74.5,276.5Z"/></g><path class="cls-16-2" d="M158.5,234.5s-59,1-84,42,23,42,23,42l132-13s-5-16,5-27,19-7,19-7S219.5,231.5,158.5,234.5Z"/><path class="cls-16-3" d="M215,245s6.5,1.46-.5,16.46-13,26-10,41,24.32,0,24.32,0-3.6-15.79,7-25.4c12.91-11.65,19.71-3.56,16.64-5.6C252.5,271.5,235.49,253.58,215,245Z"/><path class="cls-16-2" d="M158.5,234.5s-59,1-84,42,23,42,23,42l132-13s-5-16,5-27c7-9,19-7,19-7S219.5,231.5,158.5,234.5Z"/><path class="cls-16-4" d="M62.5,280.5s5,0,0,9-8,11-9,14-6.5-2.33-6.5-2.33-3.23-4.81,5.63-14.74C52.63,286.43,57.5,280.5,62.5,280.5Z"/><path class="cls-16-5" d="M74.5,276.5a29.05,29.05,0,0,0-26,16c-9,17,10,17,10,17l8.79-9.5S64.5,293.5,74.5,276.5Z"/><path class="cls-16-4" d="M77.5,301.5s0-19,13-30,16-5,15-2-14,24-14,32S77.5,301.5,77.5,301.5Z"/><path class="cls-16-5" d="M277.5,301.5s-5-39-37.28-27.69c-7.72,4.69-13.72,11.69-11.72,28.69Z"/><path class="cls-16-3" d="M261.5,304.5s4-7,3-15-8.36-16.57-8.36-16.57a25.86,25.86,0,0,1,18.77,16.29,27.17,27.17,0,0,1,2.58,15.29Z"/><path class="cls-16-6" d="M103.5,303.5s2-30,41-29,41,29,41,29H174.25c-.75-2-12.12-18.58-30.75-18-15.88-.37-26.93,4.64-30.5,18Z"/><path class="cls-16-7" d="M113.5,301.5s3-16,28-16,32,17,32,17Z"/><path class="cls-16-8" d="M118.5,300.5s9-12,24-11,24,12,24,12Z"/><circle cx="125.5" cy="283.5" r="1"/><circle cx="113" cy="291" r="1"/><circle cx="148" cy="280" r="1"/><circle cx="170" cy="289" r="1"/><path class="cls-16-9" d="M147,200s0,44,1,45a14.41,14.41,0,0,0,14,4c8-2,7-7,7-7a144.32,144.32,0,0,1-2.5-24.5v-19S148.5,198.5,147,200Z"/><path class="cls-16-3" d="M159.5,202.5s1,17,2,23,2,22.83-2,23.91c0,0,9.62-.9,9.31-8.41s-2.43-19.17-2.37-30.34.06-12.17.06-12.17l-7,.11Z"/><path class="cls-16-5" d="M147,233.5s1.08,3,5.4,3,12.83-1.33,15-4.66"/><path class="cls-16-5" d="M147.5,219.32s1,2.68,5,2.68,11.89-1.18,13.94-4.16"/><path class="cls-16-5" d="M147.5,208.07s1,1.93,5,1.93,11.89-.85,13.94-3"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-17-2{fill:#020202;}.cls-17-2,.cls-17-4,.cls-17-6,.cls-17-7{fill-opacity:0.4;}.cls-17-3{fill-opacity:0.2;}.cls-17-4,.cls-17-6{fill:#fff;}.cls-17-4,.cls-17-5,.cls-17-8{stroke:#000;stroke-miterlimit:10;}.cls-17-5{fill:none;}.cls-17-8{fill:#6d6e70;}""",
|
||||
"""<g id="body-08"><g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M125.5,286.5s-5,5-4,15l83,1s-2-19-13-22-16-1.95-16-1.95h-.39l.39,3.95s-4.64,7.17-15.82,7.08-13.57-2.76-15.87-5.92c0,0-1.3-4.45-.8-6.81C143,276.85,135.5,277.5,125.5,286.5Z"/><path class="cls-fill-1" d="M143.5,272.5a4.87,4.87,0,0,0-.19,1.41c0,2.53,1.69,7.59,15.19,7.59,18,0,16-9,16-9s-1-5-13-5S145.5,268.5,143.5,272.5Z"/><path class="cls-fill-1" d="M143.5,282.5s0,6,14,7,18-7,18-7l-1-10s-.12-5-15.56-5-14.44,4-15.44,6a10.07,10.07,0,0,0-.43,4C143.14,280,143.5,282.5,143.5,282.5Z"/></g><path class="cls-17-2" d="M166,276l4,3s6-4,4-7-2.41-2.08-2.41-2.08L171,274Z"/><path class="cls-17-3" d="M143.5,282.5s0,6,14,7,18-7,18-7l-1-10c-2.8,12.94-32.54,10.72-31,1a24,24,0,0,0-.43,4C143.14,280,143.5,282.5,143.5,282.5Z"/><path class="cls-17-4" d="M143.5,272.5a4.87,4.87,0,0,0-.19,1.41c0,2.53,1.69,7.59,15.19,7.59,18,0,16-9,16-9s-1-5-13-5S145.5,268.5,143.5,272.5Z"/><path class="cls-17-5" d="M143.5,282.5s0,6,14,7,18-7,18-7l-1-10s-.12-5-15.56-5-14.44,4-15.44,6a10.07,10.07,0,0,0-.43,4C143.14,280,143.5,282.5,143.5,282.5Z"/><path class="cls-17-5" d="M125.5,286.5s-5,5-4,15l83,1s-2-19-13-22-16-1.95-16-1.95h-.39l.39,3.95s-4.64,7.17-15.82,7.08-13.57-2.76-15.87-5.92c0,0-1.3-4.45-.8-6.81C143,276.85,135.5,277.5,125.5,286.5Z"/><path class="cls-17-6" d="M134.5,281.5s5-1,3,2-7,4-9,9a17.55,17.55,0,0,0-1.2,9.07l-5.8-.07S120,288.59,129.74,283A12.58,12.58,0,0,1,134.5,281.5Z"/><path class="cls-17-7" d="M168.21,288.58s15.29-5.08,20.29-1.08,5,12,5,14,11,0,11,0-2.7-17.32-10.85-20.16a50.64,50.64,0,0,0-18.15-2.84h0v4S169.92,288.67,168.21,288.58Z"/><path class="cls-17-7" d="M170.33,279.34v7.9s5.17-3.74,5.17-4.74-.94-9.36-.94-9.36S175.16,277.17,170.33,279.34Z"/><path class="cls-17-8" d="M143,201s8,21,7,38-2,35-2,35,.5,3.5,10.5,3.5,13-5,13-5,0-37-3-53-8-22-8-22S147.5,200.5,143,201Z"/><path class="cls-17-3" d="M153.89,199s6.61,15.53,8.61,28.53,4,24,4,33v15.63s5-1.63,5-3.63,1.23-37.53-5.39-62.77c0,0-3.61-10.23-5.61-12.23Z"/><path class="cls-17-5" d="M150,226.5s14.89,2.35,18.24-8.32"/><path class="cls-17-5" d="M150.5,237.5s1,4,8,3,11-4,12-6"/><path class="cls-17-5" d="M149.63,250.63s.88,3.88,6.88,3.88S170,251,171.24,248.74"/><path class="cls-17-5" d="M147.5,213.5s1,2,6,2,12.85-4.58,12.43-6.29"/><path class="cls-17-5" d="M149,262.5s0,5,8.18,4,12.77-3.62,14.06-6.31"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-fill-2,.cls-18-3,.cls-18-5,.cls-18-6{stroke:#000;stroke-miterlimit:10;}.cls-18-3{fill:none;}.cls-18-4{fill:#fff;}.cls-18-4,.cls-18-5{fill-opacity:0.4;}.cls-18-6{fill:#6d6e70;}.cls-18-7{opacity:0.2;}.cls-18-8{fill-opacity:0.2;}""",
|
||||
"""<g id="body-09"><g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M95.13,227.69S74.5,232.5,73.5,248.5c-.44,7.05,4.25,12.09,9.63,15.53a52.45,52.45,0,0,0,14.79,6.13s-3.16-18.66-2.79-30.66S95.13,227.69,95.13,227.69Z"/><path class="cls-fill-1" d="M97.5,301.5s2-21,0-35-2-29-2-36,14-26.11,46-28.05,59-1.95,63,18.05,3,30,3,30,2,53,0,55S97.5,301.5,97.5,301.5Z"/><path class="cls-fill-2" d="M204.5,220.5s28,1,29,23S216.6,267,208,268.24C208,268.24,209.5,233.5,204.5,220.5Z"/></g><path class="cls-18-3" d="M95.5,230.5s-7,20,40,23,69-15.69,69.51-30.35-12.33-22.39-48.92-21.52S100.5,212.5,95.5,230.5Z"/><path class="cls-18-4" d="M87.5,232.5s-9,9-9,14,6,16,2,15-7.57-12-5.79-18.48S86.5,229.5,87.5,232.5Z"/><path class="cls-18-3" d="M97.5,301.5s2-21,0-35-2-29-2-36,14-26.11,46-28.05,59-1.95,63,18.05,3,30,3,30,2,53,0,55S97.5,301.5,97.5,301.5Z"/><path class="cls-18-5" d="M102.5,227.5s-4,22,38,21c0,0,56,2,57-26s-69-14-69-14S106.5,214.5,102.5,227.5Z"/><path class="cls-18-3" d="M95.5,230.5s-7,20,40,23,69-15.69,69.51-30.35-12.33-22.39-48.92-21.52S100.5,212.5,95.5,230.5Z"/><path class="cls-18-3" d="M95.13,227.69S74.5,232.5,73.5,248.5c-.44,7.05,4.25,12.09,9.63,15.53a52.45,52.45,0,0,0,14.79,6.13s-3.16-18.66-2.79-30.66S95.13,227.69,95.13,227.69Z"/><path class="cls-18-3" d="M204.5,220.5s28,1,29,23S216.6,267,208,268.24C208,268.24,209.5,233.5,204.5,220.5Z"/><path class="cls-18-4" d="M104.5,263.5a4.45,4.45,0,0,0,2,5c3,2,7,0,6-2S106.5,260.5,104.5,263.5Z"/><path class="cls-18-4" d="M109.5,276.5s-5-1-5,9a161.75,161.75,0,0,0,1,18l11,1s-3-5-3-14S112.5,278.5,109.5,276.5Z"/><path class="cls-18-6" d="M224.9,263.3s-1.4,20.2,16.6,39.2h-22s-7.35-6.33-11.18-20.67l.18-13.33S218.3,268.1,224.9,263.3Z"/><path class="cls-18-3" d="M208.5,281.5a10.58,10.58,0,0,1,11-6c8,1,7.71,3,7.71,3"/><path class="cls-18-3" d="M215.68,298s4.82-4.52,10.82-4.52a18.1,18.1,0,0,1,9.41,2.26"/><path class="cls-18-7" d="M217.85,223.44s9.65,6.06,7.65,17.06-8,16.09-17,18.54v9.46s24.08-.06,25-22.53C233.54,246,236.21,233.39,217.85,223.44Z"/><path class="cls-18-6" d="M76.5,302.5s9-10,8-24l-1-14,14,6s2,24,0,31S76.5,302.5,76.5,302.5Z"/><path class="cls-18-3" d="M97.5,282.5s-3.87-3-12.94-1"/><path class="cls-18-3" d="M97.5,298.5s-2.26-5.48-16.63-2.74"/><path class="cls-18-8" d="M90.5,270.5s7,25,0,32l7-1v-31l-8.76-3.76Z"/><path class="cls-18-8" d="M85.5,273.5s5,22-2,29-4.6-3.27-4.6-3.27,4.73-4.71,5.66-15.22l.94-10.51"/><path class="cls-18-8" d="M225.8,272.89s-12.3-6.39-12.3.61,11,28,14,31-6,0-6,0-10.71-12.76-12.36-19.88-.64-16.12-.64-16.12l16.4-5.2Z"/><path class="cls-18-8" d="M197.51,304.5s4-34,0-53c-2.39-11.36-2.58-11.5-2.55-11.32,0,0,8.57-4.37,10.06-17s3.49,45.35,3.49,45.35v37Z"/><circle cx="104" cy="254" r="1.5"/><circle cx="115.5" cy="258.5" r="1.5"/><circle cx="129.5" cy="261.5" r="1.5"/><circle cx="146.5" cy="262.5" r="1.5"/><circle cx="164.5" cy="260.5" r="1.5"/><circle cx="180.5" cy="255.5" r="1.5"/><circle cx="193.5" cy="248.5" r="1.5"/><circle cx="202.5" cy="241.5" r="1.5"/><path class="cls-18-6" d="M139.5,181.5l1,39s0,7,10,7,11-10,11-10a155.16,155.16,0,0,1-3-16c-1-8,0-20,0-20h-19Z"/><path class="cls-18-3" d="M158,185.5s.33,3.92-18.35,2"/><path class="cls-18-3" d="M158,197.5s0,6-17.5,1"/><path class="cls-18-3" d="M160,210.5s-2.35,7-20,2"/><path class="cls-18-8" d="M151.5,183.5a14.82,14.82,0,0,0,0,10c2,5,5,18,4,24a61.38,61.38,0,0,1-2.35,9.74s9.35-4.74,8.35-9.74-2.37-8.51-3.18-17.76a104.59,104.59,0,0,1,.18-18.24h-6.4Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-19-2{fill-opacity:0.6;}.cls-19-11,.cls-19-13,.cls-19-14,.cls-19-2,.cls-19-3,.cls-19-4,.cls-19-6,.cls-19-8{stroke:#000;}.cls-19-11,.cls-19-13,.cls-19-2,.cls-19-4,.cls-19-6,.cls-19-8{stroke-miterlimit:10;}.cls-19-11,.cls-19-14,.cls-19-3,.cls-19-8{fill:none;}.cls-19-14,.cls-19-3{stroke-linecap:round;stroke-linejoin:round;}.cls-19-10,.cls-19-12,.cls-19-4,.cls-19-5{fill:#fff;}.cls-19-12,.cls-19-4{fill-opacity:0.2;}.cls-19-4{stroke-opacity:0;}.cls-19-5{fill-opacity:0.1;}.cls-19-6{fill:#6d6e70;}.cls-19-7{fill:#58595b;}.cls-19-13,.cls-19-9{fill-opacity:0.4;}.cls-19-10{fill-opacity:0.5;}.cls-19-11,.cls-19-14{stroke-width:0.75px;}""",
|
||||
"""<g id="body-10"><g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M222.5,301.5s-10-35,11-53,58-15,70,2,2,51,2,51Z"/><path class="cls-fill-1" d="M83.5,254.5s-6,28,67,26,77-30,76-36-12.3-10.12-26-13c0,5,1,22,1,22s-8,13-46,17c-29,1-45-9-45-9s-1.5-16-2.5-23.13C96.5,241.75,85.5,247.5,83.5,254.5Z"/><path class="cls-fill-1" d="M110.5,261.5s20,11,44,9,36-7,47-17l-1-24a10.53,10.53,0,0,1-.31,4.41c-.69,1.59-4.63,16.78-42.16,19.68s-47-4.1-50-16.1Z"/><rect class="cls-fill-1" x="83.5" y="250" width="143.5" height="58.5"/><path class="cls-fill-1" d="M87.5,247.5s-19-10-37-5-29,20-32,37c-1,11,1,16,3,20h64s2-5,0-16-3-23-2-29S87.5,247.5,87.5,247.5Z"/><path class="cls-fill-1" d="M108,237s-1,20,45,17,48.5-20.5,47.5-24.5c-.91-3.63-13.68-10.56-39-9.56,0,.56.26,3.3.26,3.3,1.74,9.26.74,15.26-9.26,16.26-3.41.33-10-5-10-5s-1-8-1-13C120.38,224.59,108.41,230,108,237Z"/></g><path class="cls-19-2" d="M108,237s-1,20,45,17,48.5-20.5,47.5-24.5c-.91-3.63-13.68-10.56-39-9.56,0,.56.26,3.3.26,3.3,1.74,9.26.74,15.26-9.26,16.26-3.41.33-10-5-10-5s-1-8-1-13C120.38,224.59,108.41,230,108,237Z"/><path class="cls-19-3" d="M110.5,261.5s20,11,44,9,36-7,47-17l-1-24a10.53,10.53,0,0,1-.31,4.41c-.69,1.59-4.63,16.78-42.16,19.68s-47-4.1-50-16.1Z"/><path class="cls-19-4" d="M193.37,260.15c-6.66,3.83-18.28,8.29-37.87,10.35-29,1-45-9-45-9s-1.5-16-2.5-23.13c-11.5,3.38-22.5,9.13-24.5,16.13,0,0-6,28,67,26,29.58-.81,47.84-5.89,59-12"/><path class="cls-19-5" d="M83.5,254.5s-6,28,67,26c29.58-.81,47.84-5.89,59-12-5.5-2.5-10.5-5.5-16.13-8.35-6.66,3.83-18.28,8.29-37.87,10.35-29,1-45-9-45-9s-1.5-16-2.5-23.13C96.5,241.75,85.5,247.5,83.5,254.5Z"/><path class="cls-19-6" d="M141.5,190.5v29a142.25,142.25,0,0,0,1,15,9.78,9.78,0,0,0,10,5c7-1,11-4,10-11-.36-2.54-.73-5-1-7.56a138.9,138.9,0,0,1-1-17.44v-14A69.37,69.37,0,0,0,141.5,190.5Z"/><path class="cls-19-7" d="M152.5,190.5s0,7,2,12a55.9,55.9,0,0,1,3.13,17.34c-.12,3.66.88,15.66-3.12,17.66s-2,2-2,2,7-1.57,8.48-3.79,1.94-3.24,1.23-9.23a177.33,177.33,0,0,1-1.71-24.86V189.5a37.46,37.46,0,0,0-6.17-.25l-1.83.25Z"/><path class="cls-19-8" d="M141.5,190.5v29a142.25,142.25,0,0,0,1,15,9.78,9.78,0,0,0,10,5c7-1,11-4,10-11-.36-2.54-.73-5-1-7.56a138.9,138.9,0,0,1-1-17.44v-14A69.37,69.37,0,0,0,141.5,190.5Z"/><path class="cls-19-3" d="M161.25,222.25s-2.67,3.25-9.51,3.25-10.13-2-10.13-2"/><path class="cls-19-3" d="M160,208.5s-3,3-9.06,3S142,210,142,210"/><path class="cls-19-3" d="M160,197.5s-2,2-8.06,2A86.48,86.48,0,0,1,142,199"/><path class="cls-19-9" d="M188,246.51,189.52,262s6.63-4,9.06-6l2.42-2s.72.24.36-3.88-.79-17.93-.79-17.93S200,240,188,246.51Z"/><path class="cls-19-10" d="M84,273.63S97,277,97,285s-1,9,0,14,0,4,0,4H84s3.32-3,2.41-11.26S85,279.5,85,279.5Z"/><path class="cls-19-8" d="M87.5,247.5s-19-10-37-5-29,20-32,37c-1,11,1,16,3,20h64s2-5,0-16-3-23-2-29S87.5,247.5,87.5,247.5Z"/><path class="cls-19-9" d="M87.5,247.5a63.65,63.65,0,0,0-25.06-6.39c7.06,2.39,3.06,8.39-8.94,13.39-11,6-23,10-27.5,32.5-.67,7.37.84,13.18,1.5,14.5l58-2s2-5,0-16-3-23-2-29S87.5,247.5,87.5,247.5Z"/><path class="cls-19-11" d="M33.5,299.5s-7-19-3-35a28.63,28.63,0,0,1,17-21.06"/><path class="cls-19-12" d="M62.44,241.12a3.46,3.46,0,0,1,3.06,3.38c0,3,0,4-10,9s-21.45,10-26.22,21-2.62,25-2.62,25H21.5l-3-20S27.38,241.73,62.44,241.12Z"/><path class="cls-19-9" d="M84.26,275.62A31.64,31.64,0,0,1,80.5,290.5a19.71,19.71,0,0,1-11.14,8.83l16.14.17s1.42-2.62.71-10.81A90.57,90.57,0,0,0,84.26,275.62Z"/><path class="cls-19-13" d="M222.5,301.5s-10-35,11-53,58-15,70,2,2,51,2,51Z"/><path class="cls-19-14" d="M292.5,241.5s-12-1-13,15c-1,14,4,41,7,45"/><path class="cls-19-9" d="M226.5,244.5l.74,11a39.65,39.65,0,0,0-7.12,24.19c.38,14.81,2.38,21.81,2.38,21.81h-12s1-9,.5-14.5-1.49-18.55-1.49-18.55S227.5,259.5,226.5,244.5Z"/><path class="cls-19-8" d="M209.51,268.45s18-9,17-24l.74,11a39.65,39.65,0,0,0-7.12,24.19c.38,14.81,2.38,21.81,2.38,21.81h-12"/><path class="cls-19-9" d="M293.9,242.19c-1.9-1.19,6.6,6.31,7.6,15.31s7.11,6.39,7.11,6.39-.11-8.39-5.11-13.39S295.81,243.38,293.9,242.19Z"/><circle cx="154" cy="262" r="1.5"/><circle cx="170.5" cy="259.5" r="1.5"/><circle cx="183.5" cy="255.5" r="1.5"/><circle cx="194.5" cy="250.5" r="1.5"/><circle cx="138.5" cy="261.5" r="1.5"/><circle cx="125.5" cy="259.5" r="1.5"/><circle cx="115.5" cy="255.5" r="1.5"/><path class="cls-19-8" d="M83.5,254.5s-6,28,67,26,77-30,76-36-12.3-10.12-26-13c0,5,1,22,1,22s-8,13-46,17c-29,1-45-9-45-9s-1.5-16-2.5-23.13C96.5,241.75,85.5,247.5,83.5,254.5Z"/></g>"""
|
||||
)
|
||||
)
|
||||
|
||||
private val eyes: List<Part> = listOf(
|
||||
Part(
|
||||
""".cls-20-2{fill-opacity:0.4;}.cls-20-2,.cls-20-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-20-3{fill:#461917;stroke-width:0.5px;}""",
|
||||
"""<g id="eyes-01"><g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M144.5,141.5s5,9,9,7,13-9,13-9v-9s-2-1-7,0-14,5-14,5S142.5,137.5,144.5,141.5Z"/><path class="cls-fill-1" d="M118,141l-5,10s-7.5-2.5-10.5-4.5-4-3-4-6l1-3a13.6,13.6,0,0,1,7,0c4,1,11,2,11,2S118.5,139.5,118,141Z"/></g><path class="cls-20-2" d="M144.5,141.5s5,9,9,7,13-9,13-9v-9s-2-1-7,0-14,5-14,5S142.5,137.5,144.5,141.5Z"/><path class="cls-20-3" d="M144.5,139.5s2,2,6,1,10-6,12-7a18.66,18.66,0,0,0,4-3s-3.22-1-9.11.52a67.92,67.92,0,0,0-11.89,4.48S142.5,137.5,144.5,139.5Z"/><path class="cls-20-2" d="M118,141l-5,10s-7.5-2.5-10.5-4.5-4-3-4-6l1-3a13.6,13.6,0,0,1,7,0,37.46,37.46,0,0,0,8,1S118.5,139.5,118,141Z"/><path class="cls-20-3" d="M100.5,139.5l6,3c2,1,9.7,2.6,11.35-1.2s-11.6-4.69-16-4.24c0,0-3-.17-2.67.94A2.3,2.3,0,0,0,100.5,139.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-21-2{fill-opacity:0.2;}.cls-21-2,.cls-21-3,.cls-21-4,.cls-21-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-21-2,.cls-21-4,.cls-21-5{stroke-width:0.5px;}.cls-21-3,.cls-21-4{fill-opacity:0.4;}.cls-21-5{fill:#461917;}.cls-21-6{fill:#faaf40;}""",
|
||||
"""<g id="eyes-02"><g id="fill_color" data-name="fill color"><path id="fillcolor" class="cls-fill-1" d="M110.5,122.5c-4,0-17,2-17,17s10,17,17,17,15-5,15-18S114.5,122.5,110.5,122.5Z"/><path id="fillcolor-2" data-name="fillcolor" class="cls-fill-1" d="M153.5,121.5c-4,0-17.5,2.5-17.5,17.5s10.5,16.5,17.5,16.5,16-5,16-18C167,122,157.5,121.5,153.5,121.5Z"/></g><path class="cls-21-2" d="M110.5,126.5s-13,0-13,12,10,13,13,13,12-2,12-14.52C122.5,137,121.5,127.5,110.5,126.5Z"/><path class="cls-21-3" d="M154.5,121.5s-20,0-19,18,19,16,19,16,16-2,15-17S158.5,121.5,154.5,121.5Z"/><path class="cls-21-3" d="M110.5,122.5c-4,0-17,2-17,17s10,17,17,17,15-5,15-18S114.5,122.5,110.5,122.5Z"/><path class="cls-21-4" d="M140.1,137.73s-2,13.2,12.18,13.2,14.22-10.15,14.22-13.2-3-12.18-12.18-12.18S140.1,132.65,140.1,137.73Z"/><path class="cls-21-5" d="M109,127s-9,1-9,9,2.5,12.5,10.5,12.5,12-5,12-10S117.5,126.5,109,127Z"/><path class="cls-21-5" d="M154,126s-10.5.52-11,9.4S147,149,154,149s12-6.26,12-11.49S161.5,125.51,154,126Z"/><path class="cls-21-6" d="M110.26,134.06a.92.92,0,0,0-.24.05c-.36.13-1,.61-1,2.31,0,2.36,0,3.54,1.26,3.54s2.51,0,2.51-3.54C112.77,134.06,111.12,133.86,110.26,134.06Z"/><path class="cls-21-6" d="M154.26,134.06a.92.92,0,0,0-.24.05c-.36.13-1,.61-1,2.31,0,2.36,0,3.54,1.26,3.54s2.51,0,2.51-3.54C156.77,134.06,155.12,133.86,154.26,134.06Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-22-2{opacity:0.4;}.cls-22-3{fill:#461917;}.cls-22-3,.cls-22-4,.cls-22-5{stroke:#000;}.cls-22-3,.cls-22-5{stroke-linecap:round;stroke-linejoin:round;}.cls-22-3,.cls-22-4{stroke-width:0.5px;}.cls-22-4{fill:#ec1c24;stroke-miterlimit:10;}.cls-22-5{fill:none;stroke-width:0.75px;}""",
|
||||
"""<g id="eyes-03"><path class="cls-fill-1" d="M135.5,123.5s-34,1-35,17,23,12,23,12,23-2,28-3,18-5,18-14-8-13-17-13S139.5,123.5,135.5,123.5Z"/><path class="cls-22-2" d="M117.31,152.44c-4.51,0-10.59-.74-13.91-4.27a9.8,9.8,0,0,1-2.41-7.64c1-15.37,34.18-16.52,34.52-16.53h0c1.61,0,3.25-.16,5.15-.35A107.89,107.89,0,0,1,152.5,123c8,0,16.5,3.28,16.5,12.5,0,9.71-15.8,13.15-17.6,13.51-4.92,1-27.72,3-27.95,3a42,42,0,0,1-6.14.44Z"/><path d="M152.5,123.5c7.73,0,16,3.15,16,12,0,7.56-10.81,11.74-17.2,13-4.89,1-27.66,3-27.89,3h-.08a41.19,41.19,0,0,1-6,.43c-4.41,0-10.35-.71-13.54-4.12a9.32,9.32,0,0,1-2.27-7.27c.93-14.91,33.7-16.05,34-16.06,1.65,0,3.3-.17,5.22-.36a107.45,107.45,0,0,1,11.78-.64m0-1c-9,0-13,1-17,1,0,0-34,1-35,17-.67,10.67,9.78,12.44,16.81,12.44a41.73,41.73,0,0,0,6.19-.44s23-2,28-3,18-5,18-14-8-13-17-13Z"/><path class="cls-22-3" d="M106.5,132.5s-2,2-1,6,2,9,16,8a306.78,306.78,0,0,0,31-4c6-1,16-1.5,17-6.25s-2.18-11.89-12.09-13.32-12.93-.11-16.92.23-8.25.45-11.62.89-16.37,2.44-22.37,8.44"/><path class="cls-22-4" d="M134,124s-4,2-4,6,3.5,11.5,10.5,10.5,9-5,9-11-6.58-6.54-6.58-6.54-3.42.54-4.42.54S134,124,134,124Z"/><ellipse cx="140" cy="130.5" rx="2" ry="2.5"/><path class="cls-22-5" d="M107.5,131.5s-3,2-2,7c1,4,2,9,16,8a306.78,306.78,0,0,0,31-4c6-1,16-1.5,17-6.25s-2.18-11.89-12.09-13.32-12.93-.11-16.92.23-8.25.45-11.62.89-15.37,1.44-21.37,7.44"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-23-2,.cls-23-3{fill:#fff;}.cls-23-2{fill-opacity:0.4;}.cls-23-3{fill-opacity:0.2;}.cls-23-3,.cls-23-4,.cls-23-5{stroke:#000;stroke-miterlimit:10;}.cls-23-4{fill:none;}.cls-23-4,.cls-23-5{stroke-width:0.75px;}.cls-23-5{fill:red;}""",
|
||||
"""<g id="eyes-04"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M127.5,127.5c11.08-.53,43-2,43,11,1,15-16.69,15-23.34,15.52s-37.66,1.48-47.66.48-15-4-15-12,8-11,17-13S121,127.81,127.5,127.5Z"/><path class="cls-23-2" d="M94.39,132.09s2.78-4.25,37.95-4.92,37.17,6.33,38.17,11.33-3,10.76-6,11.88c0,0,5.52-6.38-2.24-11.63s-22.72-6.58-29.74-6.91-24.86-.88-27.94-.61S96.28,131.67,94.39,132.09Z"/><path class="cls-23-3" d="M127.5,127.5c11.08-.53,43-2,43,11,1,15-16.69,15-23.34,15.52s-37.66,1.48-47.66.48-15-4-15-12,8-11,17-13S121,127.81,127.5,127.5Z"/><path class="cls-23-4" d="M121.5,131.5c-19,0-36-3-37,11s19,12,40,12,42,0,42-9C166.5,134.5,140.5,131.5,121.5,131.5Z"/><path class="cls-23-5" d="M121.5,140.5s29,0,32,1,2,4-1,4-22-1-31-1-22,1-26,0-1-2,11-3S121.5,140.5,121.5,140.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-24-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-24-2,.cls-24-3,.cls-24-5{stroke:#000;}.cls-24-2,.cls-24-5{stroke-width:0.75px;}.cls-24-3,.cls-24-5{fill:#461917;stroke-linecap:round;stroke-linejoin:round;}.cls-24-3{stroke-width:0.5px;}.cls-24-4{fill:#ec1c24;}""",
|
||||
"""<g id="eyes-05"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M131.5,119.5c-5,0-19,2-19,16,0,15,10,18,18,18s17-7,16-19S136.5,119.5,131.5,119.5Z"/><path class="cls-24-2" d="M131.5,119.5c-5,0-19,2-19,16,0,15,10,18,18,18s17-7,16-19S136.5,119.5,131.5,119.5Z"/><path class="cls-24-2" d="M132,124l-.5,0c-2.51,0-13.61.38-14.5,11-1,12,8.5,13.5,12.5,13.5s13-3,13-13C142.5,124.5,132,124,132,124Z"/><path class="cls-24-3" d="M130.5,124.5s-10,1-10,10,5,11,11,11a11.1,11.1,0,0,0,11-11C142.5,128.5,136.5,124.5,130.5,124.5Z"/><ellipse class="cls-24-4" cx="131.5" cy="134.5" rx="2" ry="3"/><path class="cls-24-5" d="M147.5,133.5h6s1,0,0,1a7.69,7.69,0,0,1-3,2h-4v-2Z"/><path class="cls-24-5" d="M112.49,135.15a29.28,29.28,0,0,1-3.29.35c-1.1,0-1.7,1-2.2,2a.79.79,0,0,0,.2.57,1.56,1.56,0,0,0,.94.36,18.66,18.66,0,0,0,2.15.07c2.2,0,2.32-.35,2.32-.35Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-25-2{fill-opacity:0.55;stroke-miterlimit:10;stroke-width:0.75px;}.cls-25-2,.cls-25-3{stroke:#000;}.cls-25-3{fill:#461917;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5px;}.cls-25-4{fill:#ec1c24;}""",
|
||||
"""<g id="eyes-06"><g id="fill_color" data-name="fill color"><path class="cls-fill-1" d="M112.71,130l-.49,0c-2.46,0-13.35.38-14.22,11-1,12,8.33,13.5,12.26,13.5s12.75-3,12.75-13C123,130.5,112.71,130,112.71,130Z"/><path class="cls-fill-1" d="M156.71,130l-.49,0c-2.46,0-13.35.38-14.22,11-1,12,8.33,13.5,12.26,13.5s12.75-3,12.75-13C167,130.5,156.71,130,156.71,130Z"/></g><path class="cls-25-2" d="M112.71,130l-.49,0c-2.46,0-13.35.38-14.22,11-1,12,8.33,13.5,12.26,13.5s12.75-3,12.75-13C123,130.5,112.71,130,112.71,130Z"/><path class="cls-25-3" d="M111.23,130.5s-9.8,1-9.8,10,4.9,11,10.78,11a11,11,0,0,0,10.78-11C123,134.5,117.12,130.5,111.23,130.5Z"/><ellipse class="cls-25-4" cx="112.22" cy="140.5" rx="1.96" ry="3"/><path class="cls-25-2" d="M156.71,130l-.49,0c-2.46,0-13.35.38-14.22,11-1,12,8.33,13.5,12.26,13.5s12.75-3,12.75-13C167,130.5,156.71,130,156.71,130Z"/><path class="cls-25-3" d="M155.23,130.5s-9.8,1-9.8,10,4.9,11,10.78,11a11,11,0,0,0,10.78-11C167,134.5,161.12,130.5,155.23,130.5Z"/><ellipse class="cls-25-4" cx="156.22" cy="140.5" rx="1.96" ry="3"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-26-2{fill-opacity:0.6;}.cls-26-2,.cls-26-3,.cls-26-4,.cls-26-5,.cls-26-6{stroke:#000;}.cls-26-2,.cls-26-3,.cls-26-4,.cls-26-6{stroke-linecap:round;stroke-linejoin:round;}.cls-26-3{fill:#461917;}.cls-26-3,.cls-26-4,.cls-26-5{stroke-width:0.5px;}.cls-26-4,.cls-26-5{fill:#f9ec31;}.cls-26-5{stroke-miterlimit:10;}.cls-26-6{fill:none;}""",
|
||||
"""<g id="eyes-07"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M99.5,142.5s0-10,12-14,26-4,26-4,20-1,25,2,5,9,5,11-2,11-19,13-33,3-33,3S99.5,153.5,99.5,142.5Z"/><path class="cls-26-2" d="M99.5,142.5s0-10,12-14,26-4,26-4,20-1,25,2,5,9,5,11-2,11-19,13-33,3-33,3S99.5,153.5,99.5,142.5Z"/><path class="cls-26-3" d="M107.16,130.47S105,133,105,138s2,9,8,10,20.5-1.5,20.5-1.5,11-2,18-2,15.74-2.78,15.87-5.89-.17-7.11-2.52-10.11-7.12-3.56-10.24-3.78-12.52-.54-14.82-.38-10.17.47-12.23.81-7.31,1.1-7.31,1.1l-7.61,1.87S107.83,129.44,107.16,130.47Z"/><path class="cls-26-4" d="M109,135s-1,11,10,10,10-10,10-10,0-6-3-8-2.85-1.25-2.85-1.25-11.28,2.64-12.21,2.94S109,133,109,135Z"/><path class="cls-26-5" d="M142,125s-3,2-3,7,3.5,10.5,9.5,10.5,12-4,12-10-3.66-7.5-3.66-7.5l-4.78-.47-4.22-.16h-3.44l-1.91.13Z"/><path d="M119.42,133s-1.17,0-1.17,2.5,1.17,2.5,1.17,2.5a2.51,2.51,0,0,0,0-5Z"/><path d="M150.08,130a1.8,1.8,0,0,0-1.82,1.94c-.07,2,.78,3,1.65,3.06s1.78-.94,1.85-2.94a1.8,1.8,0,0,0-1.68-2.06"/><path class="cls-26-6" d="M99.5,142.5s0-10,12-14,26-4,26-4,20-1,25,2,5,9,5,11-2,11-19,13-33,3-33,3S99.5,153.5,99.5,142.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-27-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-27-2,.cls-27-3,.cls-27-4{stroke:#000;}.cls-27-3,.cls-27-4{fill:#461917;stroke-linecap:round;stroke-linejoin:round;}.cls-27-3{stroke-width:0.5px;}.cls-27-4{stroke-width:0.75px;}""",
|
||||
"""<g id="eyes-08"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M110,135s-1.5,6.5.5,6.5h45v-6s0-2-3-2h-38C111.5,133.5,110.5,133.5,110,135Z"/><path class="cls-27-2" d="M110,135s-1.5,6.5.5,6.5h45v-6s0-2-3-2h-38C111.5,133.5,110.5,133.5,110,135Z"/><path class="cls-27-3" d="M110.83,133.88a25.17,25.17,0,0,0,2.67,4.62c1,1,5,0,9,0h31s2-.12,2,.94V135.5s0-2-3-2H113.31Z"/><line class="cls-27-4" x1="113.5" y1="138.5" x2="109.9" y2="141.21"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-28-2{fill:none;}.cls-28-2,.cls-28-3,.cls-28-4,.cls-28-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-28-2,.cls-28-4,.cls-28-6{stroke-width:0.75px;}.cls-28-3{fill:#461917;stroke-width:0.5px;}.cls-28-4{fill-opacity:0.4;}.cls-28-5{fill:#fff100;}.cls-28-6{fill:#fff;fill-opacity:0.2;}""",
|
||||
"""<g id="eyes-09"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M127.5,124.5s-43-1-45,16c-1.5,15.5,25,13,25,13h35s27,0,27-13S158.5,123.5,127.5,124.5Z"/><path class="cls-28-2" d="M127.5,124.5s-43-1-45,16c-1,15,21,13,25,13h35s27,0,27-13S158.5,123.5,127.5,124.5Z"/><path class="cls-28-3" d="M126,132c-15.5.5-28.5-1.5-30.5,6.5-2,9,16,7,22,7s27-1,30-1,10-1,10-3C157.5,133.5,142.5,131.5,126,132Z"/><path class="cls-28-4" d="M90.5,140.5c0,1-5,9,32,8s35-4.33,35-7.67-8.64-9.23-21.32-8.78S95.5,128.5,90.5,140.5Z"/><ellipse class="cls-28-5" cx="112" cy="139" rx="2.5" ry="3.5"/><ellipse class="cls-28-5" cx="138" cy="139" rx="2.5" ry="3.5"/><path class="cls-28-6" d="M93.5,129.5s27-3,41-2c11,0,24,3,29,10s1.39,10.59-.3,11.8,6.3-2.8,6.3-8.8-1.61-11.29-14.3-14.65-30.2-1.18-33.95-1.26S98.5,126.5,93.5,129.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-29-2{fill:#fff;}.cls-29-2,.cls-29-4{fill-opacity:0.4;}.cls-29-3{fill:none;}.cls-29-3,.cls-29-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-29-4{stroke-width:0.75px;}""",
|
||||
"""<path id="fill_color" data-name="fill color" class="cls-fill-1" d="M129.5,123.5s-30,3-31,15,8,13,15,14,20-2,26-3,25,1,27-13-17-14-17-14Z"/><path class="cls-29-2" d="M116.92,125.8s-15.42,2.7-16.42,12.7,15.5,13,24.25,10.5A97.37,97.37,0,0,1,150,145.44c6.5.06,16.78-3.69,16.64-11.31S157.5,122.5,149.5,122.5s-21.12,1.13-21.12,1.13Z"/><path class="cls-29-2" d="M130.5,123.5l-8,25.5c1.6-.06,5.38-.65,9-1.2l7-24.8A47.26,47.26,0,0,1,130.5,123.5Z"/><polygon class="cls-29-2" points="120.77 124.91 113 150 118 150 125.41 124.06 120.77 124.91"/><path class="cls-29-3" d="M129.5,123.5s-30,3-31,15,8,13,15,14,20-2,26-3,25,1,27-13-17-14-17-14Z"/><path class="cls-29-4" d="M106.61,129.46s-6.11,3-6.11,9,5,13,20,11,23-4,28-4,17.29-2.5,18.15-10.25c0,0,.3,10.92-14.42,13.09s-16.25,1.2-21,2.68-28.06,4.23-32.4-7.15C98.84,143.87,95.71,134.43,106.61,129.46Z"/>"""
|
||||
)
|
||||
)
|
||||
|
||||
private val faces: List<Part> = listOf(
|
||||
Part(
|
||||
""".cls-30-2{fill:#fff;fill-opacity:0.4;}.cls-30-3,.cls-30-4{fill:none;}.cls-30-3,.cls-30-4,.cls-30-6{stroke:#000;}.cls-30-3,.cls-30-6{stroke-linecap:round;stroke-linejoin:round;}.cls-30-4{stroke-miterlimit:10;}.cls-30-5,.cls-30-6{fill-opacity:0.2;}.cls-30-6{stroke-width:0.75px;}""",
|
||||
"""<g id="face-01"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M144.5,87.5s-51,3-53,55c0,0,0,27,5,42s10,38,10,38l16,16,1,5s13.5-1.5,19-1,14,2,14,2,6-13,19-19,18-8,18-8-4-35-4-52S201.5,88.5,144.5,87.5Z"/><path class="cls-30-2" d="M115.14,97.69s6.36,5.81-2.64,14.81-16,23-13,44,10,40,10,50-.67,18.19-.67,18.19l-2.33-2.19s-15-45-15-76.5S115.14,97.69,115.14,97.69Z"/><path class="cls-30-3" d="M144.5,87.5s-51,3-53,55c0,0,0,27,5,42s10,38,10,38l16,16,1,5s9-1,15-1,19,1,19,1a38.06,38.06,0,0,1,18-18c13-6,18-8,18-8s-4-35-4-52S201.5,88.5,144.5,87.5Z"/><path class="cls-30-4" d="M144.5,86.5s-51,3-53,55c0,0,0,27,5,42s10,38,10,38l16,16,1,5s9-1,15-1,19,1,19,1a38.06,38.06,0,0,1,18-18c13-6,18-8,18-8s-4-35-4-52S201.5,87.5,144.5,86.5Z"/><path class="cls-30-5" d="M158.5,92.5s20,15,18,32-8,28-12,29a19.27,19.27,0,0,1,8,16c0,11,1,50,1,50l.34,6.83,19.66-8.83s-3.77-39-3.38-63.49,1.38-55.13-31.62-64.82C158.5,89.19,155.5,90.5,158.5,92.5Z"/><path class="cls-30-3" d="M124.5,211.5l37-1,4.76,21.18s-8.76,8.82-8.76,11.82c-2,1-10-1-18-1a147.84,147.84,0,0,0-16,1S118.5,224.5,124.5,211.5Z"/><path class="cls-30-6" d="M159.5,212.5a19.89,19.89,0,0,0-4,14c1,8,2,17,2,17l9-12-5-21Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-31-2,.cls-31-4{fill-opacity:0.2;}.cls-31-3{fill:none;}.cls-31-3,.cls-31-4{stroke:#000;stroke-miterlimit:10;}.cls-31-4,.cls-31-5{fill:#fff;}.cls-31-4{stroke-width:0.75px;}.cls-31-5{fill-opacity:0.4;}""",
|
||||
"""<g id="face-02"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M147.5,88.5s-64,9-63,72c0,0-6,51,54,50,0,0,66-6,65-53S172,87,147.5,88.5Z"/><path class="cls-31-2" d="M154.5,90.5s18,12,25,47,2,48-7,55-24.12,17.37-42.56,17.68c0,0,56.6,1.84,70.58-33.42.73-1.85,1.84-5.7,1.84-5.7S185.5,158.5,183.17,137c-1.67-14.45,2.33-24.45,6.49-24.63a7,7,0,0,0-.63-1.08C179.68,98,160,87,154.5,90.5Z"/><path class="cls-31-3" d="M147.5,87.5s-64,9-63,72c0,0-6,51,54,50,0,0,66-6,65-53S170.5,85.5,147.5,87.5Z"/><path class="cls-31-3" d="M147.5,88.5s-64,9-63,72c0,0-6,51,54,50,0,0,66-6,65-53S171.5,86.5,147.5,88.5Z"/><path class="cls-31-4" d="M92.22,125.67s2.28,14.83.28,24.83-7.67,21-7.67,21S80.94,147.85,92.22,125.67Z"/><path class="cls-31-4" d="M187.5,113.5s-7,10-4,26,12,27.67,18.5,29.83c0,0,5.83-35.17-12.83-57Z"/><path class="cls-31-5" d="M112.5,112.5s-5-1-10,6-6,11-5,13,7,1,11-7S114.5,114.5,112.5,112.5Z"/><path class="cls-31-5" d="M117.5,102.5s-5,4-2,7a6.31,6.31,0,0,0,9,0c2-2,2-6-1-7A10.56,10.56,0,0,0,117.5,102.5Z"/><circle cx="188" cy="143" r="1.5"/><circle cx="193.5" cy="154.5" r="1.5"/><circle cx="199.5" cy="161.5" r="1.5"/><circle cx="88.5" cy="154.5" r="1.5"/><circle cx="89.5" cy="143.5" r="1.5"/><circle cx="90.5" cy="133.5" r="1.5"/><circle cx="186.5" cy="130.5" r="1.5"/><circle cx="190.5" cy="119.5" r="1.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-32-2{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-32-3{fill-opacity:0.2;}.cls-32-4{fill:#fff;fill-opacity:0.4;}""",
|
||||
"""<g id="face-03"><path class="cls-fill-1" d="M147,88c-20.15-.5-56.5,14.5-56.5,57.5,0,0,1,22,6,41s10,34,10,37c0,0,3,12,25,11s62-15,62-15-2-31-3-49S196.79,89.24,147,88Z"/><path class="cls-32-2" d="M147,88c-20.15-.5-56.5,14.5-56.5,57.5,0,0,1,22,6,41s10,34,10,37c0,0,3,12,25,11s62-15,62-15-2-31-3-49S196.79,89.24,147,88Z"/><path class="cls-32-2" d="M147,87c-20.15-.5-56.5,14.5-56.5,57.5,0,0,1,22,6,41s10,34,10,37c0,0,3,12,25,11s62-15,62-15-2-31-3-49S196.79,88.24,147,87Z"/><path class="cls-32-3" d="M135.5,96.5s36-4,45,43c0,0,3,24,3,35s-1,25,1,36l2,11,7-2s-2.53-55.76-3.27-70.88S188.5,90.81,150,88.16c0,0-19.78-1-33.64,8.7,0,0-4.86,4.65.14,2.65S132.5,96.5,135.5,96.5Z"/><path class="cls-32-4" d="M122.5,104.5s-4,1-4,4,2,7,5,6,5-5,4-7S125.5,103.5,122.5,104.5Z"/><path class="cls-32-4" d="M115.5,111.5s-9-6-17,12-1,37-1,37,2,6,8,6,4-9,4-9-4-13-1-24,7-13,7-13S120.5,115.5,115.5,111.5Z"/><circle cx="111" cy="223" r="1.5"/><circle cx="125.5" cy="227.5" r="1.5"/><circle cx="141.5" cy="226.5" r="1.5"/><circle cx="159.5" cy="223.5" r="1.5"/><circle cx="175.5" cy="219.5" r="1.5"/><circle cx="188.5" cy="214.5" r="1.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-33-2{fill-opacity:0.2;}.cls-33-3{fill:#fff;fill-opacity:0.4;}.cls-33-4{fill:none;stroke:#000;stroke-miterlimit:10;}""",
|
||||
"""<g id="face-04"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M148,87s-56.5.5-56.5,63.93,33,84.57,48,84.57,45-8.05,51-41.28c0,0,1-16.11,0-25.17s-1-29.2-1-32.22S186.5,88.51,148,87Z"/><path class="cls-33-2" d="M151.5,92.5s17,12,22,42,7,44,3,63S167,226,161.75,229.25s21.67-4.94,26.71-26.84,1.77-35.94,1.77-35.94l-1-32.24s-5-35.64-26.88-43.18c0,0-11.87-4.54-17.37-3S148.5,89.5,151.5,92.5Z"/><path class="cls-33-3" d="M113.5,110.5s-4,0-4,4,4,7,7,5,5-5,4-7S118.5,108.5,113.5,110.5Z"/><path class="cls-33-3" d="M108.5,123.5s-4-1-7,5-4,15-3,24a33.42,33.42,0,0,0,8,18c2,2,6,3,5-4s-5-14-4-25S114.5,124.5,108.5,123.5Z"/><path class="cls-33-4" d="M148,87s-56.5.5-56.5,63.5,33,84,48,84,45-8,51-41c0,0,1-16,0-25s-1-29-1-32S186.5,88.5,148,87Z"/><path class="cls-33-4" d="M148,86s-56.5.5-56.5,63.5,33,84,48,84,45-8,51-41c0,0,1-16,0-25s-1-29-1-32S186.5,87.5,148,86Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-34-2,.cls-34-3{fill:#fff;}.cls-34-2{fill-opacity:0.4;}.cls-34-3,.cls-34-5{fill-opacity:0.2;}.cls-34-3,.cls-34-4{stroke:#000;stroke-miterlimit:10;}.cls-34-4{fill:none;}""",
|
||||
"""<g id="face-05"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M91.5,107.5s4,64,4,77,1,38,1,38,4,11,22,10c36.5.5,60-11,66-16,0,0-3-106-3-116s4-20-36-22c0,0-35.59,1.45-44.8,8.73,0,0-10.2,2.27-9.2,7.27S91.5,107.5,91.5,107.5Z"/><path class="cls-34-2" d="M95.8,102s2.7,53.53,3.7,68.53,5,42,5,45v14.71s-7-3.71-8-7.71-3.65-88.21-3.83-95.61S91.5,107.5,91.5,107.5v-13S91.1,100.43,95.8,102Z"/><path class="cls-34-3" d="M145.5,78.5s-53,5-54,16,23,14,37,13,52-7,53-16S156.5,77.5,145.5,78.5Z"/><path class="cls-34-4" d="M91.5,107.5s4,64,4,77,1,38,1,38,3,9,22,10c32,2,60-11,66-16,0,0-3-106-3-116s4-20-36-22c-16,.67-31.14,3.13-44.8,8.73-4.7,1.42-7.78,3.83-9.2,7.27Z"/><path class="cls-34-5" d="M168.5,103.5s1,49,1,57,1,35,1,41-1,22.92-1,22.92l15-7.92s-2.94-96.67-3-100.83,0-25.17,0-25.17c.34,3.51-3.36,6.57-11,9.17l-2,.72Z"/><circle cx="148" cy="114" r="1.5"/><circle cx="130.5" cy="116.5" r="1.5"/><circle cx="114.5" cy="117.5" r="1.5"/><circle cx="116.5" cy="225.5" r="1.5"/><circle cx="101.5" cy="220.5" r="1.5"/><circle cx="132.5" cy="225.5" r="1.5"/><circle cx="150.5" cy="221.5" r="1.5"/><circle cx="166.5" cy="217.5" r="1.5"/><circle cx="179.5" cy="212.5" r="1.5"/><circle cx="99.5" cy="112.5" r="1.5"/><circle cx="164.5" cy="109.5" r="1.5"/><circle cx="177.5" cy="104.5" r="1.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-35-2{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-35-3{fill-opacity:0.2;}.cls-35-4{fill:#fff;fill-opacity:0.4;}""",
|
||||
"""<g id="face-06"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M156,83s-55,5-66,53,9.5,66.5,9.5,66.5,9,8,12,8,2-2,1-4a7.47,7.47,0,0,0-3-3s4,1,5,10,1,32,1,38v12s2,8,17,8,30-7,30-7-4-34-2-41,9-20,15-26,3-1,0,2a50.18,50.18,0,0,0-6,8s17-7,23-14,15-10,16-33S214.5,81.5,156,83Z"/><path class="cls-35-2" d="M156,83s-55,5-66,53,9.5,66.5,9.5,66.5,9,8,12,8,2-2,1-4a7.47,7.47,0,0,0-3-3s4,1,5,10,1,32,1,38v12s2,8,17,8,30-7,30-7-4-34-2-41,9-20,15-26,3-1,0,2a50.18,50.18,0,0,0-6,8s17-7,23-14,15-10,16-33S214.5,81.5,156,83Z"/><path class="cls-35-2" d="M156,82s-55,5-66,53,9.5,66.5,9.5,66.5,9,8,12,8,2-2,1-4a7.47,7.47,0,0,0-3-3s4,1,5,10,1,32,1,38v12s2,8,17,8,30-7,30-7-4-34-2-41,9-20,15-26,3-1,0,2a50.18,50.18,0,0,0-6,8s17-7,23-14,15-10,16-33S214.5,80.5,156,82Z"/><path class="cls-35-3" d="M181.5,93.5s18,16,16,44-6,41-28,54c0,0-16,3-19,28s0,49.27,0,49.27l12-4.27s-6.87-36.09.07-46.54,7.21-11.15,17.07-15.3,30.94-21.59,29.4-50.87-.09-56-35.32-67.16l-3.94-.77S168.5,84.5,181.5,93.5Z"/><path class="cls-35-4" d="M110.5,117.5s-7,1-10,11-11,25-8,35,7,10,9,5,1-17,5-28S116.5,119.5,110.5,117.5Z"/><path class="cls-35-4" d="M122.5,99.5s-9,6-9,9,0,4,5,4,13-4,13-9S128.5,96.5,122.5,99.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-36-2,.cls-36-6{fill:#fff;}.cls-36-2{fill-opacity:0.4;}.cls-36-3,.cls-36-4{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-36-3{stroke-width:2px;}.cls-36-5,.cls-36-6{fill-opacity:0.2;}""",
|
||||
"""<g id="face-07"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M71.5,130.5a51.83,51.83,0,0,0,6,29c8,15,39,71,66,70s76-50,76-93-38-54-77-53S73.5,102.5,71.5,130.5Z"/><path class="cls-36-2" d="M116.5,88s-24.18,5.5-35.59,20.5-9.79,26-9.1,34.48,8.2,22.55,14.44,33,18.64,28.81,22.95,32.65,11.3,11.84,11.3,11.84.3-6.17-5.85-18.58S83.5,164,81.5,143.74,87.5,101.49,116.5,88Z"/><path class="cls-36-3" d="M71.5,130.5a51.83,51.83,0,0,0,6,29c8,15,39,71,66,70s76-50,76-93-38-54-77-53S73.5,102.5,71.5,130.5Z"/><path class="cls-36-4" d="M75.44,115.88S108.5,100.5,140.5,101.5s65,7,67,30-3,34-9,50a89.16,89.16,0,0,1-16.32,27"/><path d="M80,118.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S80,118.5,80,118.5Z"/><path d="M73,133.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S73,133.5,73,133.5Z"/><path d="M98,112.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S98,112.5,98,112.5Z"/><path d="M118,109.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S118,109.5,118,109.5Z"/><path d="M143,107.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S143,107.5,143,107.5Z"/><path d="M164,107.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S164,107.5,164,107.5Z"/><path d="M184,112.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S184,112.5,184,112.5Z"/><path d="M201,125.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S201,125.5,201,125.5Z"/><path d="M202,149.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S202,149.5,202,149.5Z"/><path d="M195,173.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S195,173.5,195,173.5Z"/><path d="M185,195.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S185,195.5,185,195.5Z"/><path d="M169,210.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S169,210.5,169,210.5Z"/><path d="M141,220.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S141,220.5,141,220.5Z"/><path d="M115,209.5s-1.5,0-1.5,2,1.5,2,1.5,2,1.5,0,1.5-2S115,209.5,115,209.5Z"/><path class="cls-36-5" d="M199.5,115.5l-9-5.07S188,110,191,120s6.5,25.5,4.5,40.5-7,27-19,40-17.37,20.78-17.19,23.89S184.5,208.5,188.5,202.5s11.93-23.83,16-39S211.5,124.5,199.5,115.5Z"/><path class="cls-36-6" d="M201,100l-10,10s-23.39-8.18-41.7-8.09-33.49-1.25-53.4,6.42L76,116s-.08-2.83,6.71-9.66,18-18.34,48.89-22.09,49.94,2.2,58.92,6.73,12,7.09,12,7.09Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-37-2{fill:#fff;fill-opacity:0.4;}.cls-37-3{fill:none;}.cls-37-3,.cls-37-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-37-4{fill-opacity:0.2;}""",
|
||||
"""<g id="face-08"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M212.5,90.5s-19.5-20.5-55-12c-31,8-61,14-64,60a122.71,122.71,0,0,0,4,35c5,19,11,42,12,45,0,0,8,9,25,9s56-10,56-10,11-5,12-6-20-60-11-86c0,0,3-7,14-8s14,0,14,0S217.5,96.5,212.5,90.5Z"/><path class="cls-37-2" d="M130.5,88.5s-9,6-12,9-16,15-18,28,2,38,4,46,5,21,4,26-2.93,6.66-2.93,6.66S99.5,183.5,97.5,173.5s-6-27-1.52-51S130.5,85.5,130.5,88.5Z"/><path class="cls-37-3" d="M212.5,90.5s-18.25-20.21-55-12c-31.25,7-61,14-64,60a122.71,122.71,0,0,0,4,35c5,19,11,42,12,45,0,0,8,9,25,9s56-10,56-10,11-5,12-6-20-60-11-86c0,0,3-7,14-8s14,0,14,0S217.5,96.5,212.5,90.5Z"/><path class="cls-37-3" d="M212.5,89.5s-18-19-55-12c-31.23,7.06-61,14-64,60a122.71,122.71,0,0,0,4,35c5,19,11,42,12,45,0,0,8,9,25,9s56-10,56-10,11-5,12-6-20-60-11-86c0,0,3-7,14-8s14,0,14,0S217.5,95.5,212.5,89.5Z"/><path class="cls-37-4" d="M209.5,92.5s-15,0-21,6-16,16-13,41,12,65,12,65l3,13,12-6s-22.77-76.47-7.39-90.23c0,0,2.39-4.77,24.39-3.77,0,0-.4-21-5.7-25Z"/><circle cx="211" cy="103" r="1.5"/><circle cx="195.5" cy="207.5" r="1.5"/><circle cx="182.5" cy="213.5" r="1.5"/><circle cx="165.5" cy="217.5" r="1.5"/><circle cx="146.5" cy="220.5" r="1.5"/><circle cx="129.5" cy="220.5" r="1.5"/><circle cx="114.5" cy="216.5" r="1.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-38-2{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-38-3,.cls-38-6{fill:#fff;}.cls-38-3,.cls-38-4{fill-opacity:0.2;}.cls-38-5{fill-opacity:0.1;}.cls-38-6{fill-opacity:0.4;}""",
|
||||
"""<g id="face-09"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M143,89s-39,6-64,66,15.5,70.5,34.5,74.5,85,13,102-27S175.5,85.5,143,89Z"/><path class="cls-38-2" d="M143,88s-39,6-64,66,15.5,70.5,34.5,74.5,85,13,102-27S175.5,84.5,143,88Z"/><path class="cls-38-3" d="M99,121.07S122,96,139.5,98.5a137.38,137.38,0,0,1,21.88,7.59s4.12-7.59,6.12-8.59-17-9-23-9C133,89,109.5,104.64,99,121.07Z"/><path class="cls-38-2" d="M143,89s-39,6-64,66,15.5,70.5,34.5,74.5,85,13,102-27S175.5,85.5,143,89Z"/><path class="cls-38-4" d="M161.38,106.09a20.31,20.31,0,0,0-1.88,4.41c-1,6,2,11,9,19s20,27,21,45,1,35-13,45-24.21,14.08-24.21,14.08,34.71.42,47.8-19.51c11.41-15.58,10.59-36,0-59.77-11.09-27.31-33.81-44.6-33.81-44.6Z"/><path class="cls-38-5" d="M167.41,97.44c-2.41-.44-5.91,9.06-5.91,9.06s27,14,39,48,8.86,41,5.43,50-17.62,24.63-36,27.81,37.6-2.82,45.6-29.82c11.5-39.5-22.87-80.23-22.87-80.23S175,101,167.41,97.44Z"/><path class="cls-38-6" d="M91.5,140.5s-14,24-12,43,12,26,19,31,9.2,13.33,9.2,13.33S72,224,71.08,186.84c0,0,3.19-39.76,30.31-68.55L113.22,108S95.5,132.5,91.5,140.5Z"/><circle cx="138.5" cy="106.5" r="2"/><circle cx="111" cy="115" r="2"/><circle cx="164" cy="117" r="2"/><circle cx="185" cy="136" r="2"/><circle cx="194" cy="158" r="2"/><circle cx="201" cy="181" r="2"/><circle cx="200" cy="205" r="2"/><circle cx="183" cy="218" r="2"/><circle cx="164" cy="223" r="2"/><circle cx="142" cy="223" r="2"/><circle cx="118" cy="221" r="2"/><circle cx="97" cy="218" r="2"/><path class="cls-38-2" d="M139.5,98.5s35,3,57,48,13.83,72-24.08,85c-54.92,7-79.92-7-91-16.46-1.5.92-21-20.3-3-60.55C95.74,116,122.5,97.5,139.5,98.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-39-2{fill:#fff;fill-opacity:0.4;}.cls-39-3{fill:none;}.cls-39-3,.cls-39-4{stroke:#000;stroke-miterlimit:10;}.cls-39-4{fill-opacity:0.2;stroke-width:0.75px;}""",
|
||||
"""<path id="fill_color" data-name="fill color" class="cls-fill-1" d="M177,71s-73-2-82,51c0,0-4.5,17.5,1.5,40.5s14,50,14,50,6,10,46,6,75-25,75-25-10-100-16-108S203,72,177,71Z"/><path class="cls-39-2" d="M135.5,78.5s1,4-5,9-20,12-25,31,0,29,2,37,7,25,6,35-4.75,16-4.75,16-21.38-60.92-12.82-89S129,81,135.5,78.5Z"/><path class="cls-39-3" d="M177,71s-73-2-82,51c0,0-4.5,17.5,1.5,40.5s14,50,14,50,6,10,46,6,75-25,75-25-10-100-16-108S200.5,70.5,177,71Z"/><path class="cls-39-3" d="M177,70s-73-2-82,51c0,0-4.5,17.5,1.5,40.5s14,50,14,50,6,10,46,6,75-25,75-25-10-100-16-108S200.5,69.5,177,70Z"/><path class="cls-39-4" d="M205.5,86.5s-27,2-29,28,9,73,9,73l5.38,23.67L231.5,193.5s-8-94-15-106C216.48,87.51,208.5,86.5,205.5,86.5Z"/><circle cx="147.5" cy="214.5" r="1.5"/><circle cx="165.5" cy="211.5" r="1.5"/><circle cx="183.5" cy="206.5" r="1.5"/><circle cx="196.5" cy="202.5" r="1.5"/><circle cx="208.5" cy="198.5" r="1.5"/><circle cx="219.5" cy="194.5" r="1.5"/><circle cx="228.5" cy="190.5" r="1.5"/><circle cx="130.5" cy="214.5" r="1.5"/><circle cx="115.5" cy="210.5" r="1.5"/>"""
|
||||
)
|
||||
)
|
||||
|
||||
private val mouths: List<Part> = listOf(
|
||||
Part(
|
||||
""".cls-40-2{fill-opacity:0.4;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}""",
|
||||
"""<g id="mouth-01"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M123,183l-1,9a26.74,26.74,0,0,0,3.5.5c2,.44,22,.35,23-1l-1-9S132.5,181.5,123,183Z"/><path class="cls-40-2" d="M123,183l-1,9a26.74,26.74,0,0,0,3.5.5c2,.44,22,.35,23-1l-1-9S132.5,181.5,123,183Z"/><path d="M123.5,183.34s3.07,2.66,12.26,2.66S147,182.45,147,182.45a113.13,113.13,0,0,0-12.2-.28C129.63,182.45,124.52,182.45,123.5,183.34Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-41-2,.cls-41-4{fill:none;}.cls-41-2,.cls-41-3,.cls-41-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-41-3{fill-opacity:0.4;}.cls-41-3,.cls-41-4{stroke-width:0.75px;}""",
|
||||
"""<g id="mouth-02"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M126,180a129.54,129.54,0,0,1,3,25,19.93,19.93,0,0,0,10,1,24.23,24.23,0,0,0,8.5-3.5s-4-23-4-25c0,0-6,2-9,2-1.54,0-3.73.13-5.51.26S126,180,126,180Z"/><path class="cls-41-2" d="M126,180a129.54,129.54,0,0,1,3,25,19.93,19.93,0,0,0,10,1,24.23,24.23,0,0,0,8.5-3.5s-4-23-4-25c0,0-6,2-9,2-1.54,0-3.73.13-5.51.26S126,180,126,180Z"/><path class="cls-41-3" d="M131.5,190.71a113.49,113.49,0,0,1,1,12.25l-3,1.54s-1-14-2.28-18.07c-.72-2.86-1.15-6.11-1.15-6.11l3.43.18S130.5,182.55,131.5,190.71Z"/><path class="cls-41-3" d="M129.29,205.11l3.21-1.61a32,32,0,0,0,7-1,53.36,53.36,0,0,0,7.56-2.57l.44,2.57s-4.93,3.27-7,3.14S138.07,207.71,129.29,205.11Z"/><path class="cls-41-4" d="M131.81,193.42c.93.47,1.84.92,1.69-.92,0,0,0-2,1-2s7-1,7-1a1,1,0,0,1,1,1c0,1,0,3,1,2a3.44,3.44,0,0,1,2.12-1"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-42-2{fill-opacity:0.4;}.cls-42-2,.cls-42-5,.cls-42-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-42-3{fill:#ee4036;}.cls-42-4{fill:#f05a28;}.cls-42-5{fill:#faaf40;stroke-width:0.75px;}.cls-42-6{fill:none;}""",
|
||||
"""<g id="mouth-03"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M170.5,168.5S151,177,141,179l-1,.2a112,112,0,0,1-32.68,1.74q-1.88-.18-3.82-.44l6,20a51.86,51.86,0,0,0,26,5c15-1,35-7,41-15Z"/><path class="cls-42-2" d="M110.5,199.5l2-2s-3-10-4-13-1.87-3.63-1.87-3.63l-3.13-.37,3.38,11.26,2.5,8.33Z"/><path class="cls-42-3" d="M118,181s3.13,7.7,4.06,9.85,2.31,5.09,2.12,7.12h0l.19,4a72.8,72.8,0,0,1,7.93.57C135,203,144,201,144,201a14.36,14.36,0,0,1-.15-3c.15-1-3.27-16.16-3.27-16.16l-1-2.27L135,180l-9.08,1.13Z"/><path class="cls-42-4" d="M155.48,174.52,159,185.15l2,9.85v2s10.25-4.55,12.63-6.27L176,189l-3.63-13.65-1.85-6.79Z"/><path class="cls-42-2" d="M113.5,198.5s2,3,14,4,35-6,35-6S174.56,191,176,188.77l.47,1.73s-7.38,11-40.69,15c0,0-16.31,1-26.31-5l3-3Z"/><path class="cls-42-5" d="M119.94,181.44s2.56,10.06,3.56,13.06a27.84,27.84,0,0,1,.88,7.67s-7.88.33-11.88-4.67l-5-15a5.38,5.38,0,0,0-1-1.7A80.36,80.36,0,0,0,119.94,181.44Z"/><path class="cls-42-5" d="M140.5,181.5c0,.14,4.14,18,3.56,19.5,0,0,15.93-2.76,17.18-4.13,0,0-1.75-10.37-2.75-13.37s-3-9-3-9-7.82,3-10.4,3.47a54.09,54.09,0,0,0-5.58,1.51A6,6,0,0,1,140.5,181.5Z"/><path class="cls-42-6" d="M170.5,168.5S151,177,141,179l-1,.2a112,112,0,0,1-32.68,1.74q-1.88-.18-3.82-.44l6,20a51.86,51.86,0,0,0,26,5c15-1,35-7,41-15Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-43-2{fill:#f9ec31;}.cls-43-3{fill:#faaf40;}.cls-43-4,.cls-43-6{fill:none;}.cls-43-4,.cls-43-5,.cls-43-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-43-5{fill-opacity:0.4;}.cls-43-5,.cls-43-6{stroke-width:0.75px;}""",
|
||||
"""<g id="mouth-04"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M104,173s5.5,23.5,5.5,27.5c0,0,6,5,24,5s41-10,43-14c0,0-4-29-5-30,0,0-25,11-45,12-10.61.62-16.74.27-19.85-.08A20.06,20.06,0,0,1,104,173Z"/><path class="cls-43-2" d="M106.65,173.42s3.08,5.1,4.72,14.34,1,10.85,1,10.85S123,203,128,203a73,73,0,0,0,18-1.77,131.07,131.07,0,0,0,22.47-7.33l7.93-3.29-3.18-21.13-1.7-8s-26,11-45,12-20,0-20,0"/><path class="cls-43-3" d="M142.5,181.5V190l17.5-4,1,6v4.9l15-5.9-3-20.5C173.15,170.88,153.19,178.85,142.5,181.5Z"/><path class="cls-43-3" d="M142.4,190a38.22,38.22,0,0,0,2.1,11.5s-10.5,1.5-17,1S113,199,113,199l-2.5-15.5h11L123,191A122,122,0,0,0,142.4,190Z"/><path class="cls-43-3" d="M118,174l3.5,9c7.62.33,21-1.5,21-1.5s-1.13-7.27-2.5-9.5Z"/><path class="cls-43-4" d="M104,173s5.5,23.5,5.5,27.5c0,0,6,5,24,5s41-10,43-14c0,0-4-29-5-30,0,0-25,11-45,12-10.61.62-16.74.27-19.85-.08A20.06,20.06,0,0,1,104,173Z"/><path class="cls-43-5" d="M112.33,198.61S122,203,130,203s23.5-2.5,32.5-6.5,13.88-5.89,13.88-5.89l.12.89a31.45,31.45,0,0,1-8.77,5.75c-5.23,2.25-19.73,8.25-35.48,8.25s-22.75-5-22.75-5l2.83-1.89"/><path class="cls-43-5" d="M107.5,174.5a93.08,93.08,0,0,1,4,14,67.34,67.34,0,0,1,1,10l-3,2s-2.2-15.2-4.6-23.6l-.9-3.69,2.93.25Z"/><path class="cls-43-6" d="M124.61,202.46c.11-5-2.11-16-4.11-22s-3.42-6.66-3.42-6.66"/><path class="cls-43-6" d="M140.25,171.58s2.25,5.92,2.25,9.92v8s.33,10.83,2.67,11.92"/><path class="cls-43-6" d="M112,191s17,1,30-1,30-7,30-7l3-1"/><path class="cls-43-6" d="M142.5,181.5v8.42l17.51-3.7,1,8.28V194s-1-9-2-14-2.84-12.76-2.84-12.76"/><path class="cls-43-6" d="M176.5,190.5,173,170c-1.23,1.23-7.76,4-15.16,6.6-4.63,1.64-9.61,3.24-13.84,4.4-11,3-33.55,2.5-33.55,2.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-44-2{fill:none;}.cls-44-2,.cls-44-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-44-3{opacity:0.4;}.cls-44-4{fill:#461917;}.cls-44-5{fill-opacity:0.4;stroke-width:0.75px;}""",
|
||||
"""<g id="mouth-05"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M130.5,175.5v15a73.09,73.09,0,0,0,1,13s6,1,6,0-1-28-1-28C134.18,175.38,131.28,175.06,130.5,175.5Z"/><path class="cls-44-2" d="M130.5,175.5v15a73.09,73.09,0,0,0,1,13s6,1,6,0-1-28-1-28C134.18,175.38,131.28,175.06,130.5,175.5Z"/><path class="cls-44-3" d="M131.65,203a75.68,75.68,0,0,1-1.4-15.06,102.33,102.33,0,0,1,.48-12.27,3,3,0,0,1,1.25-.25,5.33,5.33,0,0,1,1,.1l.43.2h0s0,.16-.13.72a138.49,138.49,0,0,0-1,14.06,64.14,64.14,0,0,0,1,10.92Z"/><path d="M132,175.67h0a5,5,0,0,1,.87.08l.25.11a3.34,3.34,0,0,1-.09.52v.05a139.54,139.54,0,0,0-1,14.06,64.67,64.67,0,0,0,1,10.83l-1.16,1.16a77,77,0,0,1-1.3-14.56,107.4,107.4,0,0,1,.46-12.1,2.85,2.85,0,0,1,1-.17m0-.5a3.13,3.13,0,0,0-1.48.33s-.5,3-.5,12.44a72,72,0,0,0,1.5,15.56l2-2a62.68,62.68,0,0,1-1-11,138,138,0,0,1,1-14c.13-.56.15-.92,0-1l-.5-.23a5.57,5.57,0,0,0-1-.1Z"/><path class="cls-44-4" d="M137.19,201.56a5.59,5.59,0,0,0-2.48-.65,3.64,3.64,0,0,0-1.34.25,39.16,39.16,0,0,1-1.12-9c0-4.56,1.26-14.93,1.47-16.62l2.54.17.56,15.47Z"/><path d="M133.94,175.83,136,176l.56,15.25.35,9.94a5.61,5.61,0,0,0-2.21-.5,4,4,0,0,0-1.17.17,39,39,0,0,1-1-8.64c0-4.37,1.16-14.11,1.44-16.36m-.44-.53s-1.5,11.86-1.5,16.89a38.68,38.68,0,0,0,1.2,9.31,3.33,3.33,0,0,1,1.51-.35,5.56,5.56,0,0,1,2.74.85l-.38-10.8-.57-15.7-3-.2Z"/><path class="cls-44-5" d="M137.5,201.5v2l-5.7,0,1.7-2S134.5,199.5,137.5,201.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-45-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-45-2,.cls-45-3{stroke:#000;}.cls-45-3{fill:#461917;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5px;}""",
|
||||
"""<g id="mouth-06"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M149,179s-12,2-15,2-13.5,1.5-15.5,5.5-1,11.5,8,10.5,12-1.5,20-2.5,13-4,14-6,0-5-2-7S154.5,178.5,149,179Z"/><path class="cls-45-2" d="M149,179s-12,2-15,2-11.5.5-14.5,4.5c-4,4-2,11.5,7,11.5,8,0,12-1.5,20-2.5s13-4,14-6,0-5-2-7S154.5,178.5,149,179Z"/><path class="cls-45-3" d="M121.5,184.5s-3,4-1,6,4,3,12,2,13-3,18-3,10.83-2.26,9.42-6.13-5.75-5.12-8.58-4.5-12.7,1.53-16.26,2.08-6.7-.1-10.63,1.73S121.5,184.5,121.5,184.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-46-2{fill-opacity:0.4;}.cls-46-3{fill:none;}.cls-46-3,.cls-46-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-46-4{fill:#461917;stroke-width:0.5px;}""",
|
||||
"""<g id="mouth-07"><g id="fill_color" data-name="fill color"><path id="background" class="cls-fill-1" d="M122,180s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,122,180Z"/><path id="background-2" data-name="background" class="cls-fill-1" d="M131,179s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,131,179Z"/><path id="background-3" data-name="background" class="cls-fill-1" d="M141,179s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,141,179Z"/><path id="background-4" data-name="background" class="cls-fill-1" d="M149,178s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,149,178Z"/></g><path class="cls-46-2" d="M122,180s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,122,180Z"/><path class="cls-46-3" d="M122,180s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,122,180Z"/><path class="cls-46-4" d="M124.5,180.5s1,11,2,14a15.77,15.77,0,0,1,1,4s2.34-.11,2.17.44a31,31,0,0,0-.82-8.63,59,59,0,0,1-1.35-10.07v-.75h-3Z"/><line class="cls-46-4" x1="125.85" y1="200.49" x2="127.5" y2="198.5"/><path class="cls-46-2" d="M131,179s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,131,179Z"/><path class="cls-46-3" d="M131,179s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,131,179Z"/><path class="cls-46-4" d="M133.5,179.5s1,11,2,14a15.77,15.77,0,0,1,1,4s2.34-.11,2.17.44a31,31,0,0,0-.82-8.63,59,59,0,0,1-1.35-10.07v-.75h-3Z"/><line class="cls-46-4" x1="134.85" y1="199.49" x2="136.5" y2="197.5"/><path class="cls-46-2" d="M141,179s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,141,179Z"/><path class="cls-46-3" d="M141,179s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,141,179Z"/><path class="cls-46-4" d="M143.5,179.5s1,11,2,14a15.77,15.77,0,0,1,1,4s2.34-.11,2.17.44a31,31,0,0,0-.82-8.63,59,59,0,0,1-1.35-10.07v-.75h-3Z"/><line class="cls-46-4" x1="144.85" y1="199.49" x2="146.5" y2="197.5"/><path class="cls-46-2" d="M149,178s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,149,178Z"/><path class="cls-46-3" d="M149,178s1,10,2,14,1.5,6.5,1.5,6.5,3,0,4-1-2-17-2-18v-2s-1.49,0-3,.11A12,12,0,0,0,149,178Z"/><path class="cls-46-4" d="M151.5,178.5s1,11,2,14a15.77,15.77,0,0,1,1,4s2.34-.11,2.17.44a31,31,0,0,0-.82-8.63,59,59,0,0,1-1.35-10.07v-.75h-3Z"/><line class="cls-46-4" x1="152.85" y1="198.49" x2="154.5" y2="196.5"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-47-2{fill-opacity:0.4;}.cls-47-2,.cls-47-3,.cls-47-4,.cls-47-5{stroke:#000;stroke-miterlimit:10;}.cls-47-3{fill:#f6921e;}.cls-47-4{fill:#f9ec31;stroke-width:0.75px;}.cls-47-5{fill:none;}""",
|
||||
"""<g id="mouth-08"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M148,175s-6.5.5-12.5,1.5a83.3,83.3,0,0,1-12,1v14c0,2,3,13,13,12s13-7,13-11-1-17-1-17h0S148.5,174.5,148,175Z"/><path class="cls-47-2" d="M148,175s-6.5.5-12.5,1.5a83.3,83.3,0,0,1-12,1v14c0,2,3,13,13,12s13-7,13-11-1-17-1-17h0S148.5,174.5,148,175Z"/><path class="cls-47-3" d="M129.59,177.21,130,191s1,7,7,8,12-.83,12.5-4.92-1-19.58-1-19.58l-10.88,1.67C135.5,176.5,129.59,177.21,129.59,177.21Z"/><path class="cls-47-4" d="M137.25,176.19l.22,22.88s-7-.57-7.39-7.64c-.58-4.93-.58-13.93-.58-13.93Z"/><path class="cls-47-5" d="M129.59,177.21,130,191s1,7,7,8,12-.83,12.5-4.92-1-19.58-1-19.58l-10.88,1.67C135.5,176.5,129.59,177.21,129.59,177.21Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-48-2{opacity:0.4;}.cls-48-3,.cls-48-4,.cls-48-5{fill:none;}.cls-48-3,.cls-48-4,.cls-48-5,.cls-48-6,.cls-48-7,.cls-48-8{stroke:#000;}.cls-48-3,.cls-48-5,.cls-48-7{stroke-miterlimit:10;}.cls-48-4,.cls-48-6,.cls-48-8{stroke-linecap:round;stroke-linejoin:round;}.cls-48-4,.cls-48-5{stroke-width:0.5px;}.cls-48-6{fill:#f6921e;stroke-width:1.2px;}.cls-48-7{fill:#f9ec31;}.cls-48-7,.cls-48-8{stroke-width:0.75px;}.cls-48-8{fill:#f05a27;}""",
|
||||
"""<g id="mouth-09"><path id="fill_color" data-name="fill color" class="cls-fill-1" d="M109.5,197.5c0,4,1,5,1,5h34c10,0,21-1,21-1v-23l-1-4h-2c-2,0-54.5.5-54.5.5v11Z"/><path class="cls-48-2" d="M163.5,174.5v25s-52,1.5-53.75,1l.61,1.79,28.9.21s19.65-.79,21.7-.65A9,9,0,0,0,165,201l.41-22.84S164,174,163.5,174.5Z"/><path class="cls-48-3" d="M109.5,197.5c0,4,1,5,1,5h34c10,0,21-1,21-1v-23l-1-4h-2c-2,0-54.5.5-54.5.5v11Z"/><path class="cls-48-4" d="M113.5,200.5h27c10,0,23-1,23-1v-24"/><line class="cls-48-5" x1="163.5" y1="199.5" x2="165" y2="201"/><path class="cls-48-4" d="M162,177v12"/><path class="cls-48-6" d="M112.5,177.5h27c9,0,20-1,20-1v20l-54,1a34.21,34.21,0,0,1,2-11A44.27,44.27,0,0,1,112.5,177.5Z"/><path class="cls-48-7" d="M159,176.5s-3,7.5-4,11.5a87,87,0,0,0-1.59,8.61l-12.91.19s-1-2.3,1-7.3,5-12,5-12S157.5,177,159,176.5Z"/><path class="cls-48-7" d="M128.5,196.89h-11a19.28,19.28,0,0,1,2-11.22c3-6.12,3.75-8.16,3.75-8.16h11.56s-1.31,3.06-2.31,6.12-4,6.12-4,10.2Z"/><path class="cls-48-8" d="M135.13,177.5H146.5l-5.43,13.25a8.08,8.08,0,0,0-.82,4c.25,1.89,0,2.25,0,2.25H128.5a23.82,23.82,0,0,1,.92-7.35c1.08-3.08,2.1-3,3.59-7.58S135.13,177.5,135.13,177.5Z"/></g>"""
|
||||
),
|
||||
Part(
|
||||
""".cls-49-2{fill:none;}.cls-49-2,.cls-49-3,.cls-49-5{stroke:#000;}.cls-49-2,.cls-49-3{stroke-linecap:round;stroke-linejoin:round;}.cls-49-3{opacity:0.1;}.cls-49-4{opacity:0.4;}.cls-49-5{fill-opacity:0.4;stroke-miterlimit:10;stroke-width:0.75px;}""",
|
||||
"""<path id="fill_color" data-name="fill color" class="cls-fill-1" d="M105.5,180.5s18,3,32,0,32-11,32-11h1.33a1.13,1.13,0,0,1,.85.65l4.82,17.35v3a3.73,3.73,0,0,1-.94,1.67c-2.53,1.85-9.8,6.88-17.06,9.3a72.67,72.67,0,0,1-25,4c-8.1,0-21.07-4.05-23.57-4.86l-.43-.14-5-19A1,1,0,0,1,105.5,180.5Z"/><path class="cls-49-2" d="M105.5,180.5s18,3,32,0,32-11,32-11h1.33a1.13,1.13,0,0,1,.85.65l4.82,17.35v3a3.73,3.73,0,0,1-.94,1.67c-2.53,1.85-9.8,6.88-17.06,9.3a72.67,72.67,0,0,1-25,4c-8.1,0-21.07-4.05-23.57-4.86l-.43-.14-5-19A1,1,0,0,1,105.5,180.5Z"/><path class="cls-49-3" d="M105.5,180.5s18,3,32,0,32-11,32-11h1.33a1.13,1.13,0,0,1,.85.65l4.82,17.35v3a3.73,3.73,0,0,1-.94,1.67c-2.53,1.85-9.8,6.88-17.06,9.3a72.67,72.67,0,0,1-25,4c-8.1,0-21.07-4.05-23.57-4.86l-.43-.14-5-19A1,1,0,0,1,105.5,180.5Z"/><path class="cls-49-4" d="M108.33,195.83c0-.11-2.8-10.45-3.4-12.94a6.79,6.79,0,0,1-.3-2,2.1,2.1,0,0,1,.46,0,3,3,0,0,1,1.77.84c.32.85,3,8.16,4,11.88a31.56,31.56,0,0,1,1,5.64l-2.35.78Z"/><path d="M105.08,181.25h.09a2.55,2.55,0,0,1,1.36.68c.43,1.16,3,8.16,3.91,11.75a33.25,33.25,0,0,1,.95,5.29l-1.7.57-1-3.8c0-.1-2.8-10.45-3.4-12.93a13.31,13.31,0,0,1-.31-1.55h.1m0-.75c-.92,0-1.11,0-.52,2.48S108,195.93,108,195.93l1.2,4.57,3-1a28,28,0,0,0-1-6c-1-4-4-12-4-12a3.49,3.49,0,0,0-2-1Z"/><path class="cls-49-5" d="M111.5,199.5s17,6,36,2,29-11.87,29-11.87v.88l-.36.94-.44.64L174,193.33l-7,4.33L159.9,201l-10.68,3-4.63.79-5.34.51-5.75.16-5.06-.39L121,203.7,114.57,202l-5.07-1.54-.12-.67Z"/><path class="cls-49-2" d="M116.51,200.51c1-3,3-11,3-11s4,2,13,2,27-7,27-7,7,6,8,10"/>"""
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -29,6 +30,7 @@ import coil.fetch.Fetcher
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.Options
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.theme.isLight
|
||||
import java.util.Base64
|
||||
|
||||
@Composable
|
||||
@@ -45,9 +47,10 @@ fun RobohashAsyncImage(
|
||||
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isLightTheme = MaterialTheme.colorScheme.isLight
|
||||
|
||||
val imageRequest = remember(robot) {
|
||||
Robohash.imageRequest(context, robot)
|
||||
RobohashImageRequest.build(context, robot, isLightTheme)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
@@ -78,8 +81,9 @@ fun RobohashFallbackAsyncImage(
|
||||
loadProfilePicture: Boolean
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isLightTheme = MaterialTheme.colorScheme.isLight
|
||||
val painter = rememberAsyncImagePainter(
|
||||
model = Robohash.imageRequest(context, robot)
|
||||
model = RobohashImageRequest.build(context, robot, isLightTheme)
|
||||
)
|
||||
|
||||
if (model != null && loadProfilePicture) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -184,7 +185,7 @@ fun VideoView(
|
||||
|
||||
if (blurhash == null) {
|
||||
val ratio = aspectRatio(dimensions)
|
||||
val modifier = if (ratio != null && roundedCorner) {
|
||||
val modifier = if (ratio != null && roundedCorner && automaticallyStartPlayback.value) {
|
||||
Modifier.aspectRatio(ratio)
|
||||
} else {
|
||||
Modifier
|
||||
@@ -223,7 +224,12 @@ fun VideoView(
|
||||
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
if (!automaticallyStartPlayback.value) {
|
||||
DisplayBlurHash(blurhash, null, ContentScale.Crop, MaterialTheme.colorScheme.imageModifier)
|
||||
DisplayBlurHash(
|
||||
blurhash,
|
||||
null,
|
||||
ContentScale.Crop,
|
||||
MaterialTheme.colorScheme.imageModifier
|
||||
)
|
||||
IconButton(
|
||||
modifier = Modifier.size(Size75dp),
|
||||
onClick = { automaticallyStartPlayback.value = true }
|
||||
@@ -273,54 +279,73 @@ fun VideoViewInner(
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
VideoPlayerActiveMutex(videoUri) { modifier, activeOnScreen ->
|
||||
val mediaItem = remember(videoUri) {
|
||||
mutableStateOf(
|
||||
MediaItem.Builder()
|
||||
.setMediaId(videoUri)
|
||||
.setUri(videoUri)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setArtist(authorName?.ifBlank { null })
|
||||
.setTitle(title?.ifBlank { null } ?: videoUri)
|
||||
.setArtworkUri(
|
||||
try {
|
||||
if (artworkUri != null) {
|
||||
Uri.parse(artworkUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
)
|
||||
.build()
|
||||
GetMediaItem(videoUri, title, artworkUri, authorName) { mediaItem ->
|
||||
GetVideoController(
|
||||
mediaItem = mediaItem,
|
||||
videoUri = videoUri,
|
||||
defaultToStart = defaultToStart,
|
||||
nostrUriCallback = nostrUriCallback
|
||||
) { controller, keepPlaying ->
|
||||
RenderVideoPlayer(
|
||||
controller = controller,
|
||||
thumbData = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
dimensions = dimensions,
|
||||
blurhash = blurhash,
|
||||
topPaddingForControllers = topPaddingForControllers,
|
||||
waveform = waveform,
|
||||
keepPlaying = keepPlaying,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
activeOnScreen = activeOnScreen,
|
||||
modifier = modifier,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
onDialog = onDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GetMediaItem(
|
||||
videoUri: String,
|
||||
title: String?,
|
||||
artworkUri: String?,
|
||||
authorName: String?,
|
||||
inner: @Composable (State<MediaItem>) -> Unit
|
||||
) {
|
||||
val mediaItem = produceState<MediaItem?>(
|
||||
initialValue = null,
|
||||
key1 = videoUri
|
||||
) {
|
||||
this.value = MediaItem.Builder()
|
||||
.setMediaId(videoUri)
|
||||
.setUri(videoUri)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setArtist(authorName?.ifBlank { null })
|
||||
.setTitle(title?.ifBlank { null } ?: videoUri)
|
||||
.setArtworkUri(
|
||||
try {
|
||||
if (artworkUri != null) {
|
||||
Uri.parse(artworkUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
GetVideoController(
|
||||
mediaItem = mediaItem,
|
||||
videoUri = videoUri,
|
||||
defaultToStart = defaultToStart,
|
||||
nostrUriCallback = nostrUriCallback
|
||||
) { controller, keepPlaying ->
|
||||
RenderVideoPlayer(
|
||||
controller = controller,
|
||||
thumbData = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
dimensions = dimensions,
|
||||
blurhash = blurhash,
|
||||
topPaddingForControllers = topPaddingForControllers,
|
||||
waveform = waveform,
|
||||
keepPlaying = keepPlaying,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
activeOnScreen = activeOnScreen,
|
||||
modifier = modifier,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
onDialog = onDialog
|
||||
)
|
||||
mediaItem.value?.let {
|
||||
val myState = remember(videoUri) {
|
||||
mutableStateOf(it)
|
||||
}
|
||||
inner(myState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +364,7 @@ sealed class MediaControllerState {
|
||||
@Composable
|
||||
@OptIn(UnstableApi::class)
|
||||
fun GetVideoController(
|
||||
mediaItem: MutableState<MediaItem>,
|
||||
mediaItem: State<MediaItem>,
|
||||
videoUri: String,
|
||||
defaultToStart: Boolean = false,
|
||||
nostrUriCallback: String? = null,
|
||||
|
||||
@@ -21,7 +21,7 @@ class ChannelFeedFilter(val channel: Channel, val account: Account) : AdditiveFe
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return collection
|
||||
.filter { it.idHex in channel.notes.keys && account.isAcceptable(it) }
|
||||
.filter { channel.notes.containsKey(it.idHex) && account.isAcceptable(it) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.vitorpamplona.amethyst.ui.actions.updated
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
|
||||
@@ -20,19 +19,15 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
val me = account.userProfile()
|
||||
val followingKeySet = account.followingKeySet()
|
||||
|
||||
val privateChatrooms = me.privateChatrooms
|
||||
val messagingWith = privateChatrooms.keys.filter {
|
||||
(
|
||||
privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
|
||||
me.hasSentMessagesTo(it)
|
||||
) && !account.isAllHidden(it.users)
|
||||
val knownChatrooms = me.privateChatrooms.filter {
|
||||
(it.value.senderIntersects(followingKeySet) || me.hasSentMessagesTo(it.key)) && !account.isAllHidden(it.key.users)
|
||||
}
|
||||
|
||||
val privateMessages = messagingWith.mapNotNull { it ->
|
||||
privateChatrooms[it]
|
||||
?.roomMessages
|
||||
?.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
?.lastOrNull { it.event != null }
|
||||
val privateMessages = knownChatrooms.mapNotNull { it ->
|
||||
it.value
|
||||
.roomMessages
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.lastOrNull { it.event != null }
|
||||
}
|
||||
|
||||
val publicChannels = account.selectedChatsFollowList().mapNotNull {
|
||||
@@ -50,58 +45,53 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
}
|
||||
|
||||
override fun updateListWith(oldList: List<Note>, newItems: Set<Note>): List<Note> {
|
||||
val (feed, elapsed) = measureTimedValue {
|
||||
val me = account.userProfile()
|
||||
val me = account.userProfile()
|
||||
|
||||
// Gets the latest message by channel from the new items.
|
||||
val newRelevantPublicMessages = filterRelevantPublicMessages(newItems, account)
|
||||
// Gets the latest message by channel from the new items.
|
||||
val newRelevantPublicMessages = filterRelevantPublicMessages(newItems, account)
|
||||
|
||||
// Gets the latest message by room from the new items.
|
||||
val newRelevantPrivateMessages = filterRelevantPrivateMessages(newItems, account)
|
||||
// Gets the latest message by room from the new items.
|
||||
val newRelevantPrivateMessages = filterRelevantPrivateMessages(newItems, account)
|
||||
|
||||
if (newRelevantPrivateMessages.isEmpty() && newRelevantPublicMessages.isEmpty()) {
|
||||
return oldList
|
||||
}
|
||||
|
||||
var myNewList = oldList
|
||||
|
||||
newRelevantPublicMessages.forEach { newNotePair ->
|
||||
var hasUpdated = false
|
||||
oldList.forEach { oldNote ->
|
||||
if (newNotePair.key == oldNote.channelHex()) {
|
||||
hasUpdated = true
|
||||
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
|
||||
myNewList = myNewList.updated(oldNote, newNotePair.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasUpdated) {
|
||||
myNewList = myNewList.plus(newNotePair.value)
|
||||
}
|
||||
}
|
||||
|
||||
newRelevantPrivateMessages.forEach { newNotePair ->
|
||||
var hasUpdated = false
|
||||
oldList.forEach { oldNote ->
|
||||
val oldRoom = (oldNote.event as? ChatroomKeyable)?.chatroomKey(me.pubkeyHex)
|
||||
|
||||
if (newNotePair.key == oldRoom) {
|
||||
hasUpdated = true
|
||||
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
|
||||
myNewList = myNewList.updated(oldNote, newNotePair.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasUpdated) {
|
||||
myNewList = myNewList.plus(newNotePair.value)
|
||||
}
|
||||
}
|
||||
|
||||
sort(myNewList.toSet()).take(1000)
|
||||
if (newRelevantPrivateMessages.isEmpty() && newRelevantPublicMessages.isEmpty()) {
|
||||
return oldList
|
||||
}
|
||||
|
||||
// Log.d("Time", "${this.javaClass.simpleName} Modified Additive Feed in $elapsed with ${feed.size} objects")
|
||||
return feed
|
||||
var myNewList = oldList
|
||||
|
||||
newRelevantPublicMessages.forEach { newNotePair ->
|
||||
var hasUpdated = false
|
||||
oldList.forEach { oldNote ->
|
||||
if (newNotePair.key == oldNote.channelHex()) {
|
||||
hasUpdated = true
|
||||
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
|
||||
myNewList = myNewList.updated(oldNote, newNotePair.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasUpdated) {
|
||||
myNewList = myNewList.plus(newNotePair.value)
|
||||
}
|
||||
}
|
||||
|
||||
newRelevantPrivateMessages.forEach { newNotePair ->
|
||||
var hasUpdated = false
|
||||
oldList.forEach { oldNote ->
|
||||
val oldRoom = (oldNote.event as? ChatroomKeyable)?.chatroomKey(me.pubkeyHex)
|
||||
|
||||
if (newNotePair.key == oldRoom) {
|
||||
hasUpdated = true
|
||||
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
|
||||
myNewList = myNewList.updated(oldNote, newNotePair.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasUpdated) {
|
||||
myNewList = myNewList.plus(newNotePair.value)
|
||||
}
|
||||
}
|
||||
|
||||
return sort(myNewList.toSet()).take(1000)
|
||||
}
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> {
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.vitorpamplona.amethyst.ui.actions.updated
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
|
||||
@@ -19,17 +18,15 @@ class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter<Note>
|
||||
val me = account.userProfile()
|
||||
val followingKeySet = account.followingKeySet()
|
||||
|
||||
val privateChatrooms = account.userProfile().privateChatrooms
|
||||
val messagingWith = privateChatrooms.keys.filter {
|
||||
privateChatrooms[it]?.senderIntersects(followingKeySet) == false &&
|
||||
!me.hasSentMessagesTo(it) && !account.isAllHidden(it.users)
|
||||
val newChatrooms = me.privateChatrooms.filter {
|
||||
!it.value.senderIntersects(followingKeySet) && !me.hasSentMessagesTo(it.key) && !account.isAllHidden(it.key.users)
|
||||
}
|
||||
|
||||
val privateMessages = messagingWith.mapNotNull { it ->
|
||||
privateChatrooms[it]
|
||||
?.roomMessages
|
||||
?.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
?.lastOrNull { it.event != null }
|
||||
val privateMessages = newChatrooms.mapNotNull { it ->
|
||||
it.value
|
||||
.roomMessages
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.lastOrNull { it.event != null }
|
||||
}
|
||||
|
||||
return privateMessages
|
||||
@@ -38,40 +35,35 @@ class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter<Note>
|
||||
}
|
||||
|
||||
override fun updateListWith(oldList: List<Note>, newItems: Set<Note>): List<Note> {
|
||||
val (feed, elapsed) = measureTimedValue {
|
||||
val me = account.userProfile()
|
||||
val me = account.userProfile()
|
||||
|
||||
// Gets the latest message by room from the new items.
|
||||
val newRelevantPrivateMessages = filterRelevantPrivateMessages(newItems, account)
|
||||
// Gets the latest message by room from the new items.
|
||||
val newRelevantPrivateMessages = filterRelevantPrivateMessages(newItems, account)
|
||||
|
||||
if (newRelevantPrivateMessages.isEmpty()) {
|
||||
return oldList
|
||||
}
|
||||
|
||||
var myNewList = oldList
|
||||
|
||||
newRelevantPrivateMessages.forEach { newNotePair ->
|
||||
var hasUpdated = false
|
||||
oldList.forEach { oldNote ->
|
||||
val oldRoom = (oldNote.event as? ChatroomKeyable)?.chatroomKey(me.pubkeyHex)
|
||||
|
||||
if (newNotePair.key == oldRoom) {
|
||||
hasUpdated = true
|
||||
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
|
||||
myNewList = myNewList.updated(oldNote, newNotePair.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasUpdated) {
|
||||
myNewList = myNewList.plus(newNotePair.value)
|
||||
}
|
||||
}
|
||||
|
||||
sort(myNewList.toSet()).take(1000)
|
||||
if (newRelevantPrivateMessages.isEmpty()) {
|
||||
return oldList
|
||||
}
|
||||
|
||||
// Log.d("Time", "${this.javaClass.simpleName} Modified Additive Feed in $elapsed with ${feed.size} objects")
|
||||
return feed
|
||||
var myNewList = oldList
|
||||
|
||||
newRelevantPrivateMessages.forEach { newNotePair ->
|
||||
var hasUpdated = false
|
||||
oldList.forEach { oldNote ->
|
||||
val oldRoom = (oldNote.event as? ChatroomKeyable)?.chatroomKey(me.pubkeyHex)
|
||||
|
||||
if (newNotePair.key == oldRoom) {
|
||||
hasUpdated = true
|
||||
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
|
||||
myNewList = myNewList.updated(oldNote, newNotePair.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasUpdated) {
|
||||
myNewList = myNewList.plus(newNotePair.value)
|
||||
}
|
||||
}
|
||||
|
||||
return sort(myNewList.toSet()).take(1000)
|
||||
}
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> {
|
||||
|
||||
@@ -56,7 +56,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
.asSequence()
|
||||
.filter { it ->
|
||||
val noteEvent = it.event
|
||||
val isGlobalRelay = it.relays.any { gRelays.contains(it) }
|
||||
val isGlobalRelay = it.relays.any { gRelays.contains(it.url) }
|
||||
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent || noteEvent is AudioHeaderEvent) &&
|
||||
(!ignoreAddressables || noteEvent.kind() < 10000) &&
|
||||
((isGlobal && isGlobalRelay) || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeohashSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) &&
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.vitorpamplona.amethyst.ui.elements
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparison
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AddButtonPreview() {
|
||||
ThemeComparison(
|
||||
onDark = {
|
||||
Row() {
|
||||
Column {
|
||||
AddButton(isActive = true) {}
|
||||
AddButton(isActive = false) {}
|
||||
}
|
||||
|
||||
Column {
|
||||
RemoveButton(isActive = true) {}
|
||||
RemoveButton(isActive = false) {}
|
||||
}
|
||||
}
|
||||
},
|
||||
onLight = {
|
||||
Row() {
|
||||
Column {
|
||||
AddButton(isActive = true) {}
|
||||
AddButton(isActive = false) {}
|
||||
}
|
||||
|
||||
Column {
|
||||
RemoveButton(isActive = true) {}
|
||||
RemoveButton(isActive = false) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddButton(
|
||||
text: Int = R.string.add,
|
||||
isActive: Boolean = true,
|
||||
modifier: Modifier = Modifier.padding(start = 3.dp),
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
if (isActive) {
|
||||
onClick()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
enabled = isActive,
|
||||
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoveButton(
|
||||
isActive: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.padding(start = 3.dp),
|
||||
onClick = {
|
||||
if (isActive) {
|
||||
onClick()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
enabled = isActive,
|
||||
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.remove), color = Color.White)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.vitorpamplona.amethyst.ui.elements
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
|
||||
@Composable
|
||||
fun DisplayFollowingCommunityInPost(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Column(HalfStartPadding) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
DisplayCommunity(baseNote, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayCommunity(note: Note, nav: (String) -> Unit) {
|
||||
val communityTag = remember(note) {
|
||||
note.event?.getTagOfAddressableKind(CommunityDefinitionEvent.kind)
|
||||
} ?: return
|
||||
|
||||
val displayTag = remember(note) { AnnotatedString(getCommunityShortName(communityTag)) }
|
||||
val route = remember(note) { "Community/${communityTag.toTag()}" }
|
||||
|
||||
ClickableText(
|
||||
text = displayTag,
|
||||
onClick = { nav(route) },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.52f
|
||||
)
|
||||
),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCommunityShortName(communityTag: ATag): String {
|
||||
val name = if (communityTag.dTag.length > 10) {
|
||||
communityTag.dTag.take(10) + "..."
|
||||
} else {
|
||||
communityTag.dTag.take(10)
|
||||
}
|
||||
|
||||
return "/n/$name"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.vitorpamplona.amethyst.ui.elements
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun DisplayFollowingHashtagsInPost(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteEvent = remember { baseNote.event } ?: return
|
||||
|
||||
val userFollowState by accountViewModel.userFollows.observeAsState()
|
||||
var firstTag by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = userFollowState) {
|
||||
launch(Dispatchers.Default) {
|
||||
val followingTags = userFollowState?.user?.cachedFollowingTagSet() ?: emptySet()
|
||||
val newFirstTag = noteEvent.firstIsTaggedHashes(followingTags)
|
||||
|
||||
if (firstTag != newFirstTag) {
|
||||
launch(Dispatchers.Main) {
|
||||
firstTag = newFirstTag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstTag?.let {
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
DisplayTagList(it, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayTagList(firstTag: String, nav: (String) -> Unit) {
|
||||
val displayTag = remember(firstTag) { AnnotatedString(" #$firstTag") }
|
||||
val route = remember(firstTag) { "Hashtag/$firstTag" }
|
||||
|
||||
ClickableText(
|
||||
text = displayTag,
|
||||
onClick = { nav(route) },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.52f
|
||||
)
|
||||
),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.vitorpamplona.amethyst.ui.elements
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparison
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun DisplayPoWPreview() {
|
||||
ThemeComparison(
|
||||
onDark = {
|
||||
DisplayPoW(pow = 24)
|
||||
},
|
||||
onLight = {
|
||||
DisplayPoW(pow = 24)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayPoW(
|
||||
pow: Int
|
||||
) {
|
||||
val powStr = remember(pow) {
|
||||
"PoW-$pow"
|
||||
}
|
||||
|
||||
Text(
|
||||
powStr,
|
||||
color = MaterialTheme.colorScheme.lessImportantLink,
|
||||
fontSize = Font14SP,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
package com.vitorpamplona.amethyst.ui.elements
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
@@ -9,19 +10,26 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
@@ -35,10 +43,102 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
data class Reward(val amount: BigDecimal)
|
||||
|
||||
@Composable
|
||||
fun DisplayReward(
|
||||
baseReward: Reward,
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column() {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable { popupExpanded = true }
|
||||
) {
|
||||
ClickableText(
|
||||
text = AnnotatedString("#bounty"),
|
||||
onClick = { nav("Hashtag/bounty") },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.52f
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
RenderPledgeAmount(baseNote, baseReward, accountViewModel)
|
||||
}
|
||||
|
||||
if (popupExpanded) {
|
||||
AddBountyAmountDialog(baseNote, accountViewModel) {
|
||||
popupExpanded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderPledgeAmount(
|
||||
baseNote: Note,
|
||||
baseReward: Reward,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val repliesState by baseNote.live().replies.observeAsState()
|
||||
var reward by remember {
|
||||
mutableStateOf<String>(
|
||||
showAmount(baseReward.amount)
|
||||
)
|
||||
}
|
||||
|
||||
var hasPledge by remember {
|
||||
mutableStateOf<Boolean>(
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = repliesState) {
|
||||
launch(Dispatchers.IO) {
|
||||
repliesState?.note?.pledgedAmountByOthers()?.let {
|
||||
val newRewardAmount = showAmount(baseReward.amount.add(it))
|
||||
if (newRewardAmount != reward) {
|
||||
reward = newRewardAmount
|
||||
}
|
||||
}
|
||||
val newHasPledge = repliesState?.note?.hasPledgeBy(accountViewModel.userProfile()) == true
|
||||
if (hasPledge != newHasPledge) {
|
||||
launch(Dispatchers.Main) {
|
||||
hasPledge = newHasPledge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPledge) {
|
||||
ZappedIcon(modifier = Size20Modifier)
|
||||
} else {
|
||||
ZapIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = reward,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
class AddBountyAmountViewModel : ViewModel() {
|
||||
private var account: Account? = null
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.vitorpamplona.amethyst.ui.elements
|
||||
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun DisplayUncitedHashtags(
|
||||
hashtags: ImmutableList<String>,
|
||||
eventContent: String,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val unusedHashtags = remember(eventContent) {
|
||||
hashtags.filter { !eventContent.contains(it, true) }
|
||||
}
|
||||
|
||||
if (unusedHashtags.isNotEmpty()) {
|
||||
FlowRow(
|
||||
modifier = HalfTopPadding
|
||||
) {
|
||||
unusedHashtags.forEach { hashtag ->
|
||||
ClickableText(
|
||||
text = remember { AnnotatedString("#$hashtag ") },
|
||||
onClick = { nav("Hashtag/$hashtag") },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.lessImportantLink
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.vitorpamplona.amethyst.ui.elements
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun DisplayZapSplits(noteEvent: EventInterface, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val list = remember(noteEvent) { noteEvent.zapSplitSetup() }
|
||||
if (list.isEmpty()) return
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(20.dp)
|
||||
.width(25.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.align(Alignment.CenterStart),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ArrowForwardIos,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = Modifier
|
||||
.size(13.dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
|
||||
FlowRow {
|
||||
list.forEach {
|
||||
if (it.isLnAddress) {
|
||||
ClickableText(
|
||||
text = AnnotatedString(it.lnAddressOrPubKeyHex),
|
||||
onClick = { },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
} else {
|
||||
UserPicture(
|
||||
userHex = it.lnAddressOrPubKeyHex,
|
||||
size = Size25dp,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.vitorpamplona.amethyst.ui.layouts
|
||||
|
||||
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.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.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.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.NewItemsBubble
|
||||
import com.vitorpamplona.amethyst.ui.note.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatHeadlineBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ChannelNamePreview() {
|
||||
Column {
|
||||
ChatHeaderLayout(
|
||||
channelPicture = {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.github),
|
||||
contentDescription = stringResource(id = R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
},
|
||||
firstRow = {
|
||||
Text("This is my author", Modifier.weight(1f))
|
||||
TimeAgo(TimeUtils.now())
|
||||
},
|
||||
secondRow = {
|
||||
Text("This is a message from this person", Modifier.weight(1f))
|
||||
NewItemsBubble()
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
)
|
||||
|
||||
Divider()
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("This is my author", Modifier.weight(1f))
|
||||
TimeAgo(TimeUtils.now())
|
||||
}
|
||||
},
|
||||
supportingContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("This is a message from this person", Modifier.weight(1f))
|
||||
NewItemsBubble()
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.github),
|
||||
contentDescription = stringResource(id = R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Size55Modifier
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatHeaderLayout(
|
||||
channelPicture: @Composable () -> Unit,
|
||||
firstRow: @Composable RowScope.() -> Unit,
|
||||
secondRow: @Composable RowScope.() -> Unit,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(modifier = remember { Modifier.clickable(onClick = onClick) }) {
|
||||
Row(
|
||||
modifier = ChatHeadlineBorders,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(Size55Modifier) {
|
||||
channelPicture()
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
firstRow()
|
||||
}
|
||||
|
||||
Spacer(modifier = Height4dpModifier)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
secondRow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
modifier = StdTopPadding,
|
||||
thickness = DividerThickness
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.vitorpamplona.amethyst.ui.layouts
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.note.RepostedIcon
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun GenericRepostSectionPreview() {
|
||||
GenericRepostLayout(
|
||||
baseAuthorPicture = {
|
||||
Text("ab")
|
||||
},
|
||||
repostAuthorPicture = {
|
||||
Text("cd")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GenericRepostLayout(
|
||||
baseAuthorPicture: @Composable () -> Unit,
|
||||
repostAuthorPicture: @Composable () -> Unit
|
||||
) {
|
||||
Box(modifier = Size55Modifier) {
|
||||
Box(remember { Size35Modifier.align(Alignment.TopStart) }) {
|
||||
baseAuthorPicture()
|
||||
}
|
||||
|
||||
Box(
|
||||
remember {
|
||||
Size18Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(1.dp)
|
||||
}
|
||||
) {
|
||||
RepostedIcon(modifier = Size18Modifier, MaterialTheme.colorScheme.placeholderText)
|
||||
}
|
||||
|
||||
Box(
|
||||
remember { Size35Modifier.align(Alignment.BottomEnd) },
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
repostAuthorPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ private fun AccountPicture(user: User, loadProfilePicture: Boolean) {
|
||||
val profilePicture by user.live().profilePictureChanges.observeAsState()
|
||||
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = remember(user) { user.pubkeyHex },
|
||||
robot = user.pubkeyHex,
|
||||
model = profilePicture,
|
||||
contentDescription = stringResource(R.string.profile_image),
|
||||
modifier = AccountPictureModifier,
|
||||
|
||||
@@ -30,7 +30,6 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -143,49 +142,45 @@ private fun RowScope.HasNewItemsIcon(
|
||||
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
ObserveNewItems(route, accountViewModel) { hasNewItems ->
|
||||
NotifiableIcon(
|
||||
route.icon,
|
||||
route.notifSize,
|
||||
route.iconSize,
|
||||
selected,
|
||||
hasNewItems
|
||||
)
|
||||
}
|
||||
NotifiableIcon(
|
||||
selected,
|
||||
route,
|
||||
accountViewModel
|
||||
)
|
||||
},
|
||||
selected = selected,
|
||||
onClick = { nav(route, selected) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveNewItems(route: Route, accountViewModel: AccountViewModel, inner: @Composable (hasNewItems: State<Boolean>?) -> Unit) {
|
||||
val hasNewItems = accountViewModel.notificationDots.hasNewItems[route]?.collectAsStateWithLifecycle()
|
||||
|
||||
inner(hasNewItems)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotifiableIcon(
|
||||
icon: Int,
|
||||
size: Dp,
|
||||
iconSize: Dp,
|
||||
selected: Boolean,
|
||||
hasNewItems: State<Boolean>?
|
||||
route: Route,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Box(remember { Modifier.size(size) }) {
|
||||
Box(route.notifSize) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
painter = painterResource(id = route.icon),
|
||||
contentDescription = null,
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
modifier = route.iconSize,
|
||||
tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified
|
||||
)
|
||||
|
||||
if (hasNewItems?.value == true) {
|
||||
NotificationDotIcon(
|
||||
Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
AddNotifIconIfNeeded(route, accountViewModel, Modifier.align(Alignment.TopEnd))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddNotifIconIfNeeded(
|
||||
route: Route,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier
|
||||
) {
|
||||
val flow = accountViewModel.notificationDots.hasNewItems[route] ?: return
|
||||
val hasNewItems by flow.collectAsStateWithLifecycle()
|
||||
if (hasNewItems) {
|
||||
NotificationDotIcon(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
@@ -725,12 +726,12 @@ fun SimpleTextSpinner(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Size20Modifier)
|
||||
Text(currentText)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import java.net.URLEncoder
|
||||
|
||||
fun routeFor(note: Note, loggedIn: User): String? {
|
||||
val noteEvent = note.event
|
||||
|
||||
if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) {
|
||||
note.channelHex()?.let {
|
||||
return "Channel/$it"
|
||||
}
|
||||
} else if (noteEvent is LiveActivitiesEvent || noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
note.channelHex()?.let {
|
||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
||||
}
|
||||
} else if (noteEvent is ChatroomKeyable) {
|
||||
val room = noteEvent.chatroomKey(loggedIn.pubkeyHex)
|
||||
loggedIn.createChatroom(room)
|
||||
return "Room/${room.hashCode()}"
|
||||
} else if (noteEvent is CommunityDefinitionEvent) {
|
||||
return "Community/${URLEncoder.encode(note.idHex, "utf-8")}"
|
||||
} else {
|
||||
return "Note/${URLEncoder.encode(note.idHex, "utf-8")}"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun routeToMessage(user: HexKey, draftMessage: String?, accountViewModel: AccountViewModel): String {
|
||||
val withKey = ChatroomKey(persistentSetOf(user))
|
||||
accountViewModel.account.userProfile().createChatroom(withKey)
|
||||
return if (draftMessage != null) {
|
||||
"Room/${withKey.hashCode()}?message=$draftMessage"
|
||||
} else {
|
||||
"Room/${withKey.hashCode()}"
|
||||
}
|
||||
}
|
||||
|
||||
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
|
||||
return routeToMessage(user.pubkeyHex, draftMessage, accountViewModel)
|
||||
}
|
||||
|
||||
fun routeFor(note: Channel): String {
|
||||
return "Channel/${note.idHex}"
|
||||
}
|
||||
|
||||
fun routeFor(user: User): String {
|
||||
return "User/${user.pubkeyHex}"
|
||||
}
|
||||
|
||||
fun authorRouteFor(note: Note): String {
|
||||
return "User/${note.author?.pubkeyHex}"
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NamedNavArgument
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDestination
|
||||
@@ -37,16 +38,16 @@ sealed class Route(
|
||||
val route: String,
|
||||
val base: String = route.substringBefore("?"),
|
||||
val icon: Int,
|
||||
val notifSize: Dp = Size23dp,
|
||||
val iconSize: Dp = Size20dp,
|
||||
val notifSize: Modifier = Modifier.size(Size23dp),
|
||||
val iconSize: Modifier = Modifier.size(Size20dp),
|
||||
val hasNewItems: (Account, Set<com.vitorpamplona.amethyst.model.Note>) -> Boolean = { _, _ -> false },
|
||||
val arguments: ImmutableList<NamedNavArgument> = persistentListOf()
|
||||
) {
|
||||
object Home : Route(
|
||||
route = "Home?nip47={nip47}",
|
||||
icon = R.drawable.ic_home,
|
||||
notifSize = Size25dp,
|
||||
iconSize = Size24dp,
|
||||
notifSize = Modifier.size(Size25dp),
|
||||
iconSize = Modifier.size(Size24dp),
|
||||
arguments = listOf(
|
||||
navArgument("nip47") { type = NavType.StringType; nullable = true; defaultValue = null }
|
||||
).toImmutableList(),
|
||||
@@ -335,8 +336,9 @@ private fun getRouteWithArguments(
|
||||
): String? {
|
||||
var route = destination.route ?: return null
|
||||
arguments?.let { bundle ->
|
||||
destination.arguments.keys.forEach { key ->
|
||||
val value = destination.arguments[key]?.type?.get(bundle, key)?.toString()
|
||||
destination.arguments.forEach {
|
||||
val key = it.key
|
||||
val value = it.value.type[bundle, key]?.toString()
|
||||
if (value == null) {
|
||||
val keyStart = route.indexOf("{$key}")
|
||||
// if it is a parameter, removes the complete segment `var={key}` and adjust connectors `#`, `&` or `&`
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
|
||||
@@ -2,19 +2,13 @@ package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -51,15 +45,11 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatHeadlineBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.emptyLineItemModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
@@ -81,7 +71,7 @@ fun ChatroomHeaderCompose(
|
||||
if (hasEvent) {
|
||||
ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
BlankNote(Modifier)
|
||||
BlankNote()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,15 +106,11 @@ private fun ChatroomPrivateMessages(
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(userRoom) { room ->
|
||||
Crossfade(userRoom, label = "ChatroomPrivateMessages") { room ->
|
||||
if (room != null) {
|
||||
UserRoomCompose(baseNote, room, accountViewModel, nav)
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.height(Size75dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Box(emptyLineItemModifier) {
|
||||
// Makes sure just a max amount of objects are loaded.
|
||||
}
|
||||
}
|
||||
@@ -202,7 +188,7 @@ private fun ChannelRoomCompose(
|
||||
ChannelTitleWithLabelInfo(channelName, modifier)
|
||||
},
|
||||
channelLastTime = remember(note) { note.createdAt() },
|
||||
channelLastContent = remember(note) { "$authorName: $description" },
|
||||
channelLastContent = remember(note, authorState) { "$authorName: $description" },
|
||||
hasNewMessages = hasNewMessages,
|
||||
loadProfilePicture = automaticallyShowProfilePicture,
|
||||
onClick = { nav(route) }
|
||||
@@ -295,8 +281,8 @@ fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, accountViewModel: Acc
|
||||
it.user.privateChatrooms[room]?.subject
|
||||
}.distinctUntilChanged().observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject)
|
||||
|
||||
Crossfade(targetState = roomSubject, modifier) {
|
||||
if (it != null && it.isNotBlank()) {
|
||||
Crossfade(targetState = roomSubject, modifier, label = "RoomNameDisplay") {
|
||||
if (!it.isNullOrBlank()) {
|
||||
if (room.users.size > 1) {
|
||||
DisplayRoomSubject(it)
|
||||
} else {
|
||||
@@ -408,7 +394,7 @@ private fun WatchNotificationChanges(
|
||||
accountViewModel: AccountViewModel,
|
||||
onNewStatus: (Boolean) -> Unit
|
||||
) {
|
||||
LaunchedEffect(key1 = note, accountViewModel.accountMarkAsReadUpdates.value) {
|
||||
LaunchedEffect(key1 = note, accountViewModel.accountMarkAsReadUpdates.intValue) {
|
||||
launch(Dispatchers.IO) {
|
||||
note.event?.createdAt()?.let {
|
||||
val lastTime = accountViewModel.account.loadLastRead(route)
|
||||
@@ -454,12 +440,7 @@ fun ChannelName(
|
||||
robot = channelIdHex,
|
||||
model = channelPicture,
|
||||
contentDescription = stringResource(R.string.channel_image),
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(Size55dp)
|
||||
.height(Size55dp)
|
||||
.clip(shape = CircleShape)
|
||||
},
|
||||
modifier = AccountPictureModifier,
|
||||
loadProfilePicture = loadProfilePicture
|
||||
)
|
||||
},
|
||||
@@ -480,76 +461,38 @@ fun ChannelName(
|
||||
hasNewMessages: MutableState<Boolean>,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(modifier = remember { Modifier.clickable(onClick = onClick) }) {
|
||||
Row(modifier = ChatHeadlineBorders) {
|
||||
Column(Size55Modifier) {
|
||||
channelPicture()
|
||||
ChatHeaderLayout(
|
||||
channelPicture = channelPicture,
|
||||
firstRow = {
|
||||
channelTitle(Modifier.weight(1f))
|
||||
TimeAgo(channelLastTime)
|
||||
},
|
||||
secondRow = {
|
||||
if (channelLastContent != null) {
|
||||
Text(
|
||||
channelLastContent,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.referenced_event_not_found),
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FirstRow(channelTitle, channelLastTime, remember { Modifier.weight(1f) })
|
||||
}
|
||||
|
||||
Spacer(modifier = Height4dpModifier)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SecondRow(channelLastContent, hasNewMessages, remember { Modifier.weight(1f) })
|
||||
}
|
||||
if (hasNewMessages.value) {
|
||||
NewItemsBubble()
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
modifier = StdTopPadding,
|
||||
thickness = DividerThickness
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecondRow(channelLastContent: String?, hasNewMessages: MutableState<Boolean>, modifier: Modifier) {
|
||||
if (channelLastContent != null) {
|
||||
Text(
|
||||
channelLastContent,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.referenced_event_not_found),
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
if (hasNewMessages.value) {
|
||||
NewItemsBubble()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FirstRow(
|
||||
channelTitle: @Composable (Modifier) -> Unit,
|
||||
channelLastTime: Long?,
|
||||
modifier: Modifier
|
||||
) {
|
||||
channelTitle(modifier)
|
||||
TimeAgo(channelLastTime)
|
||||
},
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -557,10 +500,8 @@ private fun TimeAgo(channelLastTime: Long?) {
|
||||
if (channelLastTime == null) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val timeAgo by remember(channelLastTime) {
|
||||
derivedStateOf {
|
||||
timeAgo(channelLastTime, context)
|
||||
}
|
||||
val timeAgo = remember(channelLastTime) {
|
||||
timeAgo(channelLastTime, context)
|
||||
}
|
||||
Text(
|
||||
text = timeAgo,
|
||||
|
||||
@@ -59,7 +59,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
@@ -450,7 +450,7 @@ private fun MessageBubbleLines(
|
||||
grayTint = MaterialTheme.colorScheme.placeholderText,
|
||||
accountViewModel = accountViewModel,
|
||||
showCounter = false,
|
||||
iconSize = Size15dp
|
||||
iconSizeModifier = Size15Modifier
|
||||
) {
|
||||
onWantsToReply(baseNote)
|
||||
}
|
||||
|
||||
@@ -123,11 +123,11 @@ fun LikedIcon(modifier: Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LikeIcon(iconSize: Dp, grayTint: Color) {
|
||||
fun LikeIcon(iconSizeModifier: Modifier, grayTint: Color) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_like),
|
||||
null,
|
||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
||||
modifier = iconSizeModifier,
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
@@ -162,11 +162,6 @@ fun ZappedIcon(modifier: Modifier) {
|
||||
ZapIcon(modifier = modifier, BitcoinOrange)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZapIcon(iconSize: Dp, tint: Color = Color.Unspecified) {
|
||||
ZapIcon(modifier = remember(iconSize) { Modifier.size(iconSize) }, tint)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZapIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
|
||||
Icon(
|
||||
@@ -228,11 +223,11 @@ fun ExpandMoreIcon(modifier: Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CommentIcon(iconSize: Dp, tint: Color) {
|
||||
fun CommentIcon(iconSizeModifier: Modifier, tint: Color) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_comment),
|
||||
contentDescription = null,
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
modifier = iconSizeModifier,
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
@@ -44,11 +43,14 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.authorRouteFor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -75,7 +77,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
|
||||
@Composable
|
||||
@@ -129,27 +130,21 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
|
||||
}
|
||||
|
||||
Column(modifier = columnModifier) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
Galeries(multiSetCard, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
Log.d("Rendering Metrics", "All Galeries: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed - ")
|
||||
Galeries(multiSetCard, backgroundColor, accountViewModel, nav)
|
||||
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Spacer(modifier = WidthAuthorPictureModifierWithPadding)
|
||||
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
NoteCompose(
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = null,
|
||||
modifier = remember { Modifier.padding(top = 5.dp) },
|
||||
isBoostedNote = true,
|
||||
showHidden = showHidden,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Complete: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
NoteCompose(
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = null,
|
||||
modifier = remember { Modifier.padding(top = 5.dp) },
|
||||
isBoostedNote = true,
|
||||
showHidden = showHidden,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
|
||||
}
|
||||
@@ -167,9 +162,6 @@ private fun Galeries(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
|
||||
val likeEvents by remember { derivedStateOf { multiSetCard.likeEventsByType } }
|
||||
|
||||
val hasZapEvents by remember { derivedStateOf { multiSetCard.zapEvents.isNotEmpty() } }
|
||||
val hasBoostEvents by remember { derivedStateOf { multiSetCard.boostEvents.isNotEmpty() } }
|
||||
val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } }
|
||||
@@ -187,26 +179,17 @@ private fun Galeries(
|
||||
}
|
||||
}
|
||||
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Galeries Zaps: ${multiSetCard.note.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
|
||||
if (hasBoostEvents) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
RenderBoostGallery(boostEvents, nav, accountViewModel)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Galeries Repost: ${multiSetCard.note.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
RenderBoostGallery(multiSetCard.boostEvents, nav, accountViewModel)
|
||||
}
|
||||
|
||||
if (hasLikeEvents) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
likeEvents.forEach {
|
||||
RenderLikeGallery(it.key, it.value, nav, accountViewModel)
|
||||
}
|
||||
multiSetCard.likeEventsByType.forEach {
|
||||
RenderLikeGallery(it.key, it.value, nav, accountViewModel)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Galeries Like: ${multiSetCard.note.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,6 +286,31 @@ fun RenderBoostGallery(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderBoostGallery(
|
||||
noteToGetBoostEvents: NoteState,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = NotificationIconModifierSmaller
|
||||
) {
|
||||
RepostedIcon(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(Size19dp)
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AuthorGallery(noteToGetBoostEvents, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MapZaps(
|
||||
zaps: ImmutableList<CombinedZap>,
|
||||
@@ -491,6 +499,22 @@ fun AuthorGallery(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun AuthorGallery(
|
||||
noteToGetBoostEvents: NoteState,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Column(modifier = StdStartPadding) {
|
||||
FlowRow() {
|
||||
noteToGetBoostEvents.note.boosts.forEach { note ->
|
||||
BoxedAuthor(note, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxedAuthor(
|
||||
note: Note,
|
||||
|
||||
@@ -34,12 +34,12 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -28,13 +27,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.CutCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -44,7 +37,6 @@ import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@@ -53,7 +45,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.BottomEnd
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -71,7 +62,6 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
@@ -88,7 +78,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||
@@ -114,6 +104,17 @@ import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
|
||||
import com.vitorpamplona.amethyst.ui.components.imageExtensions
|
||||
import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth
|
||||
import com.vitorpamplona.amethyst.ui.components.removeQueryParamsForExtensionComparison
|
||||
import com.vitorpamplona.amethyst.ui.elements.AddButton
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayPoW
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayReward
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits
|
||||
import com.vitorpamplona.amethyst.ui.elements.RemoveButton
|
||||
import com.vitorpamplona.amethyst.ui.elements.Reward
|
||||
import com.vitorpamplona.amethyst.ui.layouts.GenericRepostLayout
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
@@ -124,8 +125,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
@@ -138,8 +137,6 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||
@@ -156,16 +153,18 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
@@ -176,8 +175,6 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
@@ -185,7 +182,6 @@ import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiUrl
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
@@ -210,16 +206,12 @@ import com.vitorpamplona.quartz.events.UserMetadata
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.math.BigDecimal
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import java.util.Locale
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -288,7 +280,7 @@ fun CheckHiddenNoteCompose(
|
||||
) {
|
||||
if (showHidden) {
|
||||
// Ignores reports as well
|
||||
val state by remember {
|
||||
val state by remember(note) {
|
||||
mutableStateOf(
|
||||
AccountViewModel.NoteComposeReportState()
|
||||
)
|
||||
@@ -349,7 +341,7 @@ fun LoadedNoteCompose(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var state by remember {
|
||||
var state by remember(note) {
|
||||
mutableStateOf(
|
||||
AccountViewModel.NoteComposeReportState()
|
||||
)
|
||||
@@ -394,7 +386,7 @@ fun RenderReportState(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var showReportedNote by remember { mutableStateOf(false) }
|
||||
var showReportedNote by remember(note) { mutableStateOf(false) }
|
||||
|
||||
Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "RenderReportState") { showHiddenNote ->
|
||||
if (showHiddenNote) {
|
||||
@@ -411,18 +403,18 @@ fun RenderReportState(
|
||||
val canPreview = (!state.isAcceptable && showReportedNote) || state.canPreview
|
||||
|
||||
NormalNote(
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
canPreview,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
baseNote = note,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
canPreview = canPreview,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -522,19 +514,19 @@ fun NormalNote(
|
||||
else ->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
CheckNewAndRenderNote(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
canPreview,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
showPopup,
|
||||
nav
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
canPreview = canPreview,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
showPopup = showPopup,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -644,8 +636,13 @@ fun LongCommunityHeader(
|
||||
)
|
||||
}
|
||||
|
||||
val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() }
|
||||
DisplayUncitedHashtags(hashtags, summary ?: "", nav)
|
||||
if (noteEvent.hasHashtags()) {
|
||||
DisplayUncitedHashtags(
|
||||
remember(noteEvent) { noteEvent.hashtags().toImmutableList() },
|
||||
summary ?: "",
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column() {
|
||||
@@ -947,43 +944,34 @@ fun InnerNoteWithReactions(
|
||||
val notBoostedNorQuote = !isBoostedNote && !isQuotedNote
|
||||
|
||||
Row(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
end = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
|
||||
// Don't add margin to the bottom because of the Divider down below
|
||||
)
|
||||
modifier = if (!isBoostedNote && addMarginTop) {
|
||||
normalWithTopMarginNoteModifier
|
||||
} else if (!isBoostedNote) {
|
||||
normalNoteModifier
|
||||
} else {
|
||||
boostedNoteModifier
|
||||
}
|
||||
) {
|
||||
if (notBoostedNorQuote) {
|
||||
Column(WidthAuthorPictureModifier) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
AuthorAndRelayInformation(baseNote, accountViewModel, nav)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Author: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
AuthorAndRelayInformation(baseNote, accountViewModel, nav)
|
||||
}
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
val showSecondRow = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && !isBoostedNote && !isQuotedNote
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
NoteBody(
|
||||
baseNote = baseNote,
|
||||
showAuthorPicture = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
canPreview = canPreview,
|
||||
showSecondRow = showSecondRow,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
Log.d("Rendering Metrics", "TextBody: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
NoteBody(
|
||||
baseNote = baseNote,
|
||||
showAuthorPicture = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
canPreview = canPreview,
|
||||
showSecondRow = showSecondRow,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -996,15 +984,12 @@ fun InnerNoteWithReactions(
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
}
|
||||
} else {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
ReactionsRow(
|
||||
baseNote = baseNote,
|
||||
showReactionDetail = notBoostedNorQuote,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Reaction: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
ReactionsRow(
|
||||
baseNote = baseNote,
|
||||
showReactionDetail = notBoostedNorQuote,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,60 +1058,6 @@ private fun NoteBody(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun DisplayZapSplits(noteEvent: EventInterface, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val list = remember(noteEvent) { noteEvent.zapSplitSetup() }
|
||||
|
||||
if (list.isEmpty()) return
|
||||
|
||||
Row(verticalAlignment = CenterVertically) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(20.dp)
|
||||
.width(25.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.align(Alignment.CenterStart),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ArrowForwardIos,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = Modifier
|
||||
.size(13.dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
|
||||
FlowRow {
|
||||
list.forEach {
|
||||
if (it.isLnAddress) {
|
||||
ClickableText(
|
||||
text = AnnotatedString(it.lnAddressOrPubKeyHex),
|
||||
onClick = { },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
} else {
|
||||
UserPicture(
|
||||
userHex = it.lnAddressOrPubKeyHex,
|
||||
size = Size25dp,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderNoteRow(
|
||||
baseNote: Note,
|
||||
@@ -1268,56 +1199,6 @@ private fun RenderNoteRow(
|
||||
}
|
||||
}
|
||||
|
||||
fun routeFor(note: Note, loggedIn: User): String? {
|
||||
val noteEvent = note.event
|
||||
|
||||
if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) {
|
||||
note.channelHex()?.let {
|
||||
return "Channel/$it"
|
||||
}
|
||||
} else if (noteEvent is LiveActivitiesEvent || noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
note.channelHex()?.let {
|
||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
||||
}
|
||||
} else if (noteEvent is ChatroomKeyable) {
|
||||
val room = noteEvent.chatroomKey(loggedIn.pubkeyHex)
|
||||
loggedIn.createChatroom(room)
|
||||
return "Room/${room.hashCode()}"
|
||||
} else if (noteEvent is CommunityDefinitionEvent) {
|
||||
return "Community/${URLEncoder.encode(note.idHex, "utf-8")}"
|
||||
} else {
|
||||
return "Note/${URLEncoder.encode(note.idHex, "utf-8")}"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun routeToMessage(user: HexKey, draftMessage: String?, accountViewModel: AccountViewModel): String {
|
||||
val withKey = ChatroomKey(persistentSetOf(user))
|
||||
accountViewModel.account.userProfile().createChatroom(withKey)
|
||||
return if (draftMessage != null) {
|
||||
"Room/${withKey.hashCode()}?message=$draftMessage"
|
||||
} else {
|
||||
"Room/${withKey.hashCode()}"
|
||||
}
|
||||
}
|
||||
|
||||
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
|
||||
return routeToMessage(user.pubkeyHex, draftMessage, accountViewModel)
|
||||
}
|
||||
|
||||
fun routeFor(note: Channel): String {
|
||||
return "Channel/${note.idHex}"
|
||||
}
|
||||
|
||||
fun routeFor(user: User): String {
|
||||
return "User/${user.pubkeyHex}"
|
||||
}
|
||||
|
||||
fun authorRouteFor(note: Note): String {
|
||||
return "User/${note.author?.pubkeyHex}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadDecryptedContent(
|
||||
note: Note,
|
||||
@@ -1414,8 +1295,10 @@ fun RenderTextEvent(
|
||||
)
|
||||
}
|
||||
|
||||
val hashtags = remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
if (note.event?.hasHashtags() == true) {
|
||||
val hashtags = remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1465,8 +1348,10 @@ fun RenderPoll(
|
||||
)
|
||||
}
|
||||
|
||||
val hashtags = remember { noteEvent.hashtags().toImmutableList() }
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
if (noteEvent.hasHashtags()) {
|
||||
val hashtags = remember { noteEvent.hashtags().toImmutableList() }
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1684,7 +1569,6 @@ private fun RenderPrivateMessage(
|
||||
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
||||
if (withMe) {
|
||||
LoadDecryptedContent(note, accountViewModel) { eventContent ->
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
|
||||
@@ -1713,7 +1597,10 @@ private fun RenderPrivateMessage(
|
||||
)
|
||||
}
|
||||
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
if (noteEvent.hasHashtags()) {
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1746,7 +1633,7 @@ fun DisplayRelaySet(
|
||||
|
||||
val relays by remember(baseNote) {
|
||||
mutableStateOf(
|
||||
noteEvent.relays().map { RelayBriefInfo(it) }.toImmutableList()
|
||||
noteEvent.relays().map { RelayBriefInfoCache.RelayBriefInfo(it) }.toImmutableList()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2286,45 +2173,6 @@ private fun EmojiListOptions(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoveButton(onClick: () -> Unit) {
|
||||
Button(
|
||||
modifier = Modifier.padding(start = 3.dp),
|
||||
onClick = onClick,
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.remove), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddButton(
|
||||
text: Int = R.string.add,
|
||||
isActive: Boolean = true,
|
||||
modifier: Modifier = Modifier.padding(start = 3.dp),
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
if (isActive) {
|
||||
onClick()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isActive) MaterialTheme.colorScheme.primary else Color.Gray
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RenderPinListEvent(
|
||||
@@ -2797,19 +2645,8 @@ private fun BadgeBox(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isRepost by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
||||
}
|
||||
}
|
||||
|
||||
if (isRepost) {
|
||||
val baseReply by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
baseNote.replyTo?.lastOrNull()
|
||||
}
|
||||
}
|
||||
baseReply?.let {
|
||||
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
||||
baseNote.replyTo?.lastOrNull()?.let {
|
||||
RelayBadges(it, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
@@ -2823,31 +2660,24 @@ private fun RenderAuthorImages(
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val baseRepost by remember {
|
||||
derivedStateOf {
|
||||
baseNote.replyTo?.lastOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
||||
|
||||
if (isRepost && baseRepost != null) {
|
||||
RepostNoteAuthorPicture(baseNote, baseRepost!!, accountViewModel, nav)
|
||||
if (isRepost) {
|
||||
val baseRepost = baseNote.replyTo?.lastOrNull()
|
||||
if (baseRepost != null) {
|
||||
RepostNoteAuthorPicture(baseNote, baseRepost, accountViewModel, nav)
|
||||
} else {
|
||||
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
|
||||
}
|
||||
} else {
|
||||
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
|
||||
}
|
||||
|
||||
val isChannel = baseNote.event is ChannelMessageEvent && baseNote.channelHex() != null
|
||||
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
accountViewModel.settings.showProfilePictures.value
|
||||
}
|
||||
|
||||
if (isChannel) {
|
||||
val baseChannelHex = remember { baseNote.channelHex() }
|
||||
if (baseNote.event is ChannelMessageEvent) {
|
||||
val baseChannelHex = remember(baseNote) { baseNote.channelHex() }
|
||||
if (baseChannelHex != null) {
|
||||
LoadChannel(baseChannelHex, accountViewModel) { channel ->
|
||||
ChannelNotePicture(channel, loadProfilePicture = automaticallyShowProfilePicture)
|
||||
ChannelNotePicture(channel, loadProfilePicture = accountViewModel.settings.showProfilePictures.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2880,27 +2710,12 @@ private fun ChannelNotePicture(baseChannel: Channel, loadProfilePicture: Boolean
|
||||
it.channel.profilePicture()
|
||||
}.distinctUntilChanged().observeAsState()
|
||||
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
|
||||
val modifier = remember {
|
||||
Modifier
|
||||
.width(30.dp)
|
||||
.height(30.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(backgroundColor)
|
||||
.border(
|
||||
2.dp,
|
||||
backgroundColor,
|
||||
CircleShape
|
||||
)
|
||||
}
|
||||
|
||||
Box(Size30Modifier) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = baseChannel.idHex,
|
||||
model = model,
|
||||
contentDescription = stringResource(R.string.group_picture),
|
||||
modifier = modifier,
|
||||
modifier = MaterialTheme.colorScheme.channelNotePictureModifier,
|
||||
loadProfilePicture = loadProfilePicture
|
||||
)
|
||||
}
|
||||
@@ -2913,7 +2728,7 @@ private fun RepostNoteAuthorPicture(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
GenericRepostSection(
|
||||
GenericRepostLayout(
|
||||
baseAuthorPicture = {
|
||||
NoteAuthorPicture(
|
||||
baseNote = baseNote,
|
||||
@@ -2933,45 +2748,6 @@ private fun RepostNoteAuthorPicture(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun GenericRepostSectionPreview() {
|
||||
GenericRepostSection(
|
||||
baseAuthorPicture = {
|
||||
Text("ab")
|
||||
},
|
||||
repostAuthorPicture = {
|
||||
Text("cd")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GenericRepostSection(
|
||||
baseAuthorPicture: @Composable () -> Unit,
|
||||
repostAuthorPicture: @Composable () -> Unit
|
||||
) {
|
||||
Box(modifier = Size55Modifier) {
|
||||
Box(remember { Size35Modifier.align(Alignment.TopStart) }) {
|
||||
baseAuthorPicture()
|
||||
}
|
||||
|
||||
Box(
|
||||
remember {
|
||||
Size18Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(1.dp)
|
||||
}
|
||||
) {
|
||||
RepostedIcon(modifier = Size18Modifier, MaterialTheme.colorScheme.placeholderText)
|
||||
}
|
||||
|
||||
Box(remember { Size35Modifier.align(Alignment.BottomEnd) }, contentAlignment = BottomEnd) {
|
||||
repostAuthorPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayHighlight(
|
||||
highlight: String,
|
||||
@@ -2987,9 +2763,7 @@ fun DisplayHighlight(
|
||||
val quote =
|
||||
remember {
|
||||
highlight
|
||||
.split("\n")
|
||||
.map { "> *${it.removeSuffix(" ")}*" }
|
||||
.joinToString("\n")
|
||||
.split("\n").joinToString("\n") { "> *${it.removeSuffix(" ")}*" }
|
||||
}
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
@@ -3109,238 +2883,6 @@ private fun LoadAndDisplayUser(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayFollowingCommunityInPost(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Column(HalfStartPadding) {
|
||||
Row(verticalAlignment = CenterVertically) {
|
||||
DisplayCommunity(baseNote, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayFollowingHashtagsInPost(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteEvent = remember { baseNote.event } ?: return
|
||||
|
||||
val userFollowState by accountViewModel.userFollows.observeAsState()
|
||||
var firstTag by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = userFollowState) {
|
||||
launch(Dispatchers.Default) {
|
||||
val followingTags = userFollowState?.user?.cachedFollowingTagSet() ?: emptySet()
|
||||
val newFirstTag = noteEvent.firstIsTaggedHashes(followingTags)
|
||||
|
||||
if (firstTag != newFirstTag) {
|
||||
launch(Dispatchers.Main) {
|
||||
firstTag = newFirstTag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstTag?.let {
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
Row(verticalAlignment = CenterVertically) {
|
||||
DisplayTagList(it, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayTagList(firstTag: String, nav: (String) -> Unit) {
|
||||
val displayTag = remember(firstTag) { AnnotatedString(" #$firstTag") }
|
||||
val route = remember(firstTag) { "Hashtag/$firstTag" }
|
||||
|
||||
ClickableText(
|
||||
text = displayTag,
|
||||
onClick = { nav(route) },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.52f
|
||||
)
|
||||
),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayCommunity(note: Note, nav: (String) -> Unit) {
|
||||
val communityTag = remember(note) {
|
||||
note.event?.getTagOfAddressableKind(CommunityDefinitionEvent.kind)
|
||||
} ?: return
|
||||
|
||||
val displayTag = remember(note) { AnnotatedString(getCommunityShortName(communityTag)) }
|
||||
val route = remember(note) { "Community/${communityTag.toTag()}" }
|
||||
|
||||
ClickableText(
|
||||
text = displayTag,
|
||||
onClick = { nav(route) },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.52f
|
||||
)
|
||||
),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCommunityShortName(communityTag: ATag): String {
|
||||
val name = if (communityTag.dTag.length > 10) {
|
||||
communityTag.dTag.take(10) + "..."
|
||||
} else {
|
||||
communityTag.dTag.take(10)
|
||||
}
|
||||
|
||||
return "/n/$name"
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun DisplayUncitedHashtags(
|
||||
hashtags: ImmutableList<String>,
|
||||
eventContent: String,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val hasHashtags = remember(eventContent) {
|
||||
hashtags.isNotEmpty()
|
||||
}
|
||||
|
||||
if (hasHashtags) {
|
||||
val unusedHashtags = remember(eventContent) {
|
||||
hashtags.filter { !eventContent.contains(it, true) }
|
||||
}
|
||||
|
||||
FlowRow(
|
||||
modifier = remember { Modifier.padding(top = 5.dp) }
|
||||
) {
|
||||
unusedHashtags.forEach { hashtag ->
|
||||
ClickableText(
|
||||
text = remember { AnnotatedString("#$hashtag ") },
|
||||
onClick = { nav("Hashtag/$hashtag") },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.52f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayPoW(
|
||||
pow: Int
|
||||
) {
|
||||
val powStr = remember(pow) {
|
||||
"PoW-$pow"
|
||||
}
|
||||
|
||||
Text(
|
||||
powStr,
|
||||
color = MaterialTheme.colorScheme.lessImportantLink,
|
||||
fontSize = Font14SP,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class Reward(val amount: BigDecimal)
|
||||
|
||||
@Composable
|
||||
fun DisplayReward(
|
||||
baseReward: Reward,
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column() {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.clickable { popupExpanded = true }
|
||||
) {
|
||||
ClickableText(
|
||||
text = AnnotatedString("#bounty"),
|
||||
onClick = { nav("Hashtag/bounty") },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.52f
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
RenderPledgeAmount(baseNote, baseReward, accountViewModel)
|
||||
}
|
||||
|
||||
if (popupExpanded) {
|
||||
AddBountyAmountDialog(baseNote, accountViewModel) {
|
||||
popupExpanded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderPledgeAmount(
|
||||
baseNote: Note,
|
||||
baseReward: Reward,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val repliesState by baseNote.live().replies.observeAsState()
|
||||
var reward by remember {
|
||||
mutableStateOf<String>(
|
||||
showAmount(baseReward.amount)
|
||||
)
|
||||
}
|
||||
|
||||
var hasPledge by remember {
|
||||
mutableStateOf<Boolean>(
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = repliesState) {
|
||||
launch(Dispatchers.IO) {
|
||||
repliesState?.note?.pledgedAmountByOthers()?.let {
|
||||
val newRewardAmount = showAmount(baseReward.amount.add(it))
|
||||
if (newRewardAmount != reward) {
|
||||
reward = newRewardAmount
|
||||
}
|
||||
}
|
||||
val newHasPledge = repliesState?.note?.hasPledgeBy(accountViewModel.userProfile()) == true
|
||||
if (hasPledge != newHasPledge) {
|
||||
launch(Dispatchers.Main) {
|
||||
hasPledge = newHasPledge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPledge) {
|
||||
ZappedIcon(modifier = Size20Modifier)
|
||||
} else {
|
||||
ZapIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = reward,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BadgeDisplay(baseNote: Note) {
|
||||
val background = MaterialTheme.colorScheme.background
|
||||
@@ -3684,9 +3226,11 @@ fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: Accou
|
||||
}
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
|
||||
val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() }
|
||||
DisplayUncitedHashtags(hashtags, content ?: "", nav)
|
||||
if (noteEvent.hasHashtags()) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
|
||||
val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() }
|
||||
DisplayUncitedHashtags(hashtags, content ?: "", nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
@@ -70,7 +71,6 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.animation.with
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -83,20 +82,23 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.TextType
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DarkerGreen
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height24dpModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowZapraiserSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size17dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size19dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size17Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size19Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
@@ -126,7 +128,7 @@ fun ReactionsRow(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val wantsToSeeReactions = remember {
|
||||
val wantsToSeeReactions = remember(baseNote) {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
@@ -406,10 +408,10 @@ private fun WatchBoostsAndRenderGallery(
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val boostsEvents by baseNote.live().boostList.observeAsState()
|
||||
val boostsEvents by baseNote.live().boosts.observeAsState()
|
||||
|
||||
boostsEvents?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
if (it.note.boosts.isNotEmpty()) {
|
||||
RenderBoostGallery(
|
||||
it,
|
||||
nav,
|
||||
@@ -532,13 +534,11 @@ fun ReplyReaction(
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
showCounter: Boolean = true,
|
||||
iconSize: Dp = Size17dp,
|
||||
iconSizeModifier: Modifier = Size17Modifier,
|
||||
onPress: () -> Unit
|
||||
) {
|
||||
IconButton(
|
||||
modifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
},
|
||||
modifier = iconSizeModifier,
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
onPress()
|
||||
@@ -550,7 +550,7 @@ fun ReplyReaction(
|
||||
}
|
||||
}
|
||||
) {
|
||||
CommentIcon(iconSize, grayTint)
|
||||
CommentIcon(iconSizeModifier, grayTint)
|
||||
}
|
||||
|
||||
if (showCounter) {
|
||||
@@ -596,7 +596,7 @@ val slideAnimation: ContentTransform =
|
||||
@Composable
|
||||
private fun TextCount(count: Int, textColor: Color) {
|
||||
Text(
|
||||
text = remember(count) { showCount(count) },
|
||||
text = showCount(count),
|
||||
fontSize = Font14SP,
|
||||
color = textColor,
|
||||
modifier = HalfStartPadding,
|
||||
@@ -625,17 +625,14 @@ fun BoostReaction(
|
||||
baseNote: Note,
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
iconSize: Dp = 20.dp,
|
||||
iconSizeModifier: Modifier = Size20Modifier,
|
||||
iconSize: Dp = Size20dp,
|
||||
onQuotePress: () -> Unit
|
||||
) {
|
||||
var wantsToBoost by remember { mutableStateOf(false) }
|
||||
|
||||
val iconButtonModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = iconButtonModifier,
|
||||
modifier = iconSizeModifier,
|
||||
onClick = {
|
||||
accountViewModel.tryBoost(baseNote) {
|
||||
wantsToBoost = true
|
||||
@@ -643,7 +640,7 @@ fun BoostReaction(
|
||||
}
|
||||
) {
|
||||
ObserveBoostIcon(baseNote, accountViewModel) { hasBoosted ->
|
||||
RepostedIcon(iconButtonModifier, if (hasBoosted) Color.Unspecified else grayTint)
|
||||
RepostedIcon(iconSizeModifier, if (hasBoosted) Color.Unspecified else grayTint)
|
||||
}
|
||||
|
||||
if (wantsToBoost) {
|
||||
@@ -695,36 +692,27 @@ fun LikeReaction(
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
iconSize: Dp = 20.dp,
|
||||
heartSize: Dp = 16.dp,
|
||||
iconSize: Dp = Size20dp,
|
||||
heartSizeModifier: Modifier = Size16Modifier,
|
||||
iconFontSize: TextUnit = Font14SP
|
||||
) {
|
||||
val iconButtonModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
var wantsToChangeReactionSymbol by remember { mutableStateOf(false) }
|
||||
var wantsToReact by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
contentAlignment = Center,
|
||||
modifier = iconButtonModifier.combinedClickable(
|
||||
modifier = Modifier.size(iconSize).combinedClickable(
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = Size24dp),
|
||||
onClick = {
|
||||
likeClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
onMultipleChoices = {
|
||||
wantsToReact = true
|
||||
},
|
||||
onWantsToSignReaction = {
|
||||
if (accountViewModel.account.reactionChoices.size == 1) {
|
||||
accountViewModel.reactToOrDelete(baseNote)
|
||||
} else if (accountViewModel.account.reactionChoices.size > 1) {
|
||||
wantsToReact = true
|
||||
}
|
||||
accountViewModel.reactToOrDelete(baseNote)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -736,40 +724,40 @@ fun LikeReaction(
|
||||
ObserveLikeIcon(baseNote, accountViewModel) { reactionType ->
|
||||
Crossfade(targetState = reactionType.value, label = "LikeIcon") {
|
||||
if (it != null) {
|
||||
RenderReactionType(it, heartSize, iconFontSize)
|
||||
RenderReactionType(it, heartSizeModifier, iconFontSize)
|
||||
} else {
|
||||
LikeIcon(heartSize, grayTint)
|
||||
LikeIcon(heartSizeModifier, grayTint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wantsToChangeReactionSymbol) {
|
||||
UpdateReactionTypeDialog(
|
||||
{ wantsToChangeReactionSymbol = false },
|
||||
accountViewModel = accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToReact) {
|
||||
ReactionChoicePopup(
|
||||
baseNote,
|
||||
iconSize,
|
||||
accountViewModel,
|
||||
onDismiss = {
|
||||
wantsToReact = false
|
||||
},
|
||||
onChangeAmount = {
|
||||
wantsToReact = false
|
||||
wantsToChangeReactionSymbol = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ObserveLikeText(baseNote) { reactionCount ->
|
||||
SlidingAnimationCount(reactionCount, grayTint)
|
||||
}
|
||||
|
||||
if (wantsToChangeReactionSymbol) {
|
||||
UpdateReactionTypeDialog(
|
||||
{ wantsToChangeReactionSymbol = false },
|
||||
accountViewModel = accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToReact) {
|
||||
ReactionChoicePopup(
|
||||
baseNote,
|
||||
iconSize,
|
||||
accountViewModel,
|
||||
onDismiss = {
|
||||
wantsToReact = false
|
||||
},
|
||||
onChangeAmount = {
|
||||
wantsToReact = false
|
||||
wantsToChangeReactionSymbol = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -798,14 +786,10 @@ fun ObserveLikeIcon(
|
||||
@Composable
|
||||
private fun RenderReactionType(
|
||||
reactionType: String,
|
||||
iconSize: Dp = Size20dp,
|
||||
iconSizeModifier: Modifier = Size20Modifier,
|
||||
iconFontSize: TextUnit
|
||||
) {
|
||||
val isCustom = remember(reactionType) {
|
||||
reactionType.startsWith(":")
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
if (reactionType.isNotEmpty() && reactionType[0] == ':') {
|
||||
val renderable = remember(reactionType) {
|
||||
listOf(
|
||||
ImageUrlType(reactionType.removePrefix(":").substringAfter(":"))
|
||||
@@ -820,7 +804,7 @@ private fun RenderReactionType(
|
||||
)
|
||||
} else {
|
||||
when (reactionType) {
|
||||
"+" -> LikedIcon(iconSize)
|
||||
"+" -> LikedIcon(iconSizeModifier)
|
||||
"-" -> Text(text = "\uD83D\uDC4E", fontSize = iconFontSize)
|
||||
else -> Text(text = reactionType, fontSize = iconFontSize)
|
||||
}
|
||||
@@ -835,7 +819,6 @@ fun ObserveLikeText(baseNote: Note, inner: @Composable (Int) -> Unit) {
|
||||
}
|
||||
|
||||
private fun likeClick(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
onMultipleChoices: () -> Unit,
|
||||
onWantsToSignReaction: () -> Unit
|
||||
@@ -851,7 +834,7 @@ private fun likeClick(
|
||||
R.string.login_with_a_private_key_to_like_posts
|
||||
)
|
||||
} else if (accountViewModel.account.reactionChoices.size == 1) {
|
||||
accountViewModel.reactToOrDelete(baseNote)
|
||||
onWantsToSignReaction()
|
||||
} else if (accountViewModel.account.reactionChoices.size > 1) {
|
||||
onMultipleChoices()
|
||||
}
|
||||
@@ -863,7 +846,8 @@ fun ZapReaction(
|
||||
baseNote: Note,
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
iconSize: Dp = 20.dp,
|
||||
iconSize: Dp = Size20dp,
|
||||
iconSizeModifier: Modifier = Size20Modifier,
|
||||
animationSize: Dp = 14.dp,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
@@ -884,48 +868,47 @@ fun ZapReaction(
|
||||
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier
|
||||
.size(iconSize)
|
||||
.combinedClickable(
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = Size24dp),
|
||||
onClick = {
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZappingProgress = { progress: Float ->
|
||||
scope.launch {
|
||||
zappingProgress = progress
|
||||
}
|
||||
},
|
||||
onMultipleChoices = {
|
||||
wantsToZap = true
|
||||
},
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = {
|
||||
wantsToPay = it
|
||||
modifier = iconSizeModifier.combinedClickable(
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = Size24dp),
|
||||
onClick = {
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZappingProgress = { progress: Float ->
|
||||
scope.launch {
|
||||
zappingProgress = progress
|
||||
}
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
wantsToChangeZapAmount = true
|
||||
},
|
||||
onDoubleClick = {
|
||||
wantsToSetCustomZap = true
|
||||
}
|
||||
)
|
||||
},
|
||||
onMultipleChoices = {
|
||||
wantsToZap = true
|
||||
},
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = {
|
||||
wantsToPay = it
|
||||
}
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
wantsToChangeZapAmount = true
|
||||
},
|
||||
onDoubleClick = {
|
||||
wantsToSetCustomZap = true
|
||||
}
|
||||
)
|
||||
) {
|
||||
if (wantsToZap) {
|
||||
ZapAmountChoicePopup(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
baseNote = baseNote,
|
||||
iconSize = iconSize,
|
||||
accountViewModel = accountViewModel,
|
||||
onDismiss = {
|
||||
wantsToZap = false
|
||||
zappingProgress = 0f
|
||||
@@ -1032,9 +1015,9 @@ fun ZapReaction(
|
||||
) { wasZappedByLoggedInUser ->
|
||||
Crossfade(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon") {
|
||||
if (it) {
|
||||
ZappedIcon(iconSize)
|
||||
ZappedIcon(iconSizeModifier)
|
||||
} else {
|
||||
ZapIcon(iconSize, grayTint)
|
||||
ZapIcon(iconSizeModifier, grayTint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1133,8 +1116,8 @@ private fun ObserveZapAmountText(
|
||||
fun ViewCountReaction(
|
||||
note: Note,
|
||||
grayTint: Color,
|
||||
barChartModifier: Modifier = Modifier.size(Size19dp),
|
||||
numberSizeModifier: Modifier = Modifier.height(Size24dp),
|
||||
barChartModifier: Modifier = Size19Modifier,
|
||||
numberSizeModifier: Modifier = Height24dpModifier,
|
||||
viewCountColorFilter: ColorFilter
|
||||
) {
|
||||
ViewCountIcon(barChartModifier, grayTint)
|
||||
@@ -1352,6 +1335,7 @@ private fun ActionableReactionButton(
|
||||
fun ZapAmountChoicePopup(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
iconSize: Dp,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
@@ -1359,17 +1343,19 @@ fun ZapAmountChoicePopup(
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val zapMessage = ""
|
||||
|
||||
val iconSizePx = with(LocalDensity.current) {
|
||||
-iconSize.toPx().toInt()
|
||||
}
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, -50),
|
||||
offset = IntOffset(0, iconSizePx),
|
||||
onDismissRequest = { onDismiss() }
|
||||
) {
|
||||
FlowRow(horizontalArrangement = Arrangement.Center) {
|
||||
account.zapAmountChoices.forEach { amountInSats ->
|
||||
accountViewModel.account.zapAmountChoices.forEach { amountInSats ->
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
@@ -1382,7 +1368,7 @@ fun ZapAmountChoicePopup(
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
account.defaultZapType
|
||||
accountViewModel.account.defaultZapType
|
||||
)
|
||||
onDismiss()
|
||||
},
|
||||
@@ -1407,7 +1393,7 @@ fun ZapAmountChoicePopup(
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
account.defaultZapType
|
||||
accountViewModel.account.defaultZapType
|
||||
)
|
||||
onDismiss()
|
||||
},
|
||||
|
||||
@@ -11,8 +11,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -20,35 +18,39 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconButtonModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val relayList by baseNote.live().relayInfo.observeAsState(persistentListOf())
|
||||
val relayList by baseNote.live().relayInfo.observeAsState(baseNote.relays)
|
||||
|
||||
Spacer(DoubleVertSpacer)
|
||||
|
||||
if (expanded) {
|
||||
VerticalRelayPanelWithFlow(relayList, accountViewModel, nav)
|
||||
} else {
|
||||
val shortRelayList by remember {
|
||||
derivedStateOf {
|
||||
relayList.take(3).toImmutableList()
|
||||
// FlowRow Seems to be a lot faster than LazyVerticalGrid
|
||||
FlowRow() {
|
||||
if (expanded) {
|
||||
relayList?.forEach {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
relayList?.getOrNull(0)?.let {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
relayList?.getOrNull(1)?.let {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
relayList?.getOrNull(2)?.let {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
VerticalRelayPanelWithFlow(shortRelayList, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (relayList.size > 3 && !expanded) {
|
||||
@@ -58,22 +60,6 @@ fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
@Stable
|
||||
private fun VerticalRelayPanelWithFlow(
|
||||
relays: ImmutableList<RelayBriefInfo>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
// FlowRow Seems to be a lot faster than LazyVerticalGrid
|
||||
FlowRow() {
|
||||
relays.forEach { url ->
|
||||
RenderRelay(url, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
|
||||
Row(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -9,7 +8,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
@@ -25,28 +23,25 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size13dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import com.vitorpamplona.amethyst.ui.theme.relayIconModifier
|
||||
|
||||
@Composable
|
||||
public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
@@ -62,14 +57,23 @@ public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewMo
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RenderRelayList(baseNote: Note, expanded: MutableState<Boolean>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteRelays by baseNote.live().relayInfo.observeAsState()
|
||||
val noteRelays by baseNote.live().relayInfo.observeAsState(baseNote.relays)
|
||||
|
||||
FlowRow(StdStartPadding, verticalArrangement = Arrangement.Center) {
|
||||
val relaysToDisplay = remember(noteRelays, expanded.value) {
|
||||
if (expanded.value) noteRelays else noteRelays?.take(3)?.toImmutableList()
|
||||
}
|
||||
relaysToDisplay?.forEach {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
if (expanded.value) {
|
||||
noteRelays?.forEach {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
noteRelays?.getOrNull(0)?.let {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
noteRelays?.getOrNull(1)?.let {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
noteRelays?.getOrNull(2)?.let {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +109,7 @@ fun ChatRelayExpandButton(onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun RenderRelay(relay: RelayBriefInfoCache.RelayBriefInfo, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
var relayInfo: RelayInformation? by remember { mutableStateOf(null) }
|
||||
|
||||
if (relayInfo != null) {
|
||||
@@ -165,23 +169,19 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav:
|
||||
modifier = clickableModifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
RenderRelayIcon(relay.favIcon, automaticallyShowProfilePicture)
|
||||
RenderRelayIcon(relay.displayUrl, relay.favIcon, automaticallyShowProfilePicture)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderRelayIcon(iconUrl: String, loadProfilePicture: Boolean, size: Dp = Size13dp) {
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier
|
||||
.size(size)
|
||||
.clip(shape = CircleShape)
|
||||
.background(backgroundColor)
|
||||
}
|
||||
|
||||
fun RenderRelayIcon(
|
||||
displayUrl: String,
|
||||
iconUrl: String,
|
||||
loadProfilePicture: Boolean,
|
||||
iconModifier: Modifier = MaterialTheme.colorScheme.relayIconModifier
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = iconUrl,
|
||||
robot = displayUrl,
|
||||
model = iconUrl,
|
||||
contentDescription = stringResource(id = R.string.relay_icon),
|
||||
colorFilter = RelayIconFilter,
|
||||
|
||||
@@ -3,28 +3,49 @@ package com.vitorpamplona.amethyst.ui.note
|
||||
import android.content.Context
|
||||
import android.text.format.DateUtils
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
fun timeAgo(mills: Long?, context: Context): String {
|
||||
if (mills == null) return " "
|
||||
if (mills == 0L) return " • ${context.getString(R.string.never)}"
|
||||
var locale = Locale.getDefault()
|
||||
var yearFormatter = SimpleDateFormat(" • MMM dd, yyyy", locale)
|
||||
var monthFormatter = SimpleDateFormat(" • MMM dd", locale)
|
||||
|
||||
var humanReadable = DateUtils.getRelativeTimeSpanString(
|
||||
mills * 1000,
|
||||
System.currentTimeMillis(),
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_ALL
|
||||
).toString()
|
||||
if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) {
|
||||
humanReadable = context.getString(R.string.now)
|
||||
fun timeAgo(time: Long?, context: Context): String {
|
||||
if (time == null) return " "
|
||||
if (time == 0L) return " • ${context.getString(R.string.never)}"
|
||||
|
||||
val timeDifference = TimeUtils.now() - time
|
||||
|
||||
return if (timeDifference > TimeUtils.oneYear) {
|
||||
// Dec 12, 2022
|
||||
|
||||
if (locale != Locale.getDefault()) {
|
||||
locale = Locale.getDefault()
|
||||
yearFormatter = SimpleDateFormat(" • MMM dd, yyyy", locale)
|
||||
monthFormatter = SimpleDateFormat(" • MMM dd", locale)
|
||||
}
|
||||
|
||||
yearFormatter.format(time * 1000)
|
||||
} else if (timeDifference > TimeUtils.oneMonth) {
|
||||
// Dec 12
|
||||
if (locale != Locale.getDefault()) {
|
||||
locale = Locale.getDefault()
|
||||
yearFormatter = SimpleDateFormat(" • MMM dd, yyyy", locale)
|
||||
monthFormatter = SimpleDateFormat(" • MMM dd", locale)
|
||||
}
|
||||
|
||||
monthFormatter.format(time * 1000)
|
||||
} else if (timeDifference > TimeUtils.oneDay) {
|
||||
// 2 days
|
||||
" • " + (timeDifference / TimeUtils.oneDay).toString() + context.getString(R.string.d)
|
||||
} else if (timeDifference > TimeUtils.oneHour) {
|
||||
" • " + (timeDifference / TimeUtils.oneHour).toString() + context.getString(R.string.h)
|
||||
} else if (timeDifference > TimeUtils.oneMinute) {
|
||||
" • " + (timeDifference / TimeUtils.oneMinute).toString() + context.getString(R.string.m)
|
||||
} else {
|
||||
" • " + context.getString(R.string.now)
|
||||
}
|
||||
|
||||
return " • " + humanReadable
|
||||
.replace(" hr. ago", context.getString(R.string.h))
|
||||
.replace(" min. ago", context.getString(R.string.m))
|
||||
.replace(" days ago", context.getString(R.string.d))
|
||||
.replace(" hr ago", context.getString(R.string.h))
|
||||
.replace(" min ago", context.getString(R.string.m))
|
||||
.replace("Yesterday", "1" + context.getString(R.string.d))
|
||||
}
|
||||
|
||||
fun timeAgoShort(mills: Long?, stringForNow: String): String {
|
||||
@@ -42,19 +63,3 @@ fun timeAgoShort(mills: Long?, stringForNow: String): String {
|
||||
|
||||
return humanReadable
|
||||
}
|
||||
|
||||
fun timeAgoLong(mills: Long?, context: Context): String {
|
||||
if (mills == null) return " "
|
||||
|
||||
var humanReadable = DateUtils.getRelativeTimeSpanString(
|
||||
mills * 1000,
|
||||
System.currentTimeMillis(),
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
DateUtils.FORMAT_SHOW_TIME
|
||||
).toString()
|
||||
if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) {
|
||||
humanReadable = context.getString(R.string.now)
|
||||
}
|
||||
|
||||
return humanReadable
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.TextType
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@@ -139,19 +139,13 @@ fun UserPicture(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val route by remember(user) {
|
||||
derivedStateOf {
|
||||
"User/${user.pubkeyHex}"
|
||||
}
|
||||
}
|
||||
|
||||
ClickableUserPicture(
|
||||
baseUser = user,
|
||||
size = size,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = pictureModifier,
|
||||
onClick = {
|
||||
nav(route)
|
||||
nav("User/${user.pubkeyHex}")
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -365,7 +359,10 @@ fun WatchUserFollows(userHex: String, accountViewModel: AccountViewModel, onFoll
|
||||
accountViewModel.userFollows.map {
|
||||
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
}.distinctUntilChanged()
|
||||
}.observeAsState(false)
|
||||
}.observeAsState(
|
||||
accountViewModel.account.userProfile().isFollowingCached(userHex) ||
|
||||
(userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
)
|
||||
|
||||
onFollowChanges(showFollowingMark)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -18,7 +17,6 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomHeaderCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Composable
|
||||
fun ChatroomListFeedView(
|
||||
@@ -96,14 +94,11 @@ private fun FeedLoaded(
|
||||
key = { index, item -> if (index == 0) index else item.idHex }
|
||||
) { _, item ->
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
ChatroomHeaderCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Chat Header Complete: ${item.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
ChatroomHeaderCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@@ -38,7 +37,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Composable
|
||||
fun RefresheableFeedView(
|
||||
@@ -224,27 +222,23 @@ private fun FeedLoaded(
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
val defaultModifier = remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItemPlacement()
|
||||
}
|
||||
|
||||
Row(defaultModifier) {
|
||||
NoteCompose(
|
||||
item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = Modifier,
|
||||
isBoostedNote = false,
|
||||
showHidden = state.showHidden.value,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
val defaultModifier = remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItemPlacement()
|
||||
}
|
||||
|
||||
Log.d("Rendering Metrics", "Complete: ${item.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
Row(defaultModifier) {
|
||||
NoteCompose(
|
||||
item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = Modifier,
|
||||
isBoostedNote = false,
|
||||
showHidden = state.showHidden.value,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,12 +294,12 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
||||
val oldNotesState = _feedContent.value
|
||||
if (localFilter is AdditiveFeedFilter && lastFeedKey == localFilter.feedKey()) {
|
||||
if (oldNotesState is FeedState.Loaded) {
|
||||
val newList = localFilter.updateListWith(oldNotesState.feed.value, newItems.toSet()).distinctBy { it.idHex }.toImmutableList()
|
||||
val newList = localFilter.updateListWith(oldNotesState.feed.value, newItems).distinctBy { it.idHex }.toImmutableList()
|
||||
if (!equalImmutableLists(newList, oldNotesState.feed.value)) {
|
||||
updateFeed(newList)
|
||||
}
|
||||
} else if (oldNotesState is FeedState.Empty) {
|
||||
val newList = localFilter.updateListWith(emptyList(), newItems.toSet()).distinctBy { it.idHex }.toImmutableList()
|
||||
val newList = localFilter.updateListWith(emptyList(), newItems).distinctBy { it.idHex }.toImmutableList()
|
||||
if (newList.isNotEmpty()) {
|
||||
updateFeed(newList)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.ui.screen
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -65,20 +64,22 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayPoW
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayReward
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits
|
||||
import com.vitorpamplona.amethyst.ui.elements.Reward
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.AudioHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.AudioTrackHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.note.CreateImageHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingCommunityInPost
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayHighlight
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayLocation
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayPeopleList
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayPoW
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayRelaySet
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayReward
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayZapSplits
|
||||
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
||||
@@ -95,8 +96,6 @@ import com.vitorpamplona.amethyst.ui.note.RenderPoll
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderPostApproval
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderRepost
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderTextEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.Reward
|
||||
import com.vitorpamplona.amethyst.ui.note.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -98,9 +98,10 @@ import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
|
||||
import com.vitorpamplona.amethyst.ui.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.MoreOptionsButton
|
||||
@@ -109,7 +110,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||
@@ -788,7 +788,7 @@ fun LongChannelHeader(
|
||||
)
|
||||
}
|
||||
|
||||
if (baseChannel is LiveActivitiesChannel) {
|
||||
if (baseChannel is LiveActivitiesChannel && baseChannel.info?.hasHashtags() == true) {
|
||||
val hashtags = remember(baseChannel.info) {
|
||||
baseChannel.info?.hashtags()?.toImmutableList() ?: persistentListOf()
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.AddButton
|
||||
import com.vitorpamplona.amethyst.ui.elements.AddButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -254,13 +255,13 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
val bottomBarHeightPx = with(LocalDensity.current) { 50.dp.roundToPx().toFloat() }
|
||||
val bottomBarOffsetHeightPx = remember { mutableStateOf(0f) }
|
||||
val bottomBarOffsetHeightPx = remember { mutableFloatStateOf(0f) }
|
||||
val shouldShow = remember { mutableStateOf(true) }
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val newOffset = bottomBarOffsetHeightPx.value + available.y
|
||||
val newOffset = bottomBarOffsetHeightPx.floatValue + available.y
|
||||
|
||||
if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) {
|
||||
val newBottomBarOffset = if (navState.value?.destination?.route !in InvertedLayouts) {
|
||||
@@ -269,16 +270,16 @@ fun MainScreen(
|
||||
newOffset.coerceIn(0f, bottomBarHeightPx)
|
||||
}
|
||||
|
||||
if (newBottomBarOffset != bottomBarOffsetHeightPx.value) {
|
||||
bottomBarOffsetHeightPx.value = newBottomBarOffset
|
||||
if (newBottomBarOffset != bottomBarOffsetHeightPx.floatValue) {
|
||||
bottomBarOffsetHeightPx.floatValue = newBottomBarOffset
|
||||
}
|
||||
} else {
|
||||
if (abs(bottomBarOffsetHeightPx.value) > 0.1) {
|
||||
bottomBarOffsetHeightPx.value = 0f
|
||||
if (abs(bottomBarOffsetHeightPx.floatValue) > 0.1) {
|
||||
bottomBarOffsetHeightPx.floatValue = 0f
|
||||
}
|
||||
}
|
||||
|
||||
val newShouldShow = abs(bottomBarOffsetHeightPx.value) < bottomBarHeightPx / 2.0f
|
||||
val newShouldShow = abs(bottomBarOffsetHeightPx.floatValue) < bottomBarHeightPx / 2.0f
|
||||
|
||||
if (shouldShow.value != newShouldShow) {
|
||||
shouldShow.value = newShouldShow
|
||||
@@ -290,7 +291,7 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
WatchNavStateToUpdateBarVisibility(navState) {
|
||||
bottomBarOffsetHeightPx.value = 0f
|
||||
bottomBarOffsetHeightPx.floatValue = 0f
|
||||
shouldShow.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -109,12 +109,12 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaIntent
|
||||
import com.vitorpamplona.amethyst.ui.note.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.qrcode.ShowQRDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.BoostReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
|
||||
@@ -59,7 +60,6 @@ import com.vitorpamplona.amethyst.ui.note.ReplyReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.ViewCountReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchForReports
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
@@ -69,7 +69,11 @@ import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size39Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.onBackgroundColorFilter
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
@@ -270,7 +274,7 @@ fun RenderReportState(
|
||||
|
||||
Crossfade(targetState = (!state.isAcceptable || state.isHiddenAuthor) && !showReportedNote) { showHiddenNote ->
|
||||
if (showHiddenNote) {
|
||||
Column(remember { Modifier.fillMaxSize() }, verticalArrangement = Arrangement.Center) {
|
||||
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
|
||||
HiddenNote(
|
||||
state.relevantReports,
|
||||
state.isHiddenAuthor,
|
||||
@@ -402,9 +406,9 @@ private fun VideoUserOptionAction(
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteRelays by baseNote.live().relayInfo.observeAsState()
|
||||
val noteRelays by baseNote.live().relayInfo.observeAsState(baseNote.relays)
|
||||
|
||||
FlowRow() {
|
||||
FlowRow {
|
||||
noteRelays?.forEach { relayInfo ->
|
||||
RenderRelay(relayInfo, accountViewModel, nav)
|
||||
}
|
||||
@@ -432,17 +436,49 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(bottom = 75.dp, end = 20.dp)) {
|
||||
ReplyReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, iconSize = 40.dp) {
|
||||
ReplyReaction(
|
||||
baseNote = baseNote,
|
||||
grayTint = MaterialTheme.colorScheme.onBackground,
|
||||
accountViewModel = accountViewModel,
|
||||
iconSizeModifier = Size40Modifier
|
||||
) {
|
||||
routeFor(
|
||||
baseNote,
|
||||
accountViewModel.userProfile()
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
BoostReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, iconSize = 40.dp) {
|
||||
BoostReaction(
|
||||
baseNote = baseNote,
|
||||
grayTint = MaterialTheme.colorScheme.onBackground,
|
||||
accountViewModel = accountViewModel,
|
||||
iconSizeModifier = Size40Modifier,
|
||||
iconSize = Size40dp
|
||||
) {
|
||||
wantsToQuote = baseNote
|
||||
}
|
||||
LikeReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, nav, iconSize = 40.dp, heartSize = Size35dp, 28.sp)
|
||||
ZapReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp, nav = nav)
|
||||
ViewCountReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, barChartModifier = Modifier.size(39.dp), viewCountColorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter)
|
||||
LikeReaction(
|
||||
baseNote = baseNote,
|
||||
grayTint = MaterialTheme.colorScheme.onBackground,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
iconSize = Size40dp,
|
||||
heartSizeModifier = Size35Modifier,
|
||||
28.sp
|
||||
)
|
||||
ZapReaction(
|
||||
baseNote = baseNote,
|
||||
grayTint = MaterialTheme.colorScheme.onBackground,
|
||||
accountViewModel = accountViewModel,
|
||||
iconSize = Size40dp,
|
||||
iconSizeModifier = Size40Modifier,
|
||||
animationSize = Size35dp,
|
||||
nav = nav
|
||||
)
|
||||
ViewCountReaction(
|
||||
note = baseNote,
|
||||
grayTint = MaterialTheme.colorScheme.onBackground,
|
||||
barChartModifier = Size39Modifier,
|
||||
viewCountColorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ val Size75dp = 75.dp
|
||||
val HalfStartPadding = Modifier.padding(start = 5.dp)
|
||||
val StdStartPadding = Modifier.padding(start = 10.dp)
|
||||
val StdTopPadding = Modifier.padding(top = 10.dp)
|
||||
val HalfTopPadding = Modifier.padding(top = 5.dp)
|
||||
|
||||
val HalfPadding = Modifier.padding(5.dp)
|
||||
val StdPadding = Modifier.padding(10.dp)
|
||||
@@ -88,13 +89,17 @@ val Size6Modifier = Modifier.size(6.dp)
|
||||
val Size10Modifier = Modifier.size(10.dp)
|
||||
val Size15Modifier = Modifier.size(15.dp)
|
||||
val Size16Modifier = Modifier.size(16.dp)
|
||||
val Size17Modifier = Modifier.size(17.dp)
|
||||
val Size18Modifier = Modifier.size(18.dp)
|
||||
val Size19Modifier = Modifier.size(19.dp)
|
||||
val Size20Modifier = Modifier.size(20.dp)
|
||||
val Size22Modifier = Modifier.size(22.dp)
|
||||
val Size24Modifier = Modifier.size(24.dp)
|
||||
val Size26Modifier = Modifier.size(26.dp)
|
||||
val Size30Modifier = Modifier.size(30.dp)
|
||||
val Size35Modifier = Modifier.size(35.dp)
|
||||
val Size39Modifier = Modifier.size(39.dp)
|
||||
val Size40Modifier = Modifier.size(40.dp)
|
||||
val Size50Modifier = Modifier.size(50.dp)
|
||||
val Size55Modifier = Modifier.size(55.dp)
|
||||
|
||||
@@ -113,6 +118,7 @@ val ReactionRowHeightChat = Modifier.height(25.dp)
|
||||
val UserNameRowHeight = Modifier.fillMaxWidth()
|
||||
val UserNameMaxRowHeight = Modifier.fillMaxWidth()
|
||||
|
||||
val Height24dpModifier = Modifier.height(24.dp)
|
||||
val Height4dpModifier = Modifier.height(4.dp)
|
||||
|
||||
val AccountPictureModifier = Modifier.size(55.dp).clip(shape = CircleShape)
|
||||
@@ -158,3 +164,26 @@ val IconRowTextModifier = Modifier.padding(start = 16.dp)
|
||||
val IconRowModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 15.dp, horizontal = 25.dp)
|
||||
|
||||
val emptyLineItemModifier = Modifier.height(Size75dp).fillMaxWidth()
|
||||
|
||||
val normalNoteModifier = Modifier.fillMaxWidth()
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 0.dp
|
||||
)
|
||||
|
||||
val normalWithTopMarginNoteModifier = Modifier.fillMaxWidth()
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp
|
||||
)
|
||||
|
||||
val boostedNoteModifier = Modifier.fillMaxWidth()
|
||||
.padding(
|
||||
start = 0.dp,
|
||||
end = 0.dp,
|
||||
top = 0.dp
|
||||
)
|
||||
|
||||
@@ -4,12 +4,14 @@ import android.app.Activity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -27,6 +29,7 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.patrykandpatrick.vico.compose.style.ChartStyle
|
||||
@@ -95,35 +98,18 @@ private val LightZapraiserBackground = BitcoinOrange.copy(0.52f).compositeOver(L
|
||||
private val DarkOverPictureBackground = DarkColorPalette.background.copy(0.62f)
|
||||
private val LightOverPictureBackground = LightColorPalette.background.copy(0.62f)
|
||||
|
||||
val RepostPictureBorderDark = Modifier.border(
|
||||
2.dp,
|
||||
DarkColorPalette.background,
|
||||
CircleShape
|
||||
)
|
||||
|
||||
val RepostPictureBorderLight = Modifier.border(
|
||||
2.dp,
|
||||
LightColorPalette.background,
|
||||
CircleShape
|
||||
)
|
||||
val RepostPictureBorderDark = Modifier.border(2.dp, DarkColorPalette.background, CircleShape)
|
||||
val RepostPictureBorderLight = Modifier.border(2.dp, LightColorPalette.background, CircleShape)
|
||||
|
||||
val DarkImageModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
DarkSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
.border(1.dp, DarkSubtleBorder, QuoteBorder)
|
||||
|
||||
val LightImageModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
LightSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkProfile35dpModifier = Modifier
|
||||
.size(Size35dp)
|
||||
@@ -137,41 +123,57 @@ val DarkReplyBorderModifier = Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
DarkSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
.border(1.dp, DarkSubtleBorder, QuoteBorder)
|
||||
|
||||
val LightReplyBorderModifier = Modifier
|
||||
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
LightSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkInnerPostBorderModifier = Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
DarkSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
.border(1.dp, DarkSubtleBorder, QuoteBorder)
|
||||
|
||||
val LightInnerPostBorderModifier = Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
LightSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkChannelNotePictureModifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(DarkColorPalette.background)
|
||||
.border(2.dp, DarkColorPalette.background, CircleShape)
|
||||
|
||||
val LightChannelNotePictureModifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(LightColorPalette.background)
|
||||
.border(2.dp, LightColorPalette.background, CircleShape)
|
||||
|
||||
val LightRelayIconModifier = Modifier
|
||||
.size(Size13dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(LightColorPalette.background)
|
||||
|
||||
val DarkRelayIconModifier = Modifier
|
||||
.size(Size13dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(DarkColorPalette.background)
|
||||
|
||||
val LightLargeRelayIconModifier = Modifier
|
||||
.size(Size55dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(LightColorPalette.background)
|
||||
|
||||
val DarkLargeRelayIconModifier = Modifier
|
||||
.size(Size55dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(DarkColorPalette.background)
|
||||
|
||||
val RichTextDefaults = RichTextStyle().resolveDefaults()
|
||||
|
||||
@@ -190,11 +192,7 @@ val MarkDownStyleOnDark = RichTextDefaults.copy(
|
||||
.padding(0.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
DarkSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
.border(1.dp, DarkSubtleBorder, QuoteBorder)
|
||||
.background(DarkColorPalette.onSurface.copy(alpha = 0.05f))
|
||||
),
|
||||
stringStyle = RichTextDefaults.stringStyle?.copy(
|
||||
@@ -224,11 +222,7 @@ val MarkDownStyleOnLight = RichTextDefaults.copy(
|
||||
.padding(0.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
LightSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
.background(DarkColorPalette.onSurface.copy(alpha = 0.05f))
|
||||
),
|
||||
stringStyle = RichTextDefaults.stringStyle?.copy(
|
||||
@@ -320,6 +314,15 @@ val ColorScheme.replyModifier: Modifier
|
||||
val ColorScheme.innerPostModifier: Modifier
|
||||
get() = if (isLight) LightInnerPostBorderModifier else DarkInnerPostBorderModifier
|
||||
|
||||
val ColorScheme.channelNotePictureModifier: Modifier
|
||||
get() = if (isLight) LightChannelNotePictureModifier else DarkChannelNotePictureModifier
|
||||
|
||||
val ColorScheme.relayIconModifier: Modifier
|
||||
get() = if (isLight) LightRelayIconModifier else DarkRelayIconModifier
|
||||
|
||||
val ColorScheme.largeRelayIconModifier: Modifier
|
||||
get() = if (isLight) LightLargeRelayIconModifier else DarkLargeRelayIconModifier
|
||||
|
||||
val ColorScheme.chartStyle: ChartStyle
|
||||
get() {
|
||||
val defaultColors = if (isLight) DefaultColors.Light else DefaultColors.Dark
|
||||
@@ -367,3 +370,27 @@ fun AmethystTheme(sharedPrefsViewModel: SharedPreferencesViewModel, content: @Co
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemeComparison(
|
||||
onDark: @Composable () -> Unit,
|
||||
onLight: @Composable () -> Unit
|
||||
) {
|
||||
Column() {
|
||||
val darkTheme: SharedPreferencesViewModel = viewModel()
|
||||
darkTheme.updateTheme(ThemeType.DARK)
|
||||
AmethystTheme(darkTheme) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
onDark()
|
||||
}
|
||||
}
|
||||
|
||||
val lightTheme: SharedPreferencesViewModel = viewModel()
|
||||
lightTheme.updateTheme(ThemeType.LIGHT)
|
||||
AmethystTheme(lightTheme) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
onLight()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,18 +293,6 @@
|
||||
<string name="zap_type_anonymous_explainer">المستقبل والجمهور لا يعرفان من الذي قام بإرسال الدفع</string>
|
||||
<string name="zap_type_nonzap_explainer">لا يوجد أثر في نوستر، فقط على شبكة الLightning</string>
|
||||
<string name="file_server">خادم الملفات</string>
|
||||
<string name="upload_server_imgur">imgur.com - موثوق به</string>
|
||||
<string name="upload_server_imgur_explainer">يمكن لـ Imgur تعديل الملف</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - (موثوق به)</string>
|
||||
<string name="upload_server_nostrimg_explainer">يمكن لـ NostrImg تعديل الملف</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build- (موثوق به)</string>
|
||||
<string name="upload_server_nostrbuild_explainer">يمكن لـ Nostr.build تعديل الملف</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - (موثوق به)</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">يمكن لـ Nostrfiles.dev تعديل الملف</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - (موثوق به)</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">يمكن لـ nostrcheck.me تعديل الملف</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">تحقق مما إذا قام Imgur بتعديل الملف.
|
||||
NIP جديد: قد لا يراه العملاء الآخرون</string>
|
||||
<string name="upload_server_relays_nip95">المرحلات الخاصة بك (NIP-95)</string>
|
||||
<string name="connect_via_tor_short">إعداد Tor/Orbot</string>
|
||||
<string name="connect_via_tor">الاتصال من خلال إعدادات Orbot الخاص بك</string>
|
||||
|
||||
@@ -299,26 +299,6 @@
|
||||
<string name="zap_type_nonzap_explainer">নস্টারে কোনো সন্ধান নেই, কেবল বিজলিতে আছে</string>
|
||||
<string name="file_server">ফাইল সার্ভার</string>
|
||||
<string name="zap_forward_lnAddress">বিজলি-ঠিকানা বা @ব্যবহারকারী</string>
|
||||
<string name="upload_server_imgur">imgur.com - বিশ্বস্ত</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - বিশ্বস্ত</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - বিশ্বস্ত</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - বিশ্বস্ত</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - বিশ্বস্ত</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_imgur_nip94">যাচাইযোগ্য Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Imgur ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_nostrimg_nip94">যাচাইযোগ্য NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">NostrImg ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_nostrbuild_nip94">যাচাইযোগ্য Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Nostr.build ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">যাচাইযোগ্য Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Nostrfiles.dev ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">যাচাইযোগ্য Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Nostrcheck.me ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_relays_nip95">আপনার রিলেগুলি (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">ফাইলগুলো আপনার রিলে দ্বারা গৃহীত হয়। নতুন NIP: তারা এটি সমর্থন করে কিনা দেখে নিন</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot সেটআপ করুন</string>
|
||||
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Žádná stopa v Nostr, pouze v Lightning</string>
|
||||
<string name="file_server">Souborový server</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress nebo @Uživatel</string>
|
||||
<string name="upload_server_imgur">imgur.com – důvěryhodný</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur může upravit soubor</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com – důvěryhodný</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg může upravit soubor</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build – důvěryhodný</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build může upravit soubor</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev – důvěryhodný</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev může upravit soubor</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - důvěryhodný</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me může upravit soubor</string>
|
||||
<string name="upload_server_imgur_nip94">Ověřitelný Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Ověří, zda Imgur upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
<string name="upload_server_nostrimg_nip94">Ověřitelný NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Ověří, zda NostrImg upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Ověřitelný Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Ověří, zda Nostr.build upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Ověřitelný Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Ověří, zda Nostrfiles.dev upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Ověřitelný Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Ověří, zda Nostrcheck.me upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
<string name="upload_server_relays_nip95">Vaše relé (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Soubory jsou hostovány vašimi relé. Nový NIP: zkontrolujte, zda je podporují</string>
|
||||
<string name="connect_via_tor_short">Nastavení Tor/Orbot</string>
|
||||
@@ -600,4 +580,5 @@
|
||||
<string name="classifieds_category_food">Potraviny</string>
|
||||
<string name="classifieds_category_misc">Různé</string>
|
||||
<string name="classifieds_category_other">Ostatní</string>
|
||||
<string name="failed_to_upload_media_no_details">Nepodařilo se nahrát média</string>
|
||||
</resources>
|
||||
|
||||
@@ -312,26 +312,6 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="zap_type_nonzap_explainer">Keine Spur in Nostr, nur in Lightning</string>
|
||||
<string name="file_server">Dateiserver</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress oder @Benutzer</string>
|
||||
<string name="upload_server_imgur">imgur.com - vertrauenswürdig</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur kann die Datei ändern</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - vertrauenswürdig</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg kann die Datei ändern</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - vertrauenswürdig</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build kann die Datei ändern</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - vertrauenswürdig</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev kann die Datei ändern</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - vertrauenswürdig</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me kann die Datei ändern</string>
|
||||
<string name="upload_server_imgur_nip94">Verifizierbares Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Überprüft, ob Imgur die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
<string name="upload_server_nostrimg_nip94">Verifizierbares NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Überprüft, ob NostrImg die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Verifizierbares Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Überprüft, ob Nostr.build die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Verifizierbares Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Überprüft, ob Nostrfiles.dev die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Verifizierbares Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Überprüft, ob Nostrcheck.me die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
<string name="upload_server_relays_nip95">Deine Relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Dateien werden von deinen Relays gehostet. Neuer NIP: Überprüfe, ob sie unterstützt werden</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot-Einrichtung</string>
|
||||
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Δεν αφήνει ίχνος στο Nostr, παρά μόνο στο Lightning</string>
|
||||
<string name="file_server">Εξυπηρετητής Αρχείων</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress (διεύθυνση Lightning) ή @Χρήστης</string>
|
||||
<string name="upload_server_imgur">imgur.com - έμπιστο</string>
|
||||
<string name="upload_server_imgur_explainer">Το imgur μπορεί να τροποποιήσει το αρχείο</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - έμπιστο</string>
|
||||
<string name="upload_server_nostrimg_explainer">Το nostrimg μπορεί να τροποποιήσει το αρχείο</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - έμπιστο</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Το nostr.build μπορεί να τροποποιήσει το αρχείο</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - έμπιστο</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Το nostrfiles.dev μπορεί να τροποποιήσει το αρχείο</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - έμπιστο</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">Το nostrcheck.me μπορεί να τροποποιήσει το αρχείο</string>
|
||||
<string name="upload_server_imgur_nip94">Επαληθεύσιμο Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Ελέγχει αν το Imgur τροποποίησε το αρχείο. Νέο NIP: η δημοσίευση μπορεί να μην είναι ορατή σε άλλους Nostr clients</string>
|
||||
<string name="upload_server_nostrimg_nip94">Επαληθεύσιμο NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Ελέγχει αν το NostrImg τροποποίησε το αρχείο. Νέο NIP: η δημοσίευση μπορεί να μην είναι ορατή σε άλλους Nostr clients</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Επαληθεύσιμο Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Ελέγχει αν το Nostr.build τροποποίησε το αρχείο. Νέο NIP: η δημοσίευση μπορεί να μην είναι ορατή σε άλλους Nostr clients</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Επαληθεύσιμο Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Ελέγχει αν το Nostrfiles.dev τροποποίησε το αρχείο. Νέο NIP: η δημοσίευση μπορεί να μην είναι ορατή σε άλλους Nostr clients</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Επαληθεύσιμο Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Ελέγχει αν το Nostrcheck.me τροποποίησε το αρχείο. Νέο NIP: η δημοσίευση μπορεί να μην είναι ορατή σε άλλους Nostr clients</string>
|
||||
<string name="upload_server_relays_nip95">Οι Διαμοιραστές σας (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Τα αρχεία φιλοξενούνται στους Διακομιστές σας (relays). Νέο NIP: ελέγξτε αν το υποστηρίζουν</string>
|
||||
<string name="connect_via_tor_short">Ρυθμίσεις Tor/Orbot</string>
|
||||
|
||||
@@ -299,26 +299,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Ne spuro en Nostr, nur en Lightning</string>
|
||||
<string name="file_server">Dosiera Servilo</string>
|
||||
<string name="zap_forward_lnAddress">LNAdreson aŭ @Uzanto</string>
|
||||
<string name="upload_server_imgur">imgur.com - fidinda</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur povas modifi la dosieron</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - fidinda</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg povas modifi la dosieron</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - fidinda</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build povas modifi la dosieron</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - fidinda</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev povas modifi la dosieron</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - fidinda</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me povas modifi la dosieron</string>
|
||||
<string name="upload_server_imgur_nip94">Atestebla Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Kontrolas se Imgur modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
|
||||
<string name="upload_server_nostrimg_nip94">Atestebla NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Kontrolas se NostrImg modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Atestebla Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Kontrolas se Nostr.build modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Atestebla NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Kontrolas se Nostrfiles.dev modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Atestebla Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Kontrolas se Nostrcheck.me modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
|
||||
<string name="upload_server_relays_nip95">Viaj plusendiloj (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Dosieroj estas konservita de viaj plusendiloj. Nova NIP: kontrolu se ili subtenas</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot aranĝo</string>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Ver recuento</string>
|
||||
<string name="boost">Impulsar</string>
|
||||
<string name="boosted">impulsada</string>
|
||||
<string name="boosted">Impulsada</string>
|
||||
<string name="quote">Citar</string>
|
||||
<string name="new_amount_in_sats">Nueva cantidad en sats</string>
|
||||
<string name="add">Agregar</string>
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">No hay rastro en Nostr, solo en Lightning</string>
|
||||
<string name="file_server">Servidor de archivos</string>
|
||||
<string name="zap_forward_lnAddress">Dirección o @usuario de Lightning</string>
|
||||
<string name="upload_server_imgur">imgur.com (confiable)</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur puede modificar el archivo</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com (confiable)</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg puede modificar el archivo</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build (confiable)</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build puede modificar el archivo</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev (confiable)</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev puede modificar el archivo</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me (confiable)</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me puede modificar el archivo</string>
|
||||
<string name="upload_server_imgur_nip94">Imgur verificable (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Comprueba si Imgur modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_nostrimg_nip94">NostrImg verificable (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Comprueba si NostrImg modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build verificable (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Comprueba si NostrImg modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev verificable (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Comprueba si Nostrfiles.dev modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me verificable (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Comprueba si Nostrcheck.me modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_relays_nip95">Tus relés (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Los archivos están alojados en tus relés. Nuevo NIP: comprueba si son compatibles.</string>
|
||||
<string name="connect_via_tor_short">Configuración de Tor/Orbot</string>
|
||||
@@ -406,6 +386,9 @@
|
||||
<string name="payment">Pago</string>
|
||||
<string name="cashu">Token de Cashu</string>
|
||||
<string name="cashu_redeem">Canjear</string>
|
||||
<string name="cashu_redeem_to_zap">Enviar a billetera de zaps</string>
|
||||
<string name="cashu_redeem_to_cashu">Abrir en billetera de Cashu</string>
|
||||
<string name="cashu_copy_token">Copiar token</string>
|
||||
<string name="no_lightning_address_set">La dirección de Lightning no está configurada</string>
|
||||
<string name="copied_token_to_clipboard">Token copiado al portapapeles</string>
|
||||
<string name="live_stream_live_tag">EN VIVO</string>
|
||||
@@ -417,6 +400,7 @@
|
||||
<string name="are_you_sure_you_want_to_log_out">Al cerrar la sesión se borra toda tu información local. Asegúrate de tener una copia de seguridad de tus claves privadas para que no pierdas la cuenta. ¿Quieres continuar?</string>
|
||||
<string name="followed_tags">Etiquetas seguidas</string>
|
||||
<string name="relay_setup">Relés</string>
|
||||
<string name="discover_marketplace">Mercado</string>
|
||||
<string name="discover_live">En vivo</string>
|
||||
<string name="discover_community">Comunidad</string>
|
||||
<string name="discover_chat">Chats</string>
|
||||
@@ -475,9 +459,10 @@
|
||||
<string name="automatically_play_videos_description">Reproducir automáticamente videos y GIF</string>
|
||||
<string name="automatically_show_url_preview_description">Mostrar vistas previas de URL</string>
|
||||
<string name="load_image_description">Cuándo cargar imágenes</string>
|
||||
<string name="copy_to_clipboard">Copiar al portapapeles</string>
|
||||
<string name="copy_url_to_clipboard">Copiar URL al portapapeles</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copiar ID de la nota al portapapeles</string>
|
||||
<string name="created_at">Creado el</string>
|
||||
<string name="created_at">Creado</string>
|
||||
<string name="rules">Reglas</string>
|
||||
<string name="login_with_external_signer">Iniciar sesión con Amber</string>
|
||||
<string name="status_update">Actualiza tu estado</string>
|
||||
@@ -530,6 +515,7 @@
|
||||
<string name="cashu_failed_redemption_explainer_already_spent">Los tokens de Cashu ya se gastaron.</string>
|
||||
<string name="cashu_successful_redemption">Cashu recibido</string>
|
||||
<string name="cashu_successful_redemption_explainer">Se enviaron %1$s sats a tu billetera (comisión: %2$s sats).</string>
|
||||
<string name="cashu_no_wallet_found">No se encontró ninguna billetera Cashu compatible en el sistema</string>
|
||||
<string name="error_unable_to_fetch_invoice">No se puede obtener la factura de los servidores del destinatario</string>
|
||||
<string name="wallet_connect_pay_invoice_error_error">El proveedor de conexión de billetera devolvió el siguiente error: %1$s</string>
|
||||
<string name="could_not_connect_to_tor">No se pudo conectar a Tor</string>
|
||||
@@ -556,4 +542,42 @@
|
||||
<string name="push_server_install_app_description"> Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Nfty](https://ntfy.sh/).
|
||||
Tras la instalación, selecciona la aplicación que deseas usar en la configuración.
|
||||
</string>
|
||||
<string name="payment_required_title">Mensaje de %1$s</string>
|
||||
<string name="thread_title">Hilo</string>
|
||||
<string name="send_the_seller_a_message">Envía un mensaje al vendedor</string>
|
||||
<string name="hi_seller_is_this_still_available">Hola, %1$s, ¿esto todavía está disponible?</string>
|
||||
<string name="hi_there_is_this_still_available">Hola, ¿esto todavía está disponible?</string>
|
||||
<string name="classifieds">Vender un artículo</string>
|
||||
<string name="classifieds_title">Título</string>
|
||||
<string name="classifieds_title_placeholder">iPhone 13</string>
|
||||
<string name="classifieds_condition">Estado</string>
|
||||
<string name="classifieds_category">Categoría</string>
|
||||
<string name="classifieds_price">Precio (en sats)</string>
|
||||
<string name="classifieds_price_placeholder">1000</string>
|
||||
<string name="classifieds_location">Ubicación</string>
|
||||
<string name="classifieds_location_placeholder">Ciudad, estado, país.</string>
|
||||
<string name="classifieds_condition_new">Nuevo</string>
|
||||
<string name="classifieds_condition_new_explainer">Producto totalmente nuevo, en la caja original.</string>
|
||||
<string name="classifieds_condition_like_new">Como nuevo</string>
|
||||
<string name="classifieds_condition_like_new_explainer">Usado, pero sin señales de uso.</string>
|
||||
<string name="classifieds_condition_good">Bueno</string>
|
||||
<string name="classifieds_condition_good_explainer">Tiene algunas marcas de uso superficiales.</string>
|
||||
<string name="classifieds_condition_fair">Aceptable</string>
|
||||
<string name="classifieds_condition_fair_explainer">Todavía está en forma aceptable y funcional.</string>
|
||||
<string name="classifieds_category_clothing">Ropa</string>
|
||||
<string name="classifieds_category_accessories">Accesorios</string>
|
||||
<string name="classifieds_category_electronics">Artículos electrónicos</string>
|
||||
<string name="classifieds_category_furniture">Muebles</string>
|
||||
<string name="classifieds_category_collectibles">Objetos de colección</string>
|
||||
<string name="classifieds_category_books">Libros</string>
|
||||
<string name="classifieds_category_pets">Mascotas</string>
|
||||
<string name="classifieds_category_sports">Deportes</string>
|
||||
<string name="classifieds_category_fitness">Acondicionamiento físico</string>
|
||||
<string name="classifieds_category_art">Arte</string>
|
||||
<string name="classifieds_category_crafts">Artesanías</string>
|
||||
<string name="classifieds_category_home">Hogar</string>
|
||||
<string name="classifieds_category_office">Oficina</string>
|
||||
<string name="classifieds_category_food">Comida</string>
|
||||
<string name="classifieds_category_misc">Varios</string>
|
||||
<string name="classifieds_category_other">Otros</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Ver recuento</string>
|
||||
<string name="boost">Impulsar</string>
|
||||
<string name="boosted">impulsada</string>
|
||||
<string name="boosted">Impulsada</string>
|
||||
<string name="quote">Citar</string>
|
||||
<string name="new_amount_in_sats">Nueva cantidad en sats</string>
|
||||
<string name="add">Agregar</string>
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">No hay rastro en Nostr, solo en Lightning</string>
|
||||
<string name="file_server">Servidor de archivos</string>
|
||||
<string name="zap_forward_lnAddress">Dirección o @usuario de Lightning</string>
|
||||
<string name="upload_server_imgur">imgur.com (confiable)</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur puede modificar el archivo</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com (confiable)</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg puede modificar el archivo</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build (confiable)</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build puede modificar el archivo</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev (confiable)</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev puede modificar el archivo</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me (confiable)</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me puede modificar el archivo</string>
|
||||
<string name="upload_server_imgur_nip94">Imgur verificable (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Comprueba si Imgur modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_nostrimg_nip94">NostrImg verificable (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Comprueba si NostrImg modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build verificable (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Comprueba si NostrImg modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev verificable (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Comprueba si Nostrcheck.me modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me verificable (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Comprueba si Nostrcheck.me modificó el archivo. Nuevo NIP: es posible que otros clientes no lo vean.</string>
|
||||
<string name="upload_server_relays_nip95">Tus relés (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Los archivos están alojados en tus relés. Nuevo NIP: comprueba si son compatibles.</string>
|
||||
<string name="connect_via_tor_short">Configuración de Tor/Orbot</string>
|
||||
@@ -406,6 +386,9 @@
|
||||
<string name="payment">Pago</string>
|
||||
<string name="cashu">Token de Cashu</string>
|
||||
<string name="cashu_redeem">Canjear</string>
|
||||
<string name="cashu_redeem_to_zap">Enviar a billetera de zaps</string>
|
||||
<string name="cashu_redeem_to_cashu">Abrir en billetera de Cashu</string>
|
||||
<string name="cashu_copy_token">Copiar token</string>
|
||||
<string name="no_lightning_address_set">La dirección de Lightning no está configurada</string>
|
||||
<string name="copied_token_to_clipboard">Token copiado al portapapeles</string>
|
||||
<string name="live_stream_live_tag">EN VIVO</string>
|
||||
@@ -417,6 +400,7 @@
|
||||
<string name="are_you_sure_you_want_to_log_out">Al cerrar la sesión se borra toda tu información local. Asegúrate de tener una copia de seguridad de tus claves privadas para que no pierdas la cuenta. ¿Quieres continuar?</string>
|
||||
<string name="followed_tags">Etiquetas seguidas</string>
|
||||
<string name="relay_setup">Relés</string>
|
||||
<string name="discover_marketplace">Mercado</string>
|
||||
<string name="discover_live">En vivo</string>
|
||||
<string name="discover_community">Comunidad</string>
|
||||
<string name="discover_chat">Chats</string>
|
||||
@@ -475,9 +459,10 @@
|
||||
<string name="automatically_play_videos_description">Reproducir automáticamente videos y GIF</string>
|
||||
<string name="automatically_show_url_preview_description">Mostrar vistas previas de URL</string>
|
||||
<string name="load_image_description">Cuándo cargar imágenes</string>
|
||||
<string name="copy_to_clipboard">Copiar al portapapeles</string>
|
||||
<string name="copy_url_to_clipboard">Copiar URL al portapapeles</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copiar ID de la nota al portapapeles</string>
|
||||
<string name="created_at">Creada el</string>
|
||||
<string name="created_at">Creado</string>
|
||||
<string name="rules">Reglas</string>
|
||||
<string name="login_with_external_signer">Iniciar sesión con Amber</string>
|
||||
<string name="status_update">Actualiza tu estado</string>
|
||||
@@ -530,6 +515,7 @@
|
||||
<string name="cashu_failed_redemption_explainer_already_spent">Los tokens de Cashu ya se gastaron.</string>
|
||||
<string name="cashu_successful_redemption">Cashu recibido</string>
|
||||
<string name="cashu_successful_redemption_explainer">Se enviaron %1$s sats a tu billetera (comisión: %2$s sats).</string>
|
||||
<string name="cashu_no_wallet_found">No se encontró ninguna billetera Cashu compatible en el sistema</string>
|
||||
<string name="error_unable_to_fetch_invoice">No se puede obtener la factura de los servidores del destinatario</string>
|
||||
<string name="wallet_connect_pay_invoice_error_error">El proveedor de conexión de billetera devolvió el siguiente error: %1$s</string>
|
||||
<string name="could_not_connect_to_tor">No se pudo conectar a Tor</string>
|
||||
@@ -556,4 +542,42 @@
|
||||
<string name="push_server_install_app_description"> Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Nfty](https://ntfy.sh/).
|
||||
Tras la instalación, selecciona la aplicación que deseas usar en la configuración.
|
||||
</string>
|
||||
<string name="payment_required_title">Mensaje de %1$s</string>
|
||||
<string name="thread_title">Hilo</string>
|
||||
<string name="send_the_seller_a_message">Envía un mensaje al vendedor</string>
|
||||
<string name="hi_seller_is_this_still_available">Hola, %1$s, ¿esto todavía está disponible?</string>
|
||||
<string name="hi_there_is_this_still_available">Hola, ¿esto todavía está disponible?</string>
|
||||
<string name="classifieds">Vender un artículo</string>
|
||||
<string name="classifieds_title">Título</string>
|
||||
<string name="classifieds_title_placeholder">iPhone 13</string>
|
||||
<string name="classifieds_condition">Estado</string>
|
||||
<string name="classifieds_category">Categoría</string>
|
||||
<string name="classifieds_price">Precio (en sats)</string>
|
||||
<string name="classifieds_price_placeholder">1000</string>
|
||||
<string name="classifieds_location">Ubicación</string>
|
||||
<string name="classifieds_location_placeholder">Ciudad, estado, país.</string>
|
||||
<string name="classifieds_condition_new">Nuevo</string>
|
||||
<string name="classifieds_condition_new_explainer">Producto totalmente nuevo, en la caja original.</string>
|
||||
<string name="classifieds_condition_like_new">Como nuevo</string>
|
||||
<string name="classifieds_condition_like_new_explainer">Usado, pero sin señales de uso.</string>
|
||||
<string name="classifieds_condition_good">Bueno</string>
|
||||
<string name="classifieds_condition_good_explainer">Tiene algunas marcas de uso superficiales.</string>
|
||||
<string name="classifieds_condition_fair">Aceptable</string>
|
||||
<string name="classifieds_condition_fair_explainer">Todavía está en forma aceptable y funcional.</string>
|
||||
<string name="classifieds_category_clothing">Ropa</string>
|
||||
<string name="classifieds_category_accessories">Accesorios</string>
|
||||
<string name="classifieds_category_electronics">Artículos electrónicos</string>
|
||||
<string name="classifieds_category_furniture">Muebles</string>
|
||||
<string name="classifieds_category_collectibles">Objetos de colección</string>
|
||||
<string name="classifieds_category_books">Libros</string>
|
||||
<string name="classifieds_category_pets">Mascotas</string>
|
||||
<string name="classifieds_category_sports">Deportes</string>
|
||||
<string name="classifieds_category_fitness">Acondicionamiento físico</string>
|
||||
<string name="classifieds_category_art">Arte</string>
|
||||
<string name="classifieds_category_crafts">Artesanías</string>
|
||||
<string name="classifieds_category_home">Hogar</string>
|
||||
<string name="classifieds_category_office">Oficina</string>
|
||||
<string name="classifieds_category_food">Comida</string>
|
||||
<string name="classifieds_category_misc">Varios</string>
|
||||
<string name="classifieds_category_other">Otros</string>
|
||||
</resources>
|
||||
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">هیچ ردی در نوستر نمی ماند، فقط در شبکه لایتنینگ انجام می شود</string>
|
||||
<string name="file_server">سرور فایل</string>
|
||||
<string name="zap_forward_lnAddress">آدرس لایتنینگ یا @User</string>
|
||||
<string name="upload_server_imgur">imgur.com - معتمد</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur می تواند فایل را بهینه کند</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - معتمد</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg می تواند فایل را بهینه کند</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - معتمد</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build می تواند فایل را بهینه کند</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - معتمد</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev می تواند فایل را بهینه کند</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - معتمد</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me می تواند فایل را اصلاح کند</string>
|
||||
<string name="upload_server_imgur_nip94">قابل بررسی Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">بررسی کنید که Imgur فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند. </string>
|
||||
<string name="upload_server_nostrimg_nip94">قابل بررسی NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">بررسی کنید که NostrImg فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند.</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build (NIP-94) قابل تایید</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">بررسی کنید که Nostr.build فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند.</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">قابل بررسی Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">بررسی کنید که Nostrfiles.dev فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند.</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me (NIP-94) قابل تایید</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">بررسی می کند آیا Nostrcheck.me فایل را اصلاح کرده است. NIP جدید: سایر کلاینت ها ممکن است این را نبینند</string>
|
||||
<string name="upload_server_relays_nip95">رله های شما (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">فایل ها در رله های شما میزبانی می شوند. NIPجدید: بررسی کنید آیا پشتیبانی می کنند یا خیر </string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot تنظیمات</string>
|
||||
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Ei jälkiä Nostrissa, vain Lightningissa</string>
|
||||
<string name="file_server">Tiedostopalvelin</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress tai @Käyttäjä</string>
|
||||
<string name="upload_server_imgur">imgur.com - luotettava</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur voi muokata tiedostoa</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - luotettava</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg voi muokata tiedostoa</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - luotettava</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build voi muokata tiedostoa</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - luotettava</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev voi muokata tiedostoa</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - luotettava</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">Nostrcheck.me voi muokata tiedostoa</string>
|
||||
<string name="upload_server_imgur_nip94">Todennettava Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Tarkistaa, onko Imgur muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_nostrimg_nip94">Todennettava NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Tarkistaa, onko NostrImg muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Todennettava Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Tarkistaa, onko Nostr.build muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Todennettava Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Tarkistaa, onko Nostrfiles.dev muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Todennettava Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Tarkistaa, onko Nostrcheck.me muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_relays_nip95">Omat releet (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Tiedostot ovat isännöityinä omilla releilläsi. Uusi NIP: tarkista, tukevatko ne sitä</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot-asetus</string>
|
||||
@@ -599,4 +579,11 @@
|
||||
<string name="classifieds_category_food">Ruoka</string>
|
||||
<string name="classifieds_category_misc">Sekalaiset</string>
|
||||
<string name="classifieds_category_other">Muu</string>
|
||||
<string name="failed_to_upload_media_no_details">Median lataus epäonnistui</string>
|
||||
<string name="could_not_open_the_compressed_file">Pakattua tiedostoa ei voitu avata</string>
|
||||
<string name="error_when_compressing_media">Virhe mediaa pakkattaessa: %1$s</string>
|
||||
<string name="failed_to_upload_media">Lähetysvirhe: %1$s</string>
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Palvelin ei antanut URL-osoitetta latauksen jälkeen</string>
|
||||
<string name="could_not_download_from_the_server">Mediaa ei voitu ladata palvelimelta</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Paikallista tiedostoa ei voitu valmistella ladattavaksi: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Aucune trace sur Nostr, seulement sur le Lightning</string>
|
||||
<string name="file_server">Serveur fichier</string>
|
||||
<string name="zap_forward_lnAddress">Adresse LN ou @Utilisateur</string>
|
||||
<string name="upload_server_imgur">imgur.com - de confiance</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur peut modifier le fichier</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - de confiance</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg peut modifier le fichier</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - de confiance</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build peut modifier le fichier</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - de confiance</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev peut modifier le fichier</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - de confiance</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me peut modifier le fichier</string>
|
||||
<string name="upload_server_imgur_nip94">Imgur vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Vérifie si Imgur a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
<string name="upload_server_nostrimg_nip94">NostrImg vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Vérifie si NostrImg a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Vérifie si Nostr.build a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Vérifie si Nostrfiles.dev a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Vérifie si Nostrcheck.me a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
<string name="upload_server_relays_nip95">Vos relais (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Les fichiers sont hébergés par vos relais. Nouveau NIP: vérifiez s\'ils sont supportés</string>
|
||||
<string name="connect_via_tor_short">Configuration Tor/Orbot</string>
|
||||
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Nostr-ban nyoma sincs, csak a Lightning-ben</string>
|
||||
<string name="file_server">Fájl Szerver</string>
|
||||
<string name="zap_forward_lnAddress">LnCím vagy @Felhasználó</string>
|
||||
<string name="upload_server_imgur">imgur.com - kevésbé biztonságos</string>
|
||||
<string name="upload_server_imgur_explainer">Feltöltés az Imgur-ba. Az Imgur a képet/videót bármikor megváltoztathatja</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - kevésbé biztonságos</string>
|
||||
<string name="upload_server_nostrimg_explainer">Feltöltés a NostrImg-be. A NostrImg a képet bármikor megváltoztathatja</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - megbízható</string>
|
||||
<string name="upload_server_nostrbuild_explainer">A Nostr.build a fájlt módosíthatja</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - megbízható</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">A Nostrfiles.dev a fájlt módosíthatja</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - megbízható</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">a Nostrcheck.me a fájlt módosíthatja</string>
|
||||
<string name="upload_server_imgur_nip94">Ellenőrizhető Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Megvéd attól, hogy az Imgur a képet/videót a közzététel után megváltoztassa</string>
|
||||
<string name="upload_server_nostrimg_nip94">Ellenőrizhető NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Megvéd attól, hogy az NostrImg a képet a közzététel után megváltoztassa</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Ellenőrizhető Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Ellenőrzi, hogy a Nostr.build a fájlt módosította-e. Új NIP: előfordulhat, hogy más kliensek nem jelenítik meg</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Ellenőrizhető Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Ellenőrzi, hogy a Nostrfiles.dev a fájlt módosította-e. Új NIP: előfordulhat, hogy más kliensek nem jelenítik meg</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Ellenőrizhető Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Ellenőrzi, hogy a Nostrcheck.me a fájlt módosította-e. Új NIP: előfordulhat, hogy más kliensek nem jelenítik meg</string>
|
||||
<string name="upload_server_relays_nip95">Saját csomópontjaid (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">A fájlokat a csomópontokra töltik fel és ott tárolják. Rögzített URL-től mentesek (harmadik féltől való függőség). Győződj meg róla, hogy legalább egy NIP-95 csomópont a csomópontlistában szerepel</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot beállítása</string>
|
||||
@@ -600,4 +580,11 @@
|
||||
<string name="classifieds_category_food">Étel</string>
|
||||
<string name="classifieds_category_misc">Vegyes</string>
|
||||
<string name="classifieds_category_other">Egyéb</string>
|
||||
<string name="failed_to_upload_media_no_details">Nem sikerült a médiát feltölteni</string>
|
||||
<string name="could_not_open_the_compressed_file">Nem sikerült a tömörített fájlt megnyitni</string>
|
||||
<string name="error_when_compressing_media">Hiba a média tömörítésekor: %1$s</string>
|
||||
<string name="failed_to_upload_media">Feltöltési hiba: %1$s</string>
|
||||
<string name="server_did_not_provide_a_url_after_uploading">A szerver a feltöltés után nem adott URL-t</string>
|
||||
<string name="could_not_download_from_the_server">Nem sikerült a szerverről a feltöltött médiát letölteni</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Nem sikerült előkészíteni a helyi fájlt a feltöltésre: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Tidak ada jejak di Nostr, hanya di Lightning</string>
|
||||
<string name="file_server">File Server</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress atau @Pengguna</string>
|
||||
<string name="upload_server_imgur">imgur.com - Terpercaya</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur bisa memodifikasi file</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - Terpercaya</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg bisa memodifikasi file</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - Terpercaya</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build bisa memodifikasi file</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - Terpercaya</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev bisa memodifikasi file</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - Terpercaya</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me bisa memodifikasi file</string>
|
||||
<string name="upload_server_imgur_nip94">Imgur yang Dapat Diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Memeriksa apakah Imgur memodifikasi file. NIP Baru: klien lain mungkin tidak melihatnya</string>
|
||||
<string name="upload_server_nostrimg_nip94">NostrImg yang dapat diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Memeriksa apakah NostrImg memodifikasi file. NIP Baru: klien lain mungkin tidak melihatnya</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build yang dapat diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Memeriksa apakah Nostr.build memodifikasi file. NIP Baru: klien lain mungkin tidak melihatnya</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev yang dapat diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Memeriksa apakah Nostrfiles.dev memodifikasi file. NIP Baru: klien lain mungkin tidak melihatnya</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me yang dapat diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Memeriksa apakah Nostrcheck.me memodifikasi file. NIP Baru: klien lain mungkin tidak melihatnya</string>
|
||||
<string name="upload_server_relays_nip95">Relay-relay anda (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">File dihosting oleh relay Anda. NIP Baru: periksa apakah relay mendukung</string>
|
||||
<string name="connect_via_tor_short">Pengaturan Tor/Orbot</string>
|
||||
|
||||
@@ -294,26 +294,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Tidak ada jejak di Nostr, hanya di Lightning Network</string>
|
||||
<string name="file_server">Server Berkas</string>
|
||||
<string name="zap_forward_lnAddress">Alamat LN atau @Pengguna</string>
|
||||
<string name="upload_server_imgur">imgur.com - terpercaya</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur dapat memodifikasi berkas</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - terpercaya</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg dapat memodifikasi berkas</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - terpercaya</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build dapat memodifikasi berkas</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - terpercaya</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev dapat memodifikasi berkas</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - terpercaya</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me dapat memodifikasi berkas</string>
|
||||
<string name="upload_server_imgur_nip94">Imgur yg dapat diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Periksa jika Imgur mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung</string>
|
||||
<string name="upload_server_nostrimg_nip94">NostrImg yg dapat diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Periksa jika NostrImg mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build yg dapat diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Periksa jika Nostr.build mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev yg dapat diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Periksa jika Nostrfiles.dev mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me yg dapat diverifikasi (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Periksa jika Nostrcheck.me mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung</string>
|
||||
<string name="upload_server_relays_nip95">Relai anda (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Berkas disimpan di relai anda. NIP baru: periksa apakah relai mendukung fitur</string>
|
||||
<string name="connect_via_tor_short">Pengaturan Tor/Orbot</string>
|
||||
|
||||
@@ -299,26 +299,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Nessuna traccia in Nostr, solo in Lightning</string>
|
||||
<string name="file_server">File Server</string>
|
||||
<string name="zap_forward_lnAddress">Indirizzo LN o @User</string>
|
||||
<string name="upload_server_imgur">imgur.com - fidato</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur può modificare il file</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - fidato</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg può modificare il file</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - fidato</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build può modificare il file</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - fidato</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev può modificare il file</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - fidato</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me può modificare il file</string>
|
||||
<string name="upload_server_imgur_nip94">Imgur verificabile (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Controlla se Imgur ha modificato il file. Nuovo NIP: altri client potrebbero non vederlo</string>
|
||||
<string name="upload_server_nostrimg_nip94">NostrImg verificabile (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Controlla se NostrImg ha modificato il file. Nuovo NIP: altri client potrebbero non vederlo</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build verificabile (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Controlla se Nostr.build ha modificato il file. Nuovo NIP: altri client potrebbero non vederlo</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev verificabile (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Controlla se Nostrfiles.dev ha modificato il file. Nuovo NIP: altri client potrebbero non vederlo</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me verificabile (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Controlla se Nostrcheck.me ha modificato il file. Nuovo NIP: altri client potrebbero non vederlo</string>
|
||||
<string name="upload_server_relays_nip95">I tuoi relè (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">I file sono ospitati dai tuoi relè. Nuovo NIP: controlla se lo supportano</string>
|
||||
<string name="connect_via_tor_short">Configurazione Tor/Orbot</string>
|
||||
|
||||
@@ -293,26 +293,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Nostrを経由せずにLightning支払いを行います</string>
|
||||
<string name="file_server">ファイルサーバ</string>
|
||||
<string name="zap_forward_lnAddress">LNアドレス または @ユーザ名</string>
|
||||
<string name="upload_server_imgur">imgur.com - 信頼済み</string>
|
||||
<string name="upload_server_imgur_explainer">Imgurはファイルの変更が可能です</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - 信頼済み</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImgはファイルの変更が可能です</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - 信頼済み</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.buildはファイルの変更が可能です</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - 信頼済み</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.devはファイルの変更が可能です</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - 信頼済み</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.meはファイルの変更が可能です</string>
|
||||
<string name="upload_server_imgur_nip94">検証可能な Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Imgurによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
<string name="upload_server_nostrimg_nip94">検証可能な NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">NostrImgによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
<string name="upload_server_nostrbuild_nip94">検証可能な Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Nostr.buildによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">検証可能な Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Nostrfiles.devによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">検証可能な Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Nostrcheck.meによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
<string name="upload_server_relays_nip95">あなたのリレー (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">ファイルはあなたのリレーによってホストされます。新規NIP: リレーがサポートしているか確認してください</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbotを設定</string>
|
||||
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Geen spoor binnen Nostr, alleen in Lightning</string>
|
||||
<string name="file_server">File server</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress of @User</string>
|
||||
<string name="upload_server_imgur">imgur.com - vertrouwd</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur kan het bestand aanpassen</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - vertrouwd</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg kan het bestand aanpassen</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - vertrouwd</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build kan mogelijk het bestand aanpassen</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - vertrouwd</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev kan het bestand aanpassen</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - vertrouwd</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me kan het bestand aanpassen</string>
|
||||
<string name="upload_server_imgur_nip94">Verifieerbare Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Checkt of Imgur het bestand heeft aangepast. Dit is een nieuwe NIP: andere clients zien het misschien niet.</string>
|
||||
<string name="upload_server_nostrimg_nip94">Verifieerbare NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Checkt of NostrImg het bestand heeft aangepast. Dit is een nieuwe NIP: andere clients zien het misschien niet.</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Verifieerbare Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Checkt of Nostr.build het bestand heeft aangepast. Dit is een nieuwe NIP: andere clients zien het misschien niet.</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Verifieerbare Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Checkt of Nostrfiles.dev het bestand heeft aangepast. Dit is een nieuwe NIP: andere clients zien het misschien niet.</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Verifieerbare nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Checkt of nostrcheck.me het bestand heeft aangepast. Dit is een nieuwe NIP: andere clients zien het misschien niet.</string>
|
||||
<string name="upload_server_relays_nip95">Uw relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Bestanden worden geüpload naar en gehost door relays. Ze zijn vrij van een vaste url (afhankelijkheid van derden). Zorg ervoor dat u een NIP-95 relay in uw lijst met relays hebt.</string>
|
||||
<string name="connect_via_tor_short">Orbot/Tor setup</string>
|
||||
@@ -600,4 +580,11 @@
|
||||
<string name="classifieds_category_food">Eten</string>
|
||||
<string name="classifieds_category_misc">Overige</string>
|
||||
<string name="classifieds_category_other">Anders</string>
|
||||
<string name="failed_to_upload_media_no_details">Uploaden van media mislukt</string>
|
||||
<string name="could_not_open_the_compressed_file">Kan het gecomprimeerde bestand niet openen</string>
|
||||
<string name="error_when_compressing_media">Fout bij het comprimeren van media: %1$s</string>
|
||||
<string name="failed_to_upload_media">Upload fout: %1$s</string>
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Server heeft geen URL opgegeven na uploaden</string>
|
||||
<string name="could_not_download_from_the_server">Kan de geüploade media niet downloaden van de server</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Kon lokaal bestand niet voorbereiden voor upload: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -307,26 +307,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Nenhum traço no Nostr, apenas na Lightning</string>
|
||||
<string name="file_server">Servidor de arquivos</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress ou @Usuário</string>
|
||||
<string name="upload_server_imgur">imgur.com - confiável</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur pode modificar o arquivo</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - confiável</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg pode modificar o arquivo</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - confiável</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build pode modificar o arquivo</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - confiável</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev pode modificar o arquivo</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - confiável</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me pode modificar o arquivo</string>
|
||||
<string name="upload_server_imgur_nip94">Imgur verificável (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Verifica se o Imgur modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_nostrimg_nip94">NostrImg verificável (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Verifica se NostrImg modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build verificável (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Verifica se Nostr.build modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev verificável (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Verifica se Nostrfiles.dev modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Verificável Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Verifica se Nostrcheck.me modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_relays_nip95">Seus relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Os arquivos são hospedados por seus relays. Novo NIP: verifique se eles suportam</string>
|
||||
<string name="connect_via_tor_short">Configuração do Tor/Orbot</string>
|
||||
|
||||
@@ -291,22 +291,6 @@
|
||||
<string name="zap_type_nonzap_explainer">Без следа в Nostr, обычный Lightning платеж</string>
|
||||
<string name="file_server">Сервер для загрузки</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress или @User</string>
|
||||
<string name="upload_server_imgur">imgur.com – доверенный</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur может изменять файл</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com – доверенный</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg может изменять файл</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build – доверенный</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build может изменять файл</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev – доверенный</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev может изменять файл</string>
|
||||
<string name="upload_server_imgur_nip94">Верифицируемый Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Проверка на изменение файла Imgur. Новый NIP: может не работать в других клиентах</string>
|
||||
<string name="upload_server_nostrimg_nip94">Верифицируемый NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Проверка на изменение файла NostrImg. Новый NIP: может не работать в других клиентах</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Верифицируемый Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Проверка на изменение файла Nostr.build. Новый NIP: может не работать в других клиентах</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Верифицируемый Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Проверка на изменение файла nostrfiles.dev. Новый NIP: может не работать в других клиентах</string>
|
||||
<string name="upload_server_relays_nip95">Ваши релеи (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Файл хранится на ваших релеях. Новый NIP: может не работать в других клиентах</string>
|
||||
<string name="follow_list_kind3follows">Все подписки</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user