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/outputs/
|
||||||
lint/tmp/
|
lint/tmp/
|
||||||
# lint/reports/
|
# 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
|
# Development Overview
|
||||||
|
|
||||||
This repository is split between Amethyst and Quartz:
|
This repository is split between Amethyst, Quartz, Commons, and DesktopApp:
|
||||||
- Amethyst is a native Android app made with Kotlin and Jetpack Compose.
|
- **Amethyst** - Native Android app with Kotlin and Jetpack Compose
|
||||||
- Quartz is our own Nostr-commons library to host classes that are of interest to other Nostr Clients.
|
- **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,
|
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.
|
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.
|
Use an Android Studio build action to install and run the app on your device or a simulator.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
Build the app:
|
|
||||||
|
Build the Android app:
|
||||||
```bash
|
```bash
|
||||||
./gradlew assembleDebug
|
./gradlew assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Build and run the Desktop app (requires Java 21+):
|
||||||
|
```bash
|
||||||
|
./gradlew :desktopApp:run
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
```bash
|
```bash
|
||||||
./gradlew test
|
./gradlew test
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import coil3.fetch.ImageFetchResult
|
|||||||
import coil3.key.Keyer
|
import coil3.key.Keyer
|
||||||
import coil3.request.Options
|
import coil3.request.Options
|
||||||
import com.vitorpamplona.amethyst.commons.base64Image.Base64Image
|
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.nip01Core.core.toHexKey
|
||||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ class Base64Fetcher(
|
|||||||
override suspend fun fetch(): FetchResult? =
|
override suspend fun fetch(): FetchResult? =
|
||||||
runCatching {
|
runCatching {
|
||||||
ImageFetchResult(
|
ImageFetchResult(
|
||||||
image = Base64Image.Companion.toBitmap(data.toString()).asImage(true),
|
image = Base64Image.toBitmap(data.toString()).asImage(true),
|
||||||
isSampled = false,
|
isSampled = false,
|
||||||
dataSource = DataSource.MEMORY,
|
dataSource = DataSource.MEMORY,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import coil3.fetch.ImageFetchResult
|
|||||||
import coil3.key.Keyer
|
import coil3.key.Keyer
|
||||||
import coil3.request.Options
|
import coil3.request.Options
|
||||||
import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoder
|
import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoder
|
||||||
|
import com.vitorpamplona.amethyst.commons.blurhash.toAndroidBitmap
|
||||||
|
|
||||||
data class BlurhashWrapper(
|
data class BlurhashWrapper(
|
||||||
val blurhash: String,
|
val blurhash: String,
|
||||||
@@ -43,10 +44,10 @@ class BlurHashFetcher(
|
|||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
val hash = data.blurhash
|
val hash = data.blurhash
|
||||||
|
|
||||||
val bitmap = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: return null
|
val platformImage = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: return null
|
||||||
|
|
||||||
return ImageFetchResult(
|
return ImageFetchResult(
|
||||||
image = bitmap.asImage(true),
|
image = platformImage.toAndroidBitmap().asImage(true),
|
||||||
isSampled = false,
|
isSampled = false,
|
||||||
dataSource = DataSource.MEMORY,
|
dataSource = DataSource.MEMORY,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ plugins {
|
|||||||
alias(libs.plugins.diffplugSpotless)
|
alias(libs.plugins.diffplugSpotless)
|
||||||
alias(libs.plugins.googleServices) apply false
|
alias(libs.plugins.googleServices) apply false
|
||||||
alias(libs.plugins.jetbrainsComposeCompiler) apply false
|
alias(libs.plugins.jetbrainsComposeCompiler) apply false
|
||||||
|
alias(libs.plugins.composeMultiplatform) apply false
|
||||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||||
alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false
|
alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false
|
||||||
alias(libs.plugins.serialization)
|
alias(libs.plugins.serialization)
|
||||||
|
|||||||
@@ -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
|
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.linearToSrgb
|
||||||
import com.vitorpamplona.amethyst.commons.blurhash.SRGB.Companion.srgbToLinear
|
import com.vitorpamplona.amethyst.commons.blurhash.SRGB.Companion.srgbToLinear
|
||||||
import kotlin.math.pow
|
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.
|
* @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
|
* 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?,
|
blurHash: String?,
|
||||||
width: Int,
|
width: Int,
|
||||||
useCache: Boolean = true,
|
useCache: Boolean = true,
|
||||||
): Bitmap? {
|
): PlatformImage? {
|
||||||
if (blurHash == null || blurHash.length < 6) {
|
if (blurHash == null || blurHash.length < 6) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -87,7 +86,7 @@ object BlurHashDecoder {
|
|||||||
val height = (width * (1 / (numCompX.toFloat() / numCompY.toFloat()))).roundToInt()
|
val height = (width * (1 / (numCompX.toFloat() / numCompY.toFloat()))).roundToInt()
|
||||||
val colors = computeColors(numCompX, numCompY, blurHash)
|
val colors = computeColors(numCompX, numCompY, blurHash)
|
||||||
val imageArray = composeImageArray(width, height, numCompX, numCompY, colors, useCache)
|
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 {
|
private fun decodeDc(colorEnc: Int): FloatArray {
|
||||||
+6
-3
@@ -20,14 +20,17 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.blurhash
|
package com.vitorpamplona.amethyst.commons.blurhash
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import kotlin.math.roundToInt
|
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()
|
val aspectRatio = this.width.toFloat() / this.height.toFloat()
|
||||||
|
|
||||||
if (this.width > 100 && this.height > 100) {
|
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)
|
val intArray = IntArray(width * height)
|
||||||
+3
-2
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.commons.blurhash
|
package com.vitorpamplona.amethyst.commons.blurhash
|
||||||
|
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
|
import kotlin.math.PI
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
|
|
||||||
object CosineCache {
|
object CosineCache {
|
||||||
@@ -54,7 +55,7 @@ object CosineCache {
|
|||||||
DoubleArray(height * numCompY) {
|
DoubleArray(height * numCompY) {
|
||||||
val y = it / numCompY
|
val y = it / numCompY
|
||||||
val j = it % numCompY
|
val j = it % numCompY
|
||||||
cos(Math.PI * y * j / height)
|
cos(PI * y * j / height)
|
||||||
}.also {
|
}.also {
|
||||||
cacheCosinesY.put(height * numCompY, it)
|
cacheCosinesY.put(height * numCompY, it)
|
||||||
}
|
}
|
||||||
@@ -73,7 +74,7 @@ object CosineCache {
|
|||||||
DoubleArray(width * numCompX) {
|
DoubleArray(width * numCompX) {
|
||||||
val x = it / numCompX
|
val x = it / numCompX
|
||||||
val i = it % numCompX
|
val i = it % numCompX
|
||||||
cos(Math.PI * x * i / width)
|
cos(PI * x * i / width)
|
||||||
}.also { cacheCosinesX.put(width * numCompX, it) }
|
}.also { cacheCosinesX.put(width * numCompX, it) }
|
||||||
}
|
}
|
||||||
else -> cacheCosinesX[width * numCompX]!!
|
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
|
package com.vitorpamplona.amethyst.commons.compose
|
||||||
|
|
||||||
import android.util.LruCache
|
import androidx.collection.LruCache
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <K, V> produceCachedStateAsync(
|
fun <K : Any, V : Any> produceCachedStateAsync(
|
||||||
cache: AsyncCachedState<K, V>,
|
cache: AsyncCachedState<K, V>,
|
||||||
key: K,
|
key: K,
|
||||||
): State<V?> =
|
): State<V?> =
|
||||||
@@ -39,7 +39,7 @@ fun <K, V> produceCachedStateAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <K, V> produceCachedStateAsync(
|
fun <K : Any, V : Any> produceCachedStateAsync(
|
||||||
cache: AsyncCachedState<K, V>,
|
cache: AsyncCachedState<K, V>,
|
||||||
key: String,
|
key: String,
|
||||||
updateValue: K,
|
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?
|
fun cached(k: K): V?
|
||||||
|
|
||||||
suspend fun update(k: K): V?
|
suspend fun update(k: K): V?
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GenericBaseCacheAsync<K, V>(
|
abstract class GenericBaseCacheAsync<K : Any, V : Any>(
|
||||||
capacity: Int,
|
capacity: Int,
|
||||||
) : AsyncCachedState<K, V> {
|
) : AsyncCachedState<K, V> {
|
||||||
private val cache = LruCache<K, V>(capacity)
|
private val cache = LruCache<K, V>(capacity)
|
||||||
+5
-5
@@ -20,13 +20,13 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.compose
|
package com.vitorpamplona.amethyst.commons.compose
|
||||||
|
|
||||||
import android.util.LruCache
|
import androidx.collection.LruCache
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <K, V> produceCachedState(
|
fun <K : Any, V : Any> produceCachedState(
|
||||||
cache: CachedState<K, V>,
|
cache: CachedState<K, V>,
|
||||||
key: K,
|
key: K,
|
||||||
): State<V?> =
|
): State<V?> =
|
||||||
@@ -39,7 +39,7 @@ fun <K, V> produceCachedState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <K, V> produceCachedState(
|
fun <K : Any, V : Any> produceCachedState(
|
||||||
cache: CachedState<K, V>,
|
cache: CachedState<K, V>,
|
||||||
key: String,
|
key: String,
|
||||||
updateValue: K,
|
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?
|
fun cached(k: K): V?
|
||||||
|
|
||||||
suspend fun update(k: K): V?
|
suspend fun update(k: K): V?
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GenericBaseCache<K, V>(
|
abstract class GenericBaseCache<K : Any, V : Any>(
|
||||||
capacity: Int,
|
capacity: Int,
|
||||||
) : CachedState<K, V> {
|
) : CachedState<K, V> {
|
||||||
private val cache = LruCache<K, V>(capacity)
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsAmethystPreview() {
|
fun CustomHashTagIconsAmethystPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsBtcPreview() {
|
fun CustomHashTagIconsBtcPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsCashuPreview() {
|
fun CustomHashTagIconsCashuPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsCoffeePreview() {
|
fun CustomHashTagIconsCoffeePreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsFlowerstrPreview() {
|
fun CustomHashTagIconsFlowerstrPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsFootstrPreview() {
|
fun CustomHashTagIconsFootstrPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsGamestrPreview() {
|
fun CustomHashTagIconsGamestrPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsGrownostrPreview() {
|
fun CustomHashTagIconsGrownostrPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsLightningPreview() {
|
fun CustomHashTagIconsLightningPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsMatePreview() {
|
fun CustomHashTagIconsMatePreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsNostrPreview() {
|
fun CustomHashTagIconsNostrPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsPlebsPreview() {
|
fun CustomHashTagIconsPlebsPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsSkullPreview() {
|
fun CustomHashTagIconsSkullPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsTunestrPreview() {
|
fun CustomHashTagIconsTunestrPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsWeedPreview() {
|
fun CustomHashTagIconsWeedPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomHashTagIconsZapPreview() {
|
fun CustomHashTagIconsZapPreview() {
|
||||||
Image(
|
Image(
|
||||||
-2
@@ -29,10 +29,8 @@ import androidx.compose.ui.graphics.StrokeCap
|
|||||||
import androidx.compose.ui.graphics.StrokeJoin
|
import androidx.compose.ui.graphics.StrokeJoin
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(Following, null)
|
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
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(Like, null)
|
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
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(Liked, null)
|
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
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(Reply, null)
|
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
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(Repost, null)
|
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
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(Reposted, null)
|
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
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(Search, null)
|
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
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(Share, null)
|
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.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.PathBuilder
|
import androidx.compose.ui.graphics.vector.PathBuilder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(Zap, null)
|
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.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.PathBuilder
|
import androidx.compose.ui.graphics.vector.PathBuilder
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VectorPreview() {
|
private fun VectorPreview() {
|
||||||
Image(ZapSplit, null)
|
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
|
package com.vitorpamplona.amethyst.commons.robohash
|
||||||
|
|
||||||
import android.util.LruCache
|
import androidx.collection.LruCache
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
object CachedRobohash {
|
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.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory0Seven
|
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory0Seven
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory1Nose
|
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory1Nose
|
||||||
@@ -107,7 +106,6 @@ val MediumGray = SolidColor(Color(0xFFd0d2d3))
|
|||||||
val DefaultSize = 55.dp
|
val DefaultSize = 55.dp
|
||||||
const val VIEWPORT_SIZE = 300f
|
const val VIEWPORT_SIZE = 300f
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RobohashPreview() {
|
fun RobohashPreview() {
|
||||||
val assembler = RobohashAssembler()
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory0SevenPreview() {
|
fun Accessory0SevenPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory1NosePreview() {
|
fun Accessory1NosePreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightBrown
|
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.MediumGray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory2HornRedPreview() {
|
fun Accessory2HornRedPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory3ButtonPreview() {
|
fun Accessory3ButtonPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory4SatellitePreview() {
|
fun Accessory4SatellitePreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory5MustachePreview() {
|
fun Accessory5MustachePreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory6Hat() {
|
fun Accessory6Hat() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory7Antenna() {
|
fun Accessory7Antenna() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory8Brush() {
|
fun Accessory8Brush() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.MediumGray
|
import com.vitorpamplona.amethyst.commons.robohash.MediumGray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Accessory9Horn() {
|
fun Accessory9Horn() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body0TropperPreview() {
|
fun Body0TropperPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body1ThinPreview() {
|
fun Body1ThinPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body2ThinnestPreview() {
|
fun Body2ThinnestPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body3FrontPreview() {
|
fun Body3FrontPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body4RoundPreview() {
|
fun Body4RoundPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body5NeckPreview() {
|
fun Body5NeckPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Yellow
|
import com.vitorpamplona.amethyst.commons.robohash.Yellow
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body6IronManPreview() {
|
fun Body6IronManPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body7NeckThinnerPreview() {
|
fun Body7NeckThinnerPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body8BigPreview() {
|
fun Body8BigPreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
import com.vitorpamplona.amethyst.commons.robohash.Gray
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Body9HugePreview() {
|
fun Body9HugePreview() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes0Squint() {
|
fun Eyes0Squint() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.DarkYellow
|
import com.vitorpamplona.amethyst.commons.robohash.DarkYellow
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes1Round() {
|
fun Eyes1Round() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes2Single() {
|
fun Eyes2Single() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes3Scott() {
|
fun Eyes3Scott() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes4RoundSingle() {
|
fun Eyes4RoundSingle() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
import com.vitorpamplona.amethyst.commons.robohash.LightRed
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes5RoundSmall() {
|
fun Eyes5RoundSmall() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightYellow
|
import com.vitorpamplona.amethyst.commons.robohash.LightYellow
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes6WallE() {
|
fun Eyes6WallE() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes7Bar() {
|
fun Eyes7Bar() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
import com.vitorpamplona.amethyst.commons.robohash.Brown
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.LightYellow
|
import com.vitorpamplona.amethyst.commons.robohash.LightYellow
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes8SmallBar() {
|
fun Eyes8SmallBar() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Eyes9Shield() {
|
fun Eyes9Shield() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Face0C3po() {
|
fun Face0C3po() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Face1Rock() {
|
fun Face1Rock() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Face2Long() {
|
fun Face2Long() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Face3Oval() {
|
fun Face3Oval() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Face4Cylinder() {
|
fun Face4Cylinder() {
|
||||||
Image(
|
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.ImageVector.Builder
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
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.Black
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Face5Baloon() {
|
fun Face5Baloon() {
|
||||||
Image(
|
Image(
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user