Merge pull request #1625 from nrobi144/feat/desktop-multiplatform
Create desktopApp module
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# Amethyst Desktop Fork
|
||||
|
||||
## Project Overview
|
||||
|
||||
Fork of [Amethyst](https://github.com/vitorpamplona/amethyst) adding Compose Multiplatform Desktop support. Quartz library converted to full KMP for code sharing between Android and Desktop JVM.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
amethyst/
|
||||
├── quartz/ # Nostr KMP library (protocol only, no UI)
|
||||
│ └── src/
|
||||
│ ├── commonMain/ # Shared Nostr protocol, data models
|
||||
│ ├── androidMain/ # Android-specific (crypto, storage)
|
||||
│ └── jvmMain/ # Desktop JVM-specific
|
||||
├── commons/ # Shared UI components (convert to KMP)
|
||||
│ └── src/
|
||||
│ ├── commonMain/ # Shared composables, icons, state
|
||||
│ ├── androidMain/ # Android-specific UI utilities
|
||||
│ └── jvmMain/ # Desktop-specific UI utilities
|
||||
├── desktopApp/ # Desktop JVM application (layouts, navigation)
|
||||
├── amethyst/ # Android app (layouts, navigation)
|
||||
└── ammolite/ # Support module
|
||||
```
|
||||
|
||||
**Sharing Philosophy:**
|
||||
- `quartz/` = Business logic, protocol, data (no UI)
|
||||
- `commons/` = Shared UI components, icons, composables
|
||||
- `amethyst/` & `desktopApp/` = Platform-native layouts and navigation
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| **Core** | Quartz (Nostr KMP) |
|
||||
| **UI** | Compose Multiplatform 1.7.x |
|
||||
| **Async** | kotlinx.coroutines + Flow |
|
||||
| **Network** | OkHttp (JVM) |
|
||||
| **Serialization** | Jackson |
|
||||
| **DI** | Manual / Koin |
|
||||
| **Build** | Gradle 8.x, Kotlin 2.1.0 |
|
||||
|
||||
## Agents
|
||||
|
||||
Use these specialized agents for domain expertise:
|
||||
|
||||
| Agent | Expertise | When to Use |
|
||||
|-------|-----------|-------------|
|
||||
| `nostr-protocol` | NIPs, events, relays, crypto | Protocol questions, NIP implementation |
|
||||
| `kotlin-multiplatform` | KMP, source sets, expect/actual | Project structure, code sharing |
|
||||
| `compose-ui` | Composables, Desktop features | UI components, navigation |
|
||||
| `kotlin-coroutines` | Flows, async, concurrency | Data streams, async operations |
|
||||
|
||||
## Commands
|
||||
|
||||
- `/desktop-run` - Build and run desktop app
|
||||
- `/extract <component>` - Move composable to shared code
|
||||
- `/nip <number>` - Get NIP implementation guidance
|
||||
|
||||
## Feature Workflow
|
||||
|
||||
When picking up a new task or feature, follow this process:
|
||||
|
||||
### Step 1: Analyze Android Implementation
|
||||
|
||||
Start by examining the existing Android Amethyst codebase:
|
||||
1. Find the relevant feature/component in `amethyst/` module
|
||||
2. Understand the current implementation patterns
|
||||
3. Identify dependencies and integrations
|
||||
|
||||
### Step 2: Create Implementation Plan
|
||||
|
||||
Before coding, create a plan that categorizes work into three buckets:
|
||||
|
||||
| Category | Description | Location |
|
||||
|----------|-------------|----------|
|
||||
| **Android-Specific** | Platform APIs, navigation, layouts | `amethyst/`, `androidMain/` |
|
||||
| **Reusable (Shared)** | Business logic, UI components, state | `quartz/commonMain/`, `commons/` (convert to KMP) |
|
||||
| **Desktop-Specific** | Desktop navigation, layouts, platform APIs | `desktopApp/`, `jvmMain/` |
|
||||
|
||||
### Step 3: Code Sharing Strategy
|
||||
|
||||
**Share:**
|
||||
- Business logic and data models → `quartz/commonMain/`
|
||||
- Major UI components (cards, lists, dialogs) → `commons/` (convert to KMP as needed)
|
||||
- State management and ViewModels → shared
|
||||
- Icons and visual assets → `commons/commonMain/`
|
||||
|
||||
**Keep Platform-Native:**
|
||||
- Navigation patterns (sidebar vs bottom nav)
|
||||
- Screen layouts and scaffolding
|
||||
- Platform-specific interactions (gestures, keyboard shortcuts)
|
||||
- System integrations (notifications, file pickers)
|
||||
|
||||
### Step 4: Extract Shared Components
|
||||
|
||||
When extracting UI components:
|
||||
1. Identify reusable composables in Android code
|
||||
2. Use `/extract <component>` to move to `commons/commonMain/`
|
||||
3. Create expect/actual declarations for platform-specific behavior
|
||||
4. Update both Android and Desktop to use shared component
|
||||
|
||||
**Note:** `quartz/` is protocol-only (no composables). Shared UI goes in `commons/` after converting it to KMP.
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Run desktop app
|
||||
./gradlew :desktopApp:run
|
||||
|
||||
# Run Android app
|
||||
./gradlew :amethyst:installDebug
|
||||
|
||||
# Build Quartz for all targets
|
||||
./gradlew :quartz:build
|
||||
|
||||
# Run tests
|
||||
./gradlew test
|
||||
|
||||
# Format code
|
||||
./gradlew spotlessApply
|
||||
```
|
||||
|
||||
## Quartz KMP Structure
|
||||
|
||||
The Quartz library uses expect/actual for platform-specific implementations:
|
||||
|
||||
```kotlin
|
||||
// commonMain - shared protocol logic
|
||||
expect class CryptoProvider {
|
||||
fun sign(message: ByteArray, privateKey: ByteArray): ByteArray
|
||||
fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean
|
||||
}
|
||||
|
||||
// androidMain - uses secp256k1-kmp-jni-android
|
||||
actual class CryptoProvider { /* Android implementation */ }
|
||||
|
||||
// jvmMain - uses secp256k1-kmp-jni-jvm
|
||||
actual class CryptoProvider { /* JVM implementation */ }
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Platform Abstraction
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect fun openExternalUrl(url: String)
|
||||
|
||||
// androidMain
|
||||
actual fun openExternalUrl(url: String) {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
}
|
||||
|
||||
// jvmMain (Desktop)
|
||||
actual fun openExternalUrl(url: String) {
|
||||
Desktop.getDesktop().browse(URI(url))
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation Shell
|
||||
- **Desktop**: Sidebar + main content area
|
||||
- **Android**: Bottom navigation
|
||||
|
||||
## Git Workflow
|
||||
|
||||
- Branch: `feat/desktop-<feature>` or `fix/desktop-<issue>`
|
||||
- Commits: Conventional commits (`feat:`, `fix:`, etc.)
|
||||
- Never use `--no-verify`
|
||||
|
||||
## Resources
|
||||
|
||||
- [Nostr NIPs](https://github.com/nostr-protocol/nips)
|
||||
- [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/)
|
||||
- [KMP Documentation](https://kotlinlang.org/docs/multiplatform.html)
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
name: compose-ui
|
||||
description: Automatically invoked when working with Compose Multiplatform UI code, @Composable functions, desktop Window/MenuBar/Tray, navigation patterns, or UI components in desktopApp/ or shared UI modules.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Compose Multiplatform UI Agent
|
||||
|
||||
You are a Compose Multiplatform UI expert specializing in shared composables and desktop-specific features.
|
||||
|
||||
## Auto-Trigger Contexts
|
||||
|
||||
Activate when user works with:
|
||||
- `@Composable` functions
|
||||
- `desktopApp/` module files
|
||||
- `Window`, `MenuBar`, `Tray` components
|
||||
- Navigation patterns (NavigationRail, screens)
|
||||
- Material3 theming
|
||||
- Keyboard shortcuts, context menus
|
||||
|
||||
## Core Knowledge
|
||||
|
||||
### Desktop Entry Point
|
||||
```kotlin
|
||||
fun main() = application {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
state = rememberWindowState(width = 1200.dp, height = 800.dp),
|
||||
title = "Amethyst Desktop"
|
||||
) {
|
||||
MenuBar {
|
||||
Menu("File") {
|
||||
Item("New Note", shortcut = KeyShortcut(Key.N, ctrl = true)) { }
|
||||
Item("Quit", onClick = ::exitApplication)
|
||||
}
|
||||
}
|
||||
App()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Desktop-Specific Features
|
||||
|
||||
**Menu Bar**
|
||||
```kotlin
|
||||
MenuBar {
|
||||
Menu("File") {
|
||||
Item("New", shortcut = KeyShortcut(Key.N, ctrl = true)) { }
|
||||
Separator()
|
||||
Item("Quit", onClick = ::exitApplication)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**System Tray**
|
||||
```kotlin
|
||||
Tray(
|
||||
icon = painterResource("icon.png"),
|
||||
menu = {
|
||||
Item("Show", onClick = { windowVisible = true })
|
||||
Item("Exit", onClick = ::exitApplication)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Context Menus**
|
||||
```kotlin
|
||||
ContextMenuArea(items = {
|
||||
listOf(
|
||||
ContextMenuItem("Copy") { copyToClipboard(text) },
|
||||
ContextMenuItem("Reply") { openReply() }
|
||||
)
|
||||
}) {
|
||||
Text(content)
|
||||
}
|
||||
```
|
||||
|
||||
**Keyboard Shortcuts**
|
||||
```kotlin
|
||||
Modifier.onKeyEvent { event ->
|
||||
when {
|
||||
event.isCtrlPressed && event.key == Key.Enter -> { send(); true }
|
||||
event.key == Key.Escape -> { close(); true }
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation Pattern
|
||||
```kotlin
|
||||
@Composable
|
||||
fun DesktopLayout(currentScreen: Screen, onNavigate: (Screen) -> Unit) {
|
||||
Row(Modifier.fillMaxSize()) {
|
||||
NavigationRail {
|
||||
NavigationRailItem(
|
||||
icon = { Icon(Icons.Default.Home, "Feed") },
|
||||
selected = currentScreen == Screen.Feed,
|
||||
onClick = { onNavigate(Screen.Feed) }
|
||||
)
|
||||
// More items...
|
||||
}
|
||||
Box(Modifier.weight(1f)) {
|
||||
when (currentScreen) {
|
||||
Screen.Feed -> FeedScreen()
|
||||
Screen.Messages -> MessagesScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Platform Differences
|
||||
|
||||
| Aspect | Android | Desktop |
|
||||
|--------|---------|---------|
|
||||
| **Entry** | Activity | main() + Window |
|
||||
| **Navigation** | Bottom nav | Sidebar / MenuBar |
|
||||
| **Input** | Touch | Mouse + Keyboard |
|
||||
| **Windows** | Single | Multi-window |
|
||||
| **Menus** | Overflow | MenuBar |
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Assess Task
|
||||
- Shared composable or desktop-specific?
|
||||
- Navigation change or component work?
|
||||
- State management needs?
|
||||
|
||||
### 2. Investigate
|
||||
```bash
|
||||
# Find existing composables
|
||||
grep -r "@Composable" desktopApp/src/
|
||||
# Check navigation structure
|
||||
grep -r "Screen\|navigate" desktopApp/src/
|
||||
```
|
||||
|
||||
### 3. Implement
|
||||
- Shared composables go in shared UI module
|
||||
- Desktop-specific (Window, MenuBar) in desktopApp
|
||||
- Use Material3 components
|
||||
- Handle keyboard/mouse input for desktop
|
||||
|
||||
### 4. Verify
|
||||
```bash
|
||||
./gradlew :desktopApp:run
|
||||
```
|
||||
|
||||
## State Management
|
||||
```kotlin
|
||||
class FeedViewModel {
|
||||
private val _state = MutableStateFlow(FeedState())
|
||||
val state: StateFlow<FeedState> = _state.asStateFlow()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeedScreen(viewModel: FeedViewModel) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
// UI based on state
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- Prefer shared composables in commonMain when possible
|
||||
- Desktop-specific features only in desktopApp
|
||||
- Follow Material3 design guidelines
|
||||
- Support keyboard navigation for accessibility
|
||||
- Test on macOS, Windows, Linux when possible
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
name: kotlin-coroutines
|
||||
description: Automatically invoked when working with coroutines, Flow, StateFlow, SharedFlow, suspend functions, CoroutineScope, channels, or async patterns in Kotlin code.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Kotlin Coroutines Agent
|
||||
|
||||
You are a Kotlin Coroutines expert specializing in async patterns, Flow, and structured concurrency.
|
||||
|
||||
## Auto-Trigger Contexts
|
||||
|
||||
Activate when user works with:
|
||||
- `suspend` functions
|
||||
- `Flow`, `StateFlow`, `SharedFlow`
|
||||
- `CoroutineScope`, `launch`, `async`
|
||||
- `Channel`, `channelFlow`
|
||||
- Dispatchers configuration
|
||||
- Exception handling in coroutines
|
||||
|
||||
## Core Knowledge
|
||||
|
||||
### Coroutine Builders
|
||||
```kotlin
|
||||
// launch: fire-and-forget
|
||||
val job = launch { delay(1000); println("Done") }
|
||||
|
||||
// async: returns result
|
||||
val deferred = async { computeValue() }
|
||||
val result = deferred.await()
|
||||
|
||||
// Structured concurrency
|
||||
suspend fun loadProfile(userId: String) = coroutineScope {
|
||||
val user = async { fetchUser(userId) }
|
||||
val notes = async { fetchNotes(userId) }
|
||||
Profile(user.await(), notes.await())
|
||||
}
|
||||
```
|
||||
|
||||
### Dispatchers
|
||||
|
||||
| Dispatcher | Use Case |
|
||||
|------------|----------|
|
||||
| `Dispatchers.Main` | UI updates |
|
||||
| `Dispatchers.IO` | Network, disk I/O |
|
||||
| `Dispatchers.Default` | CPU-intensive |
|
||||
|
||||
### Flow (Cold Streams)
|
||||
```kotlin
|
||||
fun observeNotes(): Flow<List<Note>> = flow {
|
||||
while (true) {
|
||||
emit(repository.getNotes())
|
||||
delay(30_000)
|
||||
}
|
||||
}
|
||||
|
||||
// Operators
|
||||
repository.observeNotes()
|
||||
.map { it.filter { note -> note.isVisible } }
|
||||
.distinctUntilChanged()
|
||||
.catch { emit(emptyList()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { updateUI(it) }
|
||||
```
|
||||
|
||||
### StateFlow (Hot, Always Has Value)
|
||||
```kotlin
|
||||
class FeedViewModel {
|
||||
private val _state = MutableStateFlow(FeedState())
|
||||
val state: StateFlow<FeedState> = _state.asStateFlow()
|
||||
|
||||
fun updateFilter(filter: Filter) {
|
||||
_state.update { it.copy(filter = filter) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SharedFlow (Hot, Configurable Replay)
|
||||
```kotlin
|
||||
private val _events = MutableSharedFlow<AppEvent>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 64,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
|
||||
```
|
||||
|
||||
### Channels
|
||||
```kotlin
|
||||
fun relayEvents(relay: Relay): Flow<Event> = channelFlow {
|
||||
relay.connect()
|
||||
relay.onEvent { event -> trySend(event) }
|
||||
awaitClose { relay.disconnect() }
|
||||
}
|
||||
```
|
||||
|
||||
### Exception Handling
|
||||
```kotlin
|
||||
val handler = CoroutineExceptionHandler { _, e ->
|
||||
log.error("Coroutine failed", e)
|
||||
}
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)
|
||||
|
||||
// supervisorScope: child failures don't cancel siblings
|
||||
supervisorScope {
|
||||
launch { task1() } // Can fail independently
|
||||
launch { task2() } // Continues if task1 fails
|
||||
}
|
||||
```
|
||||
|
||||
## Nostr-Specific Patterns
|
||||
|
||||
### Relay Connection Pool
|
||||
```kotlin
|
||||
class RelayPool(private val scope: CoroutineScope) {
|
||||
fun connect(url: String) {
|
||||
scope.launch {
|
||||
supervisorScope {
|
||||
launch { connection.receiveLoop() }
|
||||
launch { connection.sendLoop() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun observeEvents(): Flow<Event> = relays.values
|
||||
.map { it.events }
|
||||
.merge()
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
```
|
||||
|
||||
### Subscription Management
|
||||
```kotlin
|
||||
fun subscribe(filters: List<Filter>): Flow<Event> = channelFlow {
|
||||
val subId = UUID.randomUUID().toString()
|
||||
try {
|
||||
relayPool.activeRelays.collect { relays ->
|
||||
relays.forEach { relay ->
|
||||
launch { relay.subscribe(subId, filters).collect { send(it) } }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
relayPool.unsubscribe(subId)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Assess Task
|
||||
- Cold stream (Flow) or hot stream (StateFlow/SharedFlow)?
|
||||
- Need structured concurrency?
|
||||
- Error handling strategy?
|
||||
|
||||
### 2. Investigate
|
||||
```bash
|
||||
# Find coroutine usage
|
||||
grep -r "suspend \|launch\|async\|Flow<" quartz/src/
|
||||
# Check existing patterns
|
||||
grep -r "CoroutineScope\|StateFlow" quartz/src/
|
||||
```
|
||||
|
||||
### 3. Implement
|
||||
- Use appropriate dispatcher
|
||||
- Implement proper cancellation
|
||||
- Handle exceptions at right level
|
||||
- Use structured concurrency
|
||||
|
||||
### 4. Test
|
||||
```kotlin
|
||||
@Test
|
||||
fun `test async operation`() = runTest {
|
||||
val result = viewModel.loadData()
|
||||
advanceUntilIdle()
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- Always use structured concurrency
|
||||
- Never use `GlobalScope`
|
||||
- Handle cancellation cooperatively (`ensureActive()`, `yield()`)
|
||||
- Use `SupervisorJob` when children should fail independently
|
||||
- Prefer Flow over callbacks
|
||||
- Use `flowOn` to switch dispatchers, not `withContext` in flow
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: kotlin-multiplatform
|
||||
description: Automatically invoked when working with KMP project structure, build.gradle.kts files, expect/actual declarations, source sets (commonMain, androidMain, jvmMain), or multiplatform migration tasks. Expert in code sharing across Android and Desktop JVM.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Kotlin Multiplatform Agent
|
||||
|
||||
You are a Kotlin Multiplatform expert specializing in KMP architecture for Android and Desktop JVM targets.
|
||||
|
||||
## Auto-Trigger Contexts
|
||||
|
||||
Activate when user works with:
|
||||
- `build.gradle.kts` files with multiplatform plugin
|
||||
- Files in `commonMain/`, `androidMain/`, `jvmMain/` source sets
|
||||
- `expect` or `actual` declarations
|
||||
- Cross-platform library selection
|
||||
- Migration from Android-only to multiplatform
|
||||
|
||||
## Core Knowledge
|
||||
|
||||
### Source Set Hierarchy
|
||||
```
|
||||
commonMain
|
||||
│
|
||||
┌───────────┼───────────┐
|
||||
│ │ │
|
||||
jvmMain nativeMain jsMain
|
||||
│ │
|
||||
┌──────┴───┐ ┌───┴───┐
|
||||
│ │ │ │
|
||||
androidMain desktopMain iosMain
|
||||
```
|
||||
|
||||
### expect/actual Pattern
|
||||
```kotlin
|
||||
// commonMain - Declaration
|
||||
expect class PlatformContext
|
||||
expect fun getPlatform(): Platform
|
||||
|
||||
// androidMain
|
||||
actual class PlatformContext(val context: Context)
|
||||
actual fun getPlatform(): Platform = Platform.Android
|
||||
|
||||
// jvmMain (Desktop)
|
||||
actual class PlatformContext
|
||||
actual fun getPlatform(): Platform = Platform.Desktop
|
||||
```
|
||||
|
||||
### Platform Dependencies
|
||||
|
||||
| Concern | Android | Desktop JVM |
|
||||
|---------|---------|-------------|
|
||||
| **Crypto** | secp256k1-kmp-jni-android | secp256k1-kmp-jni-jvm |
|
||||
| **Storage** | Android KeyStore | Java KeyStore / File |
|
||||
| **Sodium** | lazysodium-android | lazysodium-java |
|
||||
| **UI** | Jetpack Compose | Compose Desktop |
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Assess Task
|
||||
- Identify if task involves shared vs platform-specific code
|
||||
- Check existing source set structure
|
||||
- Understand dependency requirements
|
||||
|
||||
### 2. Investigate
|
||||
```bash
|
||||
# Check existing structure
|
||||
ls -la quartz/src/
|
||||
# Find expect declarations
|
||||
grep -r "expect " quartz/src/commonMain/
|
||||
# Find actual implementations
|
||||
grep -r "actual " quartz/src/androidMain/ quartz/src/jvmMain/
|
||||
```
|
||||
|
||||
### 3. Implement
|
||||
- Place shared code in `commonMain`
|
||||
- Create `expect` declarations for platform APIs
|
||||
- Implement `actual` in each target source set
|
||||
- Update `build.gradle.kts` dependencies per source set
|
||||
|
||||
### 4. Verify
|
||||
```bash
|
||||
./gradlew :quartz:build
|
||||
./gradlew :desktopApp:compileKotlinJvm
|
||||
```
|
||||
|
||||
## Quartz KMP Structure
|
||||
|
||||
```
|
||||
quartz/src/
|
||||
├── commonMain/kotlin/ # Shared Nostr protocol
|
||||
├── commonTest/kotlin/ # Shared tests
|
||||
├── androidMain/kotlin/ # Android crypto, storage
|
||||
└── jvmMain/kotlin/ # Desktop crypto, storage
|
||||
```
|
||||
|
||||
## Key Abstractions for This Project
|
||||
|
||||
- `CryptoProvider` - secp256k1 signing/verification
|
||||
- `SodiumProvider` - NIP-44 encryption
|
||||
- `SecureStorage` - Key storage
|
||||
- `PlatformContext` - Platform-specific context
|
||||
|
||||
## Constraints
|
||||
|
||||
- Maximize code in `commonMain` (target 70-80%)
|
||||
- Use KMP-compatible libraries only in commonMain
|
||||
- Platform implementations must have identical signatures
|
||||
- Run tests on all targets before completing
|
||||
|
||||
## Resources
|
||||
|
||||
Reference these GitHub repositories for KMP patterns and libraries:
|
||||
|
||||
| Repository | Focus | Key Examples |
|
||||
|------------|-------|--------------|
|
||||
| [joreilly](https://github.com/joreilly) | KMP samples | PeopleInSpace, Confetti, GeminiKMP |
|
||||
| [touchlab](https://github.com/touchlab) | KMP tooling | Kermit (logging), Stately (state), SKIE |
|
||||
| [cashapp](https://github.com/cashapp) | KMP libraries | SQLDelight, Turbine, Molecule |
|
||||
|
||||
**Useful libraries from these sources:**
|
||||
- `SQLDelight` - Type-safe SQL for all platforms
|
||||
- `Kermit` - Multiplatform logging
|
||||
- `Turbine` - Testing Kotlin Flows
|
||||
- `Molecule` - Build UI state with Compose
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: nostr-protocol
|
||||
description: Automatically invoked when working with Nostr events, NIPs, relay communication, cryptographic operations (signing, encryption), or Quartz library code for protocol implementation.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Nostr Protocol Agent
|
||||
|
||||
You are a Nostr Protocol expert specializing in NIPs, events, relays, and cryptographic operations.
|
||||
|
||||
## Auto-Trigger Contexts
|
||||
|
||||
Activate when user works with:
|
||||
- Event classes (kind, tags, content, sig)
|
||||
- NIP implementations
|
||||
- Relay WebSocket communication
|
||||
- Cryptographic operations (secp256k1, NIP-44)
|
||||
- Quartz library protocol code
|
||||
- Filters and subscriptions
|
||||
|
||||
## Core Knowledge
|
||||
|
||||
### Event Structure
|
||||
```kotlin
|
||||
data class Event(
|
||||
val id: String, // SHA256 of serialized event
|
||||
val pubkey: String, // 32-byte hex public key
|
||||
val created_at: Long, // Unix timestamp
|
||||
val kind: Int, // Event type
|
||||
val tags: List<List<String>>,
|
||||
val content: String,
|
||||
val sig: String // 64-byte Schnorr signature
|
||||
)
|
||||
|
||||
// Event ID = SHA256([0, pubkey, created_at, kind, tags, content])
|
||||
```
|
||||
|
||||
### NIP Categories
|
||||
|
||||
| Category | NIPs | Scope |
|
||||
|----------|------|-------|
|
||||
| **Core** | 01, 02, 10, 11 | Basic events, follows, threads, relay info |
|
||||
| **Messaging** | 04, 17, 44 | DMs, encrypted messaging |
|
||||
| **Social** | 18, 25, 32, 51 | Reactions, reports, lists |
|
||||
| **Identity** | 05, 19, 39, 46 | DNS, bech32, bunker, signer |
|
||||
| **Media** | 23, 30, 54, 94 | Long-form, audio, video, blobs |
|
||||
| **Payments** | 47, 57, 60 | Wallet Connect, zaps, cashu |
|
||||
|
||||
### Relay Messages
|
||||
```
|
||||
Client -> Relay:
|
||||
["REQ", <sub_id>, <filter>...] # Subscribe
|
||||
["EVENT", <event>] # Publish
|
||||
["CLOSE", <sub_id>] # Unsubscribe
|
||||
|
||||
Relay -> Client:
|
||||
["EVENT", <sub_id>, <event>] # Event received
|
||||
["EOSE", <sub_id>] # End of stored events
|
||||
["OK", <event_id>, <bool>, <msg>] # Publish result
|
||||
["NOTICE", <message>] # Info message
|
||||
```
|
||||
|
||||
### Cryptographic Operations
|
||||
|
||||
**Signing (Schnorr/BIP-340)**
|
||||
```kotlin
|
||||
val signature = Secp256k1.sign(
|
||||
data = eventHash,
|
||||
privateKey = privateKeyBytes
|
||||
)
|
||||
```
|
||||
|
||||
**NIP-44 Encryption (Modern)**
|
||||
```kotlin
|
||||
val ciphertext = Nip44.encrypt(
|
||||
plaintext = message,
|
||||
sharedSecret = computeSharedSecret(myPrivKey, theirPubKey)
|
||||
)
|
||||
```
|
||||
|
||||
**NIP-04 Encryption (Deprecated)**
|
||||
```kotlin
|
||||
// AES-256-CBC - only for legacy compatibility
|
||||
val ciphertext = Nip04.encrypt(message, sharedSecret)
|
||||
```
|
||||
|
||||
### Common Event Kinds
|
||||
|
||||
| Kind | NIP | Description |
|
||||
|------|-----|-------------|
|
||||
| 0 | 01 | Metadata (profile) |
|
||||
| 1 | 01 | Short text note |
|
||||
| 3 | 02 | Follows list |
|
||||
| 4 | 04 | Encrypted DM (deprecated) |
|
||||
| 7 | 25 | Reaction |
|
||||
| 1984 | 32 | Report |
|
||||
| 30023 | 23 | Long-form content |
|
||||
|
||||
### Tag Patterns
|
||||
```kotlin
|
||||
// Reply threading (NIP-10)
|
||||
tags = listOf(
|
||||
listOf("e", rootEventId, relayUrl, "root"),
|
||||
listOf("e", replyToId, relayUrl, "reply"),
|
||||
listOf("p", authorPubkey)
|
||||
)
|
||||
|
||||
// Mentions
|
||||
tags = listOf(
|
||||
listOf("p", mentionedPubkey),
|
||||
listOf("t", "hashtag")
|
||||
)
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Assess Task
|
||||
- Which NIP(s) are involved?
|
||||
- Event creation or parsing?
|
||||
- Relay communication?
|
||||
- Crypto operations needed?
|
||||
|
||||
### 2. Investigate
|
||||
```bash
|
||||
# Check existing NIP implementations
|
||||
grep -r "kind.*=" quartz/src/
|
||||
# Find event classes
|
||||
grep -r "class.*Event" quartz/src/
|
||||
# Check crypto usage
|
||||
grep -r "Secp256k1\|Nip44\|sign\|verify" quartz/src/
|
||||
```
|
||||
|
||||
### 3. Reference NIP Spec
|
||||
```bash
|
||||
# Fetch NIP specification
|
||||
curl https://raw.githubusercontent.com/nostr-protocol/nips/master/XX.md
|
||||
```
|
||||
|
||||
### 4. Implement
|
||||
- Follow NIP spec exactly
|
||||
- Use existing Quartz patterns
|
||||
- Validate event structure
|
||||
- Test signature verification
|
||||
|
||||
### 5. Verify
|
||||
```bash
|
||||
./gradlew :quartz:test
|
||||
```
|
||||
|
||||
## Quartz Library Structure
|
||||
```
|
||||
quartz/src/commonMain/kotlin/
|
||||
├── events/ # Event types per NIP
|
||||
├── encoders/ # Bech32, hex encoding
|
||||
├── crypto/ # Signing, encryption
|
||||
├── relay/ # WebSocket communication
|
||||
└── filters/ # Subscription filters
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- Always follow NIP specifications exactly
|
||||
- Use NIP-44 for new encryption (not NIP-04)
|
||||
- Validate all incoming events (sig, id)
|
||||
- Never log private keys or decrypted content
|
||||
- Use existing Quartz classes when available
|
||||
- Test with real relay responses when possible
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
description: Build and run the desktop app
|
||||
---
|
||||
|
||||
Build and run the Amethyst Desktop application:
|
||||
|
||||
```bash
|
||||
./gradlew :desktopApp:run
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the build fails, check:
|
||||
|
||||
1. **JDK Version**: Requires JDK 17+
|
||||
```bash
|
||||
java -version
|
||||
```
|
||||
|
||||
2. **Compose Multiplatform Plugin**: Verify version in `gradle/libs.versions.toml`
|
||||
|
||||
3. **Quartz Build**: Ensure Quartz compiles first
|
||||
```bash
|
||||
./gradlew :quartz:build
|
||||
```
|
||||
|
||||
4. **Desktop Dependencies**: Check `desktopApp/build.gradle.kts` has:
|
||||
```kotlin
|
||||
implementation(compose.desktop.currentOs)
|
||||
```
|
||||
|
||||
## Creating Distributable
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
./gradlew :desktopApp:packageDmg
|
||||
|
||||
# Windows
|
||||
./gradlew :desktopApp:packageMsi
|
||||
|
||||
# Linux
|
||||
./gradlew :desktopApp:packageDeb
|
||||
```
|
||||
|
||||
Outputs will be in `desktopApp/build/compose/binaries/`
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
description: Extract a composable from amethyst to shared code
|
||||
---
|
||||
|
||||
Extract the component `$ARGUMENTS` from the Android app to shared KMP code:
|
||||
|
||||
## Process
|
||||
|
||||
1. **Locate the component** in the amethyst module:
|
||||
```bash
|
||||
find amethyst/src -name "*$ARGUMENTS*" -o -name "*$ARGUMENTS*"
|
||||
grep -r "fun $ARGUMENTS\|class $ARGUMENTS" amethyst/src/
|
||||
```
|
||||
|
||||
2. **Analyze dependencies**:
|
||||
- Android-specific imports (Context, Intent, etc.)
|
||||
- Platform APIs (Camera, MediaStore, etc.)
|
||||
- Android Compose specifics vs standard Compose
|
||||
|
||||
3. **Identify what can be shared**:
|
||||
- Pure Composable functions → `shared-ui/commonMain/`
|
||||
- Business logic → `quartz/commonMain/`
|
||||
- Platform-specific → create expect/actual
|
||||
|
||||
4. **Create shared version**:
|
||||
- Move to appropriate shared module
|
||||
- Replace Android imports with multiplatform alternatives
|
||||
- Add expect declarations for platform-specific parts
|
||||
|
||||
5. **Update references**:
|
||||
- Change imports in amethyst module
|
||||
- Add implementations in desktopApp if needed
|
||||
|
||||
## Common Replacements
|
||||
|
||||
| Android | Multiplatform |
|
||||
|---------|---------------|
|
||||
| `LocalContext.current` | expect/actual or parameter |
|
||||
| `stringResource()` | `Res.string.*` |
|
||||
| `painterResource()` | `painterResource(Res.drawable.*)` |
|
||||
| `Toast.makeText()` | Custom snackbar/notification |
|
||||
| `Intent` | expect/actual for navigation |
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
/extract NoteCard
|
||||
```
|
||||
|
||||
This will find NoteCard, analyze its dependencies, and guide you through extracting it to shared code.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
description: Get NIP specification and implementation guidance
|
||||
---
|
||||
|
||||
Fetch and explain NIP-$ARGUMENTS from the Nostr protocol:
|
||||
|
||||
1. **Get the specification** from https://github.com/nostr-protocol/nips/blob/master/$ARGUMENTS.md
|
||||
|
||||
2. **Show key details**:
|
||||
- Event kind(s) used
|
||||
- Required and optional fields
|
||||
- Tag structure
|
||||
- Message flow between client and relay
|
||||
|
||||
3. **Check implementation status** in Quartz:
|
||||
```bash
|
||||
grep -r "NIP-$ARGUMENTS\|nip$ARGUMENTS\|kind.*=" quartz/src/
|
||||
```
|
||||
|
||||
4. **Provide implementation guidance**:
|
||||
- Which Quartz classes to use or create
|
||||
- Event construction example
|
||||
- Relay subscription filters
|
||||
- Verification/validation logic
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
/nip 01 # Basic protocol
|
||||
/nip 44 # Versioned encryption
|
||||
/nip 57 # Zaps
|
||||
```
|
||||
@@ -0,0 +1,339 @@
|
||||
# Compose Desktop Skill
|
||||
|
||||
## Desktop Application Entry Point
|
||||
|
||||
```kotlin
|
||||
// desktopApp/src/jvmMain/kotlin/Main.kt
|
||||
package com.vitorpamplona.amethyst.desktop
|
||||
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.*
|
||||
|
||||
fun main() = application {
|
||||
val windowState = rememberWindowState(
|
||||
width = 1200.dp,
|
||||
height = 800.dp,
|
||||
position = WindowPosition.Aligned(Alignment.Center)
|
||||
)
|
||||
|
||||
// System tray
|
||||
Tray(
|
||||
icon = painterResource("icon.png"),
|
||||
tooltip = "Amethyst",
|
||||
menu = {
|
||||
Item("Show", onClick = { windowState.isMinimized = false })
|
||||
Separator()
|
||||
Item("Exit", onClick = ::exitApplication)
|
||||
}
|
||||
)
|
||||
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
state = windowState,
|
||||
title = "Amethyst",
|
||||
icon = painterResource("icon.png")
|
||||
) {
|
||||
MenuBar {
|
||||
Menu("File") {
|
||||
Item("New Note", shortcut = KeyShortcut(Key.N, ctrl = true)) { }
|
||||
Separator()
|
||||
Item("Settings", shortcut = KeyShortcut(Key.Comma, ctrl = true)) { }
|
||||
Separator()
|
||||
Item("Quit", shortcut = KeyShortcut(Key.Q, ctrl = true), onClick = ::exitApplication)
|
||||
}
|
||||
Menu("Edit") {
|
||||
Item("Copy", shortcut = KeyShortcut(Key.C, ctrl = true)) { }
|
||||
Item("Paste", shortcut = KeyShortcut(Key.V, ctrl = true)) { }
|
||||
}
|
||||
Menu("View") {
|
||||
Item("Feed") { }
|
||||
Item("Messages") { }
|
||||
Item("Notifications") { }
|
||||
}
|
||||
Menu("Help") {
|
||||
Item("About Amethyst") { }
|
||||
}
|
||||
}
|
||||
|
||||
App()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Desktop-Specific Components
|
||||
|
||||
### File Dialog
|
||||
```kotlin
|
||||
@Composable
|
||||
fun rememberFileDialog(): FileDialogState {
|
||||
return remember { FileDialogState() }
|
||||
}
|
||||
|
||||
class FileDialogState {
|
||||
var isOpen by mutableStateOf(false)
|
||||
var result by mutableStateOf<File?>(null)
|
||||
|
||||
fun open() { isOpen = true }
|
||||
|
||||
@Composable
|
||||
fun Dialog(
|
||||
title: String = "Select File",
|
||||
allowedExtensions: List<String> = emptyList()
|
||||
) {
|
||||
if (isOpen) {
|
||||
DisposableEffect(Unit) {
|
||||
val dialog = java.awt.FileDialog(null as java.awt.Frame?, title)
|
||||
if (allowedExtensions.isNotEmpty()) {
|
||||
dialog.setFilenameFilter { _, name ->
|
||||
allowedExtensions.any { name.endsWith(it) }
|
||||
}
|
||||
}
|
||||
dialog.isVisible = true
|
||||
result = dialog.file?.let { File(dialog.directory, it) }
|
||||
isOpen = false
|
||||
onDispose { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scroll Behavior
|
||||
```kotlin
|
||||
@Composable
|
||||
fun DesktopScrollableColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Box(modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
VerticalScrollbar(
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
adapter = rememberScrollbarAdapter(scrollState)
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Keyboard Navigation
|
||||
```kotlin
|
||||
@Composable
|
||||
fun KeyboardNavigableList(
|
||||
items: List<Note>,
|
||||
selectedIndex: Int,
|
||||
onSelect: (Int) -> Unit,
|
||||
onActivate: (Note) -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusable()
|
||||
.onKeyEvent { event ->
|
||||
when {
|
||||
event.key == Key.DirectionDown && event.type == KeyEventType.KeyDown -> {
|
||||
onSelect((selectedIndex + 1).coerceAtMost(items.lastIndex))
|
||||
true
|
||||
}
|
||||
event.key == Key.DirectionUp && event.type == KeyEventType.KeyDown -> {
|
||||
onSelect((selectedIndex - 1).coerceAtLeast(0))
|
||||
true
|
||||
}
|
||||
event.key == Key.Enter && event.type == KeyEventType.KeyDown -> {
|
||||
items.getOrNull(selectedIndex)?.let { onActivate(it) }
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
) {
|
||||
itemsIndexed(items) { index, note ->
|
||||
NoteCard(
|
||||
note = note,
|
||||
isSelected = index == selectedIndex,
|
||||
modifier = Modifier.clickable { onSelect(index) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Window Support
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ApplicationScope.NoteDetailWindow(
|
||||
note: Note,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
Window(
|
||||
onCloseRequest = onClose,
|
||||
title = "Note by ${note.author.name}",
|
||||
state = rememberWindowState(width = 600.dp, height = 400.dp)
|
||||
) {
|
||||
NoteDetailScreen(note)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in main application
|
||||
var openNotes by remember { mutableStateOf<List<Note>>(emptyList()) }
|
||||
|
||||
openNotes.forEach { note ->
|
||||
key(note.id) {
|
||||
NoteDetailWindow(
|
||||
note = note,
|
||||
onClose = { openNotes = openNotes - note }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tooltips
|
||||
```kotlin
|
||||
@Composable
|
||||
fun TooltipButton(
|
||||
tooltip: String,
|
||||
onClick: () -> Unit,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
TooltipArea(
|
||||
tooltip = {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = MaterialTheme.colorScheme.inverseSurface
|
||||
) {
|
||||
Text(
|
||||
text = tooltip,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
color = MaterialTheme.colorScheme.inverseOnSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
IconButton(onClick = onClick) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Desktop Layout Pattern
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun DesktopAppLayout(
|
||||
currentScreen: Screen,
|
||||
onNavigate: (Screen) -> Unit,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Row(Modifier.fillMaxSize()) {
|
||||
// Sidebar navigation
|
||||
NavigationRail(
|
||||
modifier = Modifier.width(72.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
NavigationRailItem(
|
||||
icon = { Icon(Icons.Default.Home, "Feed") },
|
||||
label = { Text("Feed") },
|
||||
selected = currentScreen == Screen.Feed,
|
||||
onClick = { onNavigate(Screen.Feed) }
|
||||
)
|
||||
|
||||
NavigationRailItem(
|
||||
icon = { Icon(Icons.Default.Email, "Messages") },
|
||||
label = { Text("DMs") },
|
||||
selected = currentScreen == Screen.Messages,
|
||||
onClick = { onNavigate(Screen.Messages) }
|
||||
)
|
||||
|
||||
NavigationRailItem(
|
||||
icon = { Icon(Icons.Default.Notifications, "Notifications") },
|
||||
label = { Text("Alerts") },
|
||||
selected = currentScreen == Screen.Notifications,
|
||||
onClick = { onNavigate(Screen.Notifications) }
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
NavigationRailItem(
|
||||
icon = { Icon(Icons.Default.Settings, "Settings") },
|
||||
label = { Text("Settings") },
|
||||
selected = currentScreen == Screen.Settings,
|
||||
onClick = { onNavigate(Screen.Settings) }
|
||||
)
|
||||
}
|
||||
|
||||
// Divider
|
||||
VerticalDivider()
|
||||
|
||||
// Main content
|
||||
Box(Modifier.weight(1f).fillMaxHeight()) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build Configuration
|
||||
|
||||
```kotlin
|
||||
// desktopApp/build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("org.jetbrains.compose")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.material3)
|
||||
implementation(project(":quartz"))
|
||||
}
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "com.vitorpamplona.amethyst.desktop.MainKt"
|
||||
|
||||
nativeDistributions {
|
||||
targetFormats(
|
||||
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg,
|
||||
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi,
|
||||
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Deb
|
||||
)
|
||||
|
||||
packageName = "Amethyst"
|
||||
packageVersion = "1.0.0"
|
||||
|
||||
macOS {
|
||||
bundleID = "com.vitorpamplona.amethyst.desktop"
|
||||
iconFile.set(project.file("icons/icon.icns"))
|
||||
}
|
||||
|
||||
windows {
|
||||
iconFile.set(project.file("icons/icon.ico"))
|
||||
menuGroup = "Amethyst"
|
||||
}
|
||||
|
||||
linux {
|
||||
iconFile.set(project.file("icons/icon.png"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,165 @@
|
||||
# Quartz KMP Conversion Skill
|
||||
|
||||
When working with Quartz library conversion to Kotlin Multiplatform:
|
||||
|
||||
## Current Structure (Android-only)
|
||||
```
|
||||
quartz/
|
||||
├── build.gradle
|
||||
└── src/
|
||||
├── main/kotlin/ # All Nostr code here
|
||||
├── test/
|
||||
└── androidTest/
|
||||
```
|
||||
|
||||
## Target Structure (KMP)
|
||||
```
|
||||
quartz/
|
||||
├── build.gradle.kts
|
||||
└── src/
|
||||
├── commonMain/kotlin/ # Shared protocol code
|
||||
├── commonTest/kotlin/ # Shared tests
|
||||
├── androidMain/kotlin/ # Android crypto, storage
|
||||
├── androidTest/kotlin/
|
||||
├── jvmMain/kotlin/ # Desktop crypto, storage
|
||||
└── jvmTest/kotlin/
|
||||
```
|
||||
|
||||
## Platform Abstractions Required
|
||||
|
||||
### 1. Cryptography (expect/actual)
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect object Secp256k1 {
|
||||
fun sign(data: ByteArray, privateKey: ByteArray): ByteArray
|
||||
fun verify(data: ByteArray, signature: ByteArray, pubKey: ByteArray): Boolean
|
||||
fun pubKeyCreate(privateKey: ByteArray): ByteArray
|
||||
}
|
||||
|
||||
// androidMain - uses secp256k1-kmp-jni-android
|
||||
actual object Secp256k1 {
|
||||
actual fun sign(data: ByteArray, privateKey: ByteArray): ByteArray {
|
||||
return fr.acinq.secp256k1.Secp256k1.sign(data, privateKey)
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// jvmMain - uses secp256k1-kmp-jni-jvm
|
||||
actual object Secp256k1 {
|
||||
actual fun sign(data: ByteArray, privateKey: ByteArray): ByteArray {
|
||||
return fr.acinq.secp256k1.Secp256k1.sign(data, privateKey)
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. NIP-44 Encryption (Sodium)
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect object Nip44 {
|
||||
fun encrypt(plaintext: String, sharedSecret: ByteArray): String
|
||||
fun decrypt(ciphertext: String, sharedSecret: ByteArray): String
|
||||
}
|
||||
|
||||
// androidMain - lazysodium-android
|
||||
// jvmMain - lazysodium-java or libsodium-jni
|
||||
```
|
||||
|
||||
### 3. Secure Random
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect fun secureRandomBytes(size: Int): ByteArray
|
||||
|
||||
// androidMain
|
||||
actual fun secureRandomBytes(size: Int): ByteArray {
|
||||
return SecureRandom().let { random ->
|
||||
ByteArray(size).also { random.nextBytes(it) }
|
||||
}
|
||||
}
|
||||
|
||||
// jvmMain
|
||||
actual fun secureRandomBytes(size: Int): ByteArray {
|
||||
return java.security.SecureRandom().let { random ->
|
||||
ByteArray(size).also { random.nextBytes(it) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build Configuration
|
||||
|
||||
```kotlin
|
||||
// quartz/build.gradle.kts
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
androidTarget {
|
||||
compilations.all {
|
||||
kotlinOptions.jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
jvm("desktop") {
|
||||
compilations.all {
|
||||
kotlinOptions.jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
}
|
||||
}
|
||||
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.secp256k1.kmp.jni.android)
|
||||
implementation(libs.lazysodium.android)
|
||||
}
|
||||
}
|
||||
|
||||
val desktopMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.secp256k1.kmp.jni.jvm)
|
||||
// lazysodium-java or alternative
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.vitorpamplona.quartz"
|
||||
compileSdk = 35
|
||||
defaultConfig.minSdk = 26
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Steps
|
||||
|
||||
1. **Convert build.gradle to build.gradle.kts** with KMP plugin
|
||||
2. **Move pure Kotlin code** to `commonMain/`
|
||||
3. **Identify platform dependencies** (crypto, JNA, Android APIs)
|
||||
4. **Create expect declarations** for platform-specific APIs
|
||||
5. **Implement actuals** in androidMain and jvmMain
|
||||
6. **Update imports** in amethyst module
|
||||
7. **Test on both platforms**
|
||||
|
||||
## Files to Move to commonMain
|
||||
|
||||
Most of Quartz can be shared:
|
||||
- Event classes and parsing
|
||||
- Filter definitions
|
||||
- Relay message types
|
||||
- NIP implementations (logic only)
|
||||
- Utilities (hex encoding, bech32)
|
||||
|
||||
## Files Needing expect/actual
|
||||
|
||||
- `Secp256k1.kt` - Signature operations
|
||||
- `Nip04.kt` - Legacy encryption (uses AES)
|
||||
- `Nip44.kt` - Modern encryption (uses ChaCha)
|
||||
- `KeyPair.kt` - Key generation
|
||||
@@ -138,3 +138,6 @@ lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Local task tracking
|
||||
TASKS.md
|
||||
|
||||
@@ -153,9 +153,11 @@ Information shared on Nostr can be re-broadcasted to other servers and should be
|
||||
|
||||
# Development Overview
|
||||
|
||||
This repository is split between Amethyst and Quartz:
|
||||
- Amethyst is a native Android app made with Kotlin and Jetpack Compose.
|
||||
- Quartz is our own Nostr-commons library to host classes that are of interest to other Nostr Clients.
|
||||
This repository is split between Amethyst, Quartz, Commons, and DesktopApp:
|
||||
- **Amethyst** - Native Android app with Kotlin and Jetpack Compose
|
||||
- **Quartz** - Nostr-commons KMP library for protocol classes shared across platforms
|
||||
- **Commons** - Kotlin Multiplatform module with shared UI components (icons, robohash, blurhash, composables)
|
||||
- **DesktopApp** - Compose Multiplatform Desktop application reusing commons and quartz
|
||||
|
||||
The app architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
|
||||
and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph.
|
||||
@@ -187,11 +189,17 @@ git clone https://github.com/vitorpamplona/amethyst.git
|
||||
Use an Android Studio build action to install and run the app on your device or a simulator.
|
||||
|
||||
## Building
|
||||
Build the app:
|
||||
|
||||
Build the Android app:
|
||||
```bash
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
Build and run the Desktop app (requires Java 21+):
|
||||
```bash
|
||||
./gradlew :desktopApp:run
|
||||
```
|
||||
|
||||
## Testing
|
||||
```bash
|
||||
./gradlew test
|
||||
|
||||
@@ -31,6 +31,7 @@ import coil3.fetch.ImageFetchResult
|
||||
import coil3.key.Keyer
|
||||
import coil3.request.Options
|
||||
import com.vitorpamplona.amethyst.commons.base64Image.Base64Image
|
||||
import com.vitorpamplona.amethyst.commons.base64Image.toBitmap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
|
||||
@@ -42,7 +43,7 @@ class Base64Fetcher(
|
||||
override suspend fun fetch(): FetchResult? =
|
||||
runCatching {
|
||||
ImageFetchResult(
|
||||
image = Base64Image.Companion.toBitmap(data.toString()).asImage(true),
|
||||
image = Base64Image.toBitmap(data.toString()).asImage(true),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.MEMORY,
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ import coil3.fetch.ImageFetchResult
|
||||
import coil3.key.Keyer
|
||||
import coil3.request.Options
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoder
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.toAndroidBitmap
|
||||
|
||||
data class BlurhashWrapper(
|
||||
val blurhash: String,
|
||||
@@ -43,10 +44,10 @@ class BlurHashFetcher(
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
val hash = data.blurhash
|
||||
|
||||
val bitmap = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: return null
|
||||
val platformImage = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: return null
|
||||
|
||||
return ImageFetchResult(
|
||||
image = bitmap.asImage(true),
|
||||
image = platformImage.toAndroidBitmap().asImage(true),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.MEMORY,
|
||||
)
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ plugins {
|
||||
alias(libs.plugins.diffplugSpotless)
|
||||
alias(libs.plugins.googleServices) apply false
|
||||
alias(libs.plugins.jetbrainsComposeCompiler) apply false
|
||||
alias(libs.plugins.composeMultiplatform) apply false
|
||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||
alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false
|
||||
alias(libs.plugins.serialization)
|
||||
@@ -64,4 +65,4 @@ tasks.register('installGitHook', Copy) {
|
||||
into { new File(rootProject.rootDir, '.git/hooks') }
|
||||
filePermissions { unix(0777) }
|
||||
}
|
||||
tasks.getByPath(':amethyst:preBuild').dependsOn installGitHook
|
||||
tasks.getByPath(':amethyst:preBuild').dependsOn installGitHook
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
alias(libs.plugins.jetbrainsComposeCompiler)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = 'com.vitorpamplona.amethyst.commons'
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInteger()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInteger()
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_21
|
||||
freeCompilerArgs.add("-Xstring-concat=inline")
|
||||
}
|
||||
}
|
||||
|
||||
composeCompiler {
|
||||
reportsDestination = layout.buildDirectory.dir("compose_compiler")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':quartz')
|
||||
// Import @Immutable and @Stable
|
||||
implementation platform(libs.androidx.compose.bom)
|
||||
implementation libs.androidx.ui
|
||||
implementation libs.androidx.compose.foundation
|
||||
|
||||
debugImplementation libs.androidx.ui.tooling
|
||||
implementation libs.androidx.ui.tooling.preview
|
||||
|
||||
// immutable collections to avoid recomposition
|
||||
api libs.kotlinx.collections.immutable
|
||||
|
||||
testImplementation libs.junit
|
||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.jetbrainsComposeCompiler)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.vitorpamplona.amethyst.commons"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xstring-concat=inline")
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
}
|
||||
|
||||
jvm {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
}
|
||||
}
|
||||
|
||||
androidTarget {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(project(":quartz"))
|
||||
|
||||
// Compose Multiplatform
|
||||
implementation(compose.ui)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.materialIconsExtended)
|
||||
|
||||
// LruCache (KMP-ready)
|
||||
implementation(libs.androidx.collection)
|
||||
|
||||
// Immutable collections
|
||||
api(libs.kotlinx.collections.immutable)
|
||||
}
|
||||
}
|
||||
|
||||
commonTest {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
|
||||
// Shared JVM code for both Android and Desktop
|
||||
val jvmAndroid = create("jvmAndroid") {
|
||||
dependsOn(commonMain.get())
|
||||
dependencies {
|
||||
// URL detection (JVM library, works on both)
|
||||
implementation(libs.url.detector)
|
||||
}
|
||||
}
|
||||
|
||||
jvmMain {
|
||||
dependsOn(jvmAndroid)
|
||||
dependencies {
|
||||
// Desktop-specific Compose
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.uiTooling)
|
||||
}
|
||||
}
|
||||
|
||||
androidMain {
|
||||
dependsOn(jvmAndroid)
|
||||
dependencies {
|
||||
// Android-specific Compose tooling
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
}
|
||||
}
|
||||
|
||||
androidUnitTest {
|
||||
dependencies {
|
||||
implementation(libs.junit)
|
||||
}
|
||||
}
|
||||
|
||||
androidInstrumentedTest {
|
||||
dependencies {
|
||||
implementation(libs.androidx.junit)
|
||||
implementation(libs.androidx.espresso.core)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.base64Image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage
|
||||
import java.util.Base64
|
||||
|
||||
fun Base64Image.toBitmap(content: String): Bitmap {
|
||||
val matcher = pattern.matcher(content)
|
||||
|
||||
if (matcher.find()) {
|
||||
val base64String = matcher.group(2)
|
||||
val byteArray = Base64.getDecoder().decode(base64String)
|
||||
return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
|
||||
}
|
||||
|
||||
throw Exception("Unable to convert base64 to image $content")
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a base64 image data URI to a PlatformImage.
|
||||
* Delegates to toBitmap and wraps the result.
|
||||
*/
|
||||
fun Base64Image.toPlatformImage(content: String): PlatformImage = toBitmap(content).toPlatformImage()
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
/**
|
||||
* Encodes this Android Bitmap to a blurhash string.
|
||||
* Delegates to PlatformImage.toBlurhash() for the actual encoding.
|
||||
*/
|
||||
fun Bitmap.toBlurhash(): String = this.toPlatformImage().toBlurhash()
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
actual class PlatformImage(
|
||||
val bitmap: Bitmap,
|
||||
) {
|
||||
actual val width: Int get() = bitmap.width
|
||||
actual val height: Int get() = bitmap.height
|
||||
|
||||
actual fun getPixels(
|
||||
pixels: IntArray,
|
||||
offset: Int,
|
||||
stride: Int,
|
||||
x: Int,
|
||||
y: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
) {
|
||||
bitmap.getPixels(pixels, offset, stride, x, y, width, height)
|
||||
}
|
||||
|
||||
actual fun scale(
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage = PlatformImage(Bitmap.createScaledBitmap(bitmap, width, height, false))
|
||||
|
||||
actual companion object {
|
||||
actual fun create(
|
||||
pixels: IntArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage = PlatformImage(Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension to convert Android Bitmap to PlatformImage.
|
||||
*/
|
||||
fun Bitmap.toPlatformImage(): PlatformImage = PlatformImage(this)
|
||||
|
||||
/**
|
||||
* Extension to get the underlying Android Bitmap.
|
||||
*/
|
||||
fun PlatformImage.toAndroidBitmap(): Bitmap = this.bitmap
|
||||
+3
-4
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.SRGB.Companion.linearToSrgb
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.SRGB.Companion.srgbToLinear
|
||||
import kotlin.math.pow
|
||||
@@ -66,7 +65,7 @@ object BlurHashDecoder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a blur hash into a new bitmap.
|
||||
* Decode a blur hash into a new PlatformImage.
|
||||
*
|
||||
* @param useCache use in memory cache for the calculated math, reused by images with same size.
|
||||
* if the cache does not exist yet it will be created and populated with new calculations. By
|
||||
@@ -76,7 +75,7 @@ object BlurHashDecoder {
|
||||
blurHash: String?,
|
||||
width: Int,
|
||||
useCache: Boolean = true,
|
||||
): Bitmap? {
|
||||
): PlatformImage? {
|
||||
if (blurHash == null || blurHash.length < 6) {
|
||||
return null
|
||||
}
|
||||
@@ -87,7 +86,7 @@ object BlurHashDecoder {
|
||||
val height = (width * (1 / (numCompX.toFloat() / numCompY.toFloat()))).roundToInt()
|
||||
val colors = computeColors(numCompX, numCompY, blurHash)
|
||||
val imageArray = composeImageArray(width, height, numCompX, numCompY, colors, useCache)
|
||||
return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
|
||||
return PlatformImage.create(imageArray, width, height)
|
||||
}
|
||||
|
||||
private fun decodeDc(colorEnc: Int): FloatArray {
|
||||
+6
-3
@@ -20,14 +20,17 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Bitmap.toBlurhash(): String {
|
||||
/**
|
||||
* Encodes this PlatformImage to a blurhash string.
|
||||
* The image will be scaled down if larger than 100x100 for performance.
|
||||
*/
|
||||
fun PlatformImage.toBlurhash(): String {
|
||||
val aspectRatio = this.width.toFloat() / this.height.toFloat()
|
||||
|
||||
if (this.width > 100 && this.height > 100) {
|
||||
return Bitmap.createScaledBitmap(this, 100, (100 / aspectRatio).toInt(), false).toBlurhash()
|
||||
return this.scale(100, (100 / aspectRatio).toInt()).toBlurhash()
|
||||
}
|
||||
|
||||
val intArray = IntArray(width * height)
|
||||
+3
-2
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
|
||||
object CosineCache {
|
||||
@@ -54,7 +55,7 @@ object CosineCache {
|
||||
DoubleArray(height * numCompY) {
|
||||
val y = it / numCompY
|
||||
val j = it % numCompY
|
||||
cos(Math.PI * y * j / height)
|
||||
cos(PI * y * j / height)
|
||||
}.also {
|
||||
cacheCosinesY.put(height * numCompY, it)
|
||||
}
|
||||
@@ -73,7 +74,7 @@ object CosineCache {
|
||||
DoubleArray(width * numCompX) {
|
||||
val x = it / numCompX
|
||||
val i = it % numCompX
|
||||
cos(Math.PI * x * i / width)
|
||||
cos(PI * x * i / width)
|
||||
}.also { cacheCosinesX.put(width * numCompX, it) }
|
||||
}
|
||||
else -> cacheCosinesX[width * numCompX]!!
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
/**
|
||||
* Platform-agnostic image wrapper for blurhash encoding/decoding.
|
||||
*
|
||||
* On Android: wraps android.graphics.Bitmap
|
||||
* On Desktop JVM: wraps java.awt.image.BufferedImage
|
||||
*/
|
||||
expect class PlatformImage {
|
||||
val width: Int
|
||||
val height: Int
|
||||
|
||||
/**
|
||||
* Read ARGB pixels into the provided array.
|
||||
* Pixels are in ARGB format (0xAARRGGBB).
|
||||
*/
|
||||
fun getPixels(
|
||||
pixels: IntArray,
|
||||
offset: Int,
|
||||
stride: Int,
|
||||
x: Int,
|
||||
y: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a scaled copy of this image.
|
||||
*/
|
||||
fun scale(
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a new image from ARGB pixel array.
|
||||
*/
|
||||
fun create(
|
||||
pixels: IntArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -20,13 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.compose
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
|
||||
@Composable
|
||||
fun <K, V> produceCachedStateAsync(
|
||||
fun <K : Any, V : Any> produceCachedStateAsync(
|
||||
cache: AsyncCachedState<K, V>,
|
||||
key: K,
|
||||
): State<V?> =
|
||||
@@ -39,7 +39,7 @@ fun <K, V> produceCachedStateAsync(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <K, V> produceCachedStateAsync(
|
||||
fun <K : Any, V : Any> produceCachedStateAsync(
|
||||
cache: AsyncCachedState<K, V>,
|
||||
key: String,
|
||||
updateValue: K,
|
||||
@@ -52,13 +52,13 @@ fun <K, V> produceCachedStateAsync(
|
||||
}
|
||||
}
|
||||
|
||||
interface AsyncCachedState<K, V> {
|
||||
interface AsyncCachedState<K : Any, V : Any> {
|
||||
fun cached(k: K): V?
|
||||
|
||||
suspend fun update(k: K): V?
|
||||
}
|
||||
|
||||
abstract class GenericBaseCacheAsync<K, V>(
|
||||
abstract class GenericBaseCacheAsync<K : Any, V : Any>(
|
||||
capacity: Int,
|
||||
) : AsyncCachedState<K, V> {
|
||||
private val cache = LruCache<K, V>(capacity)
|
||||
+5
-5
@@ -20,13 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.compose
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
|
||||
@Composable
|
||||
fun <K, V> produceCachedState(
|
||||
fun <K : Any, V : Any> produceCachedState(
|
||||
cache: CachedState<K, V>,
|
||||
key: K,
|
||||
): State<V?> =
|
||||
@@ -39,7 +39,7 @@ fun <K, V> produceCachedState(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <K, V> produceCachedState(
|
||||
fun <K : Any, V : Any> produceCachedState(
|
||||
cache: CachedState<K, V>,
|
||||
key: String,
|
||||
updateValue: K,
|
||||
@@ -52,13 +52,13 @@ fun <K, V> produceCachedState(
|
||||
}
|
||||
}
|
||||
|
||||
interface CachedState<K, V> {
|
||||
interface CachedState<K : Any, V : Any> {
|
||||
fun cached(k: K): V?
|
||||
|
||||
suspend fun update(k: K): V?
|
||||
}
|
||||
|
||||
abstract class GenericBaseCache<K, V>(
|
||||
abstract class GenericBaseCache<K : Any, V : Any>(
|
||||
capacity: Int,
|
||||
) : CachedState<K, V> {
|
||||
private val cache = LruCache<K, V>(capacity)
|
||||
-2
@@ -32,10 +32,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsAmethystPreview() {
|
||||
Image(
|
||||
-2
@@ -33,10 +33,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsBtcPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsCashuPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsCoffeePreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsFlowerstrPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsFootstrPreview() {
|
||||
Image(
|
||||
-2
@@ -28,10 +28,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsGamestrPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsGrownostrPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsLightningPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsMatePreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsNostrPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsPlebsPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsSkullPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsTunestrPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsWeedPreview() {
|
||||
Image(
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomHashTagIconsZapPreview() {
|
||||
Image(
|
||||
-2
@@ -29,10 +29,8 @@ import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(Following, null)
|
||||
-2
@@ -30,10 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(Like, null)
|
||||
-2
@@ -30,10 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(Liked, null)
|
||||
-2
@@ -30,10 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(Reply, null)
|
||||
-2
@@ -30,10 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(Repost, null)
|
||||
-2
@@ -30,10 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(Reposted, null)
|
||||
-2
@@ -30,10 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(Search, null)
|
||||
-2
@@ -30,10 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(Share, null)
|
||||
-2
@@ -31,9 +31,7 @@ import androidx.compose.ui.graphics.vector.DefaultFillType
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.PathBuilder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(Zap, null)
|
||||
-2
@@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.vector.DefaultFillType
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.PathBuilder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VectorPreview() {
|
||||
Image(ZapSplit, null)
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.navigation
|
||||
|
||||
/**
|
||||
* Main application screens shared between Desktop and Android.
|
||||
* Each platform implements its own navigation using these identifiers.
|
||||
*/
|
||||
enum class AppScreen(
|
||||
val label: String,
|
||||
val route: String,
|
||||
) {
|
||||
Feed("Feed", "feed"),
|
||||
Search("Search", "search"),
|
||||
Messages("Messages", "messages"),
|
||||
Notifications("Notifications", "notifications"),
|
||||
Profile("Profile", "profile"),
|
||||
Settings("Settings", "settings"),
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary navigation destinations (shown in bottom bar on mobile, sidebar on desktop).
|
||||
*/
|
||||
val primaryScreens =
|
||||
listOf(
|
||||
AppScreen.Feed,
|
||||
AppScreen.Search,
|
||||
AppScreen.Messages,
|
||||
AppScreen.Notifications,
|
||||
AppScreen.Profile,
|
||||
)
|
||||
|
||||
/**
|
||||
* Secondary navigation destinations (settings, etc.)
|
||||
*/
|
||||
val secondaryScreens =
|
||||
listOf(
|
||||
AppScreen.Settings,
|
||||
)
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.robohash
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.collection.LruCache
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
object CachedRobohash {
|
||||
-2
@@ -32,7 +32,6 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory0Seven
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory1Nose
|
||||
@@ -107,7 +106,6 @@ val MediumGray = SolidColor(Color(0xFFd0d2d3))
|
||||
val DefaultSize = 55.dp
|
||||
const val VIEWPORT_SIZE = 300f
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RobohashPreview() {
|
||||
val assembler = RobohashAssembler()
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory0SevenPreview() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory1NosePreview() {
|
||||
Image(
|
||||
-2
@@ -27,7 +27,6 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightBrown
|
||||
@@ -36,7 +35,6 @@ import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.MediumGray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory2HornRedPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory3ButtonPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory4SatellitePreview() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory5MustachePreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory6Hat() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory7Antenna() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory8Brush() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.MediumGray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Accessory9Horn() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body0TropperPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body1ThinPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body2ThinnestPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body3FrontPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body4RoundPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body5NeckPreview() {
|
||||
Image(
|
||||
-2
@@ -27,13 +27,11 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Yellow
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body6IronManPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body7NeckThinnerPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body8BigPreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Body9HugePreview() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes0Squint() {
|
||||
Image(
|
||||
-2
@@ -27,13 +27,11 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||
import com.vitorpamplona.amethyst.commons.robohash.DarkYellow
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes1Round() {
|
||||
Image(
|
||||
-2
@@ -27,13 +27,11 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes2Single() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes3Scott() {
|
||||
Image(
|
||||
-2
@@ -27,13 +27,11 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes4RoundSingle() {
|
||||
Image(
|
||||
-2
@@ -27,13 +27,11 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes5RoundSmall() {
|
||||
Image(
|
||||
-2
@@ -27,13 +27,11 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightYellow
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes6WallE() {
|
||||
Image(
|
||||
-2
@@ -27,12 +27,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes7Bar() {
|
||||
Image(
|
||||
-2
@@ -27,13 +27,11 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||
import com.vitorpamplona.amethyst.commons.robohash.LightYellow
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes8SmallBar() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Eyes9Shield() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Face0C3po() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Face1Rock() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Face2Long() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Face3Oval() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Face4Cylinder() {
|
||||
Image(
|
||||
-2
@@ -27,11 +27,9 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.PathData
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Face5Baloon() {
|
||||
Image(
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user