mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
wear: M0 - scaffold :wear module with bitchat theme, verified on Pixel Watch 3
This commit is contained in:
parent
11d0bd794e
commit
a355ed79d0
241
docs/wear-os-implementation-plan.md
Normal file
241
docs/wear-os-implementation-plan.md
Normal file
@ -0,0 +1,241 @@
|
||||
# Bitchat for Pixel Watch — Implementation Plan
|
||||
|
||||
> **Status tracker**: each milestone carries a status (`pending` / `in-progress` / `done`) and a
|
||||
> checklist. A milestone may only be started when the previous milestone's success criteria pass.
|
||||
> Update statuses in this file as work progresses.
|
||||
|
||||
| Milestone | Title | Status |
|
||||
|-----------|-------|--------|
|
||||
| M0 | Scaffolding & plan document | done |
|
||||
| M1 | Shared core compiles on Wear | pending |
|
||||
| M2 | BLE transport & background service on watch | pending |
|
||||
| M3 | Global chat | pending |
|
||||
| M4 | Noise DMs & people screen | pending |
|
||||
| M5 | File/image receive & display — **DEFERRED** (post-M7, later day) | deferred |
|
||||
| M6 | ADB test hook & mesh_lab interop | pending |
|
||||
| M7 | Polish & final design pass | pending |
|
||||
|
||||
---
|
||||
|
||||
## 1. Context for a fresh coding agent
|
||||
|
||||
- **Reference app**: this repository (`bitchat-android`) is a fully working, decentralized BLE mesh
|
||||
chat client. Single Gradle module `:app`, root package `com.bitchat.android`, applicationId
|
||||
`com.bitchat.droid`. See `AGENTS.md` for the full architecture overview.
|
||||
- **Goal**: a new `:wear` Gradle module (applicationId `com.bitchat.watch`) — a standalone Wear OS
|
||||
app for the Pixel Watch. **Bluetooth mesh only**: global chat, Noise-encrypted direct messages,
|
||||
and receiving/displaying files & images. It must be a fully interoperable bitchat client: scan,
|
||||
advertise, connect, relay, handshake, and exchange messages with the Android (and iOS) apps.
|
||||
- **Explicitly out of scope**: no internet features (no Nostr, no Tor/Arti, no relays), no GPS /
|
||||
geohash / location channels, no Wi-Fi Aware, no hotspot/APK sharing, no voice notes recording.
|
||||
The watch manifest must not even declare `INTERNET` or location permissions.
|
||||
- **Hard constraint**: **zero modifications to `:app` production code.** The only allowed changes
|
||||
to shared repo files are: `settings.gradle.kts` (add `include(":wear")`), entries in
|
||||
`gradle/libs.versions.toml` (new wear dependencies only), the new `wear/` directory, and docs.
|
||||
All shared Kotlin code is consumed by the `:wear` module *in place* via Gradle source sets —
|
||||
files are never moved, copied, or edited.
|
||||
|
||||
## 2. Code reuse strategy (shared source sets)
|
||||
|
||||
Wear OS is Android. `android.util.Log`, `android.bluetooth.*`, `EncryptedSharedPreferences`
|
||||
(androidx.security), BouncyCastle, and coroutines all work on the watch, so the vast majority of
|
||||
the bitchat protocol stack compiles unmodified.
|
||||
|
||||
In `wear/build.gradle.kts`:
|
||||
|
||||
```kotlin
|
||||
sourceSets["main"].java.srcDir("../app/src/main/java") // with include filters (see below)
|
||||
```
|
||||
|
||||
**Include** (iteratively refined by fixing compile errors — the include list lives in
|
||||
`wear/build.gradle.kts` with comments):
|
||||
|
||||
- `protocol/**` — wire format, `BinaryProtocol`, `CompressionUtil`, `MessagePadding`
|
||||
- `model/**` — `BitchatMessage`, `BitchatFilePacket`, `FragmentPayload`, `NoiseEncrypted`,
|
||||
`IdentityAnnouncement`, `RoutedPacket`
|
||||
- `noise/**` — `NoiseSession`, `NoiseSessionManager`, `NoiseEncryptionService`,
|
||||
`NoiseChannelEncryption`, vendored pure-Java `noise/southernstorm/**`
|
||||
- `crypto/**` — `EncryptionService`
|
||||
- `identity/**` — `SecureIdentityStateManager`
|
||||
- `mesh/**` — BLE stack (`BluetoothConnectionManager`, GATT server/client managers, broadcaster,
|
||||
tracker, permission manager), `FragmentManager`, `SecurityManager`, `PacketProcessor`,
|
||||
`MessageHandler`, `PeerManager`, `StoreForwardManager`, `MeshTransport`, `MeshService`,
|
||||
`TransferProgressManager`, `PrivateMediaTransfer`, `PowerManager`
|
||||
- `services/AppStateStore.kt` — process-wide state store
|
||||
- `util/AppConstants.kt` — shared constants (GATT UUIDs, fragmentation sizes)
|
||||
- Small transitive deps the compiler reveals (known: `ui/debug/DebugSettingsManager.kt` is
|
||||
referenced from the mesh layer — include the file, not the package)
|
||||
|
||||
**Exclude**: `ui/**` (except forced single-file includes), `onboarding/**`, `nostr/**`, `net/**`,
|
||||
`geohash/**`, `wifi-aware/**`, `hotspot/**`, `features/voice/**`, `service/MeshForegroundService.kt`
|
||||
(wear gets its own service), `BitchatApplication.kt`, `MainActivity.kt`.
|
||||
|
||||
**Tests**: the app's own unit tests for shared packages (`protocol`, `noise`, `crypto`, `mesh`)
|
||||
are wired into the `:wear` test source set the same way (srcDir + includes), so shared behavior is
|
||||
continuously verified on both modules.
|
||||
|
||||
**Resources**: font files cannot be selectively shared via srcDir cleanly — copy the 4 Geist Mono
|
||||
font files (`app/src/main/res/font/geist_mono_*`) into `wear/src/main/res/font/`. Theme/palette/
|
||||
peer-color logic is re-created as wear-owned files mirroring `ui/theme/` values exactly.
|
||||
|
||||
## 3. Watch UX design
|
||||
|
||||
Wear Compose Material3 (round-screen safe by default):
|
||||
|
||||
- **Screens**: Chat (global timeline) → People (connected peers w/ RSSI, unread badges) →
|
||||
DM conversation. Edge-swipe back, `TimeText` scaffold, rotary crown scrolling.
|
||||
- **Visual identity** (mirrors `ui/theme/` exactly): black background `#000000`, green primary
|
||||
`#32D74B`, error `#FF453A`, orange accent for self/mentions, djb2-hash stable peer colors
|
||||
(`PeerColors.kt` algorithm), Geist Mono typography, `BitchatMotion` timing tokens
|
||||
(120/180/240 ms) for all animations.
|
||||
- **Input**: text field using the Pixel Watch Gboard IME, plus voice dictation via
|
||||
`RecognizerIntent`. Haptic feedback on incoming messages.
|
||||
- **Background**: wear-owned foreground service (type `connectedDevice`) keeps scan/advertise
|
||||
alive; shared `PowerManager` provides duty-cycling.
|
||||
|
||||
## 4. Hardware & test environment
|
||||
|
||||
- Pixel Watch connected via ADB (target device for all milestones; screencaps via
|
||||
`adb exec-out screencap`).
|
||||
- Two phones running the Android bitchat app, also on ADB, for interop testing (used heavily from
|
||||
M6; manual interop checks from M2 onward).
|
||||
- Design verification: at every UI milestone, take ADB screencaps of every screen and review them
|
||||
for round-screen clipping, element visibility, contrast, and touch-target size. A milestone does
|
||||
not pass until its screencap set is approved.
|
||||
|
||||
## 5. Risks & notes
|
||||
|
||||
- `BluetoothMeshService` (legacy monolith) vs `MeshCore` — prefer wiring the shared components
|
||||
directly (MeshCore-style composition) in the wear service.
|
||||
- `mesh/` references `ui/debug/DebugSettingsManager` — include that single file; do not pull in
|
||||
the debug UI sheet.
|
||||
- Wear BLE MTUs are small; the shared fragmentation layer (469-byte fragments) already handles
|
||||
this.
|
||||
- `EncryptedSharedPreferences` (androidx.security-crypto) works on Wear OS — identity persistence
|
||||
is reused as-is.
|
||||
- Watch has no camera/gallery: file transfer is **receive + display only** (confirmed decision).
|
||||
|
||||
---
|
||||
|
||||
## Milestones
|
||||
|
||||
### M0 — Scaffolding & plan document
|
||||
|
||||
- [x] Write this plan to `docs/wear-os-implementation-plan.md`
|
||||
- [x] Create `:wear` module: `wear/build.gradle.kts`, manifest
|
||||
(`<uses-feature android:name="android.hardware.type.watch"/>`, standalone, BT permissions,
|
||||
**no INTERNET/location**), `MainActivity` with hello-world screen using the ported theme
|
||||
- [x] Add Wear Compose dependencies to `gradle/libs.versions.toml`; `include(":wear")` in
|
||||
`settings.gradle.kts`
|
||||
- [x] Build, install, and launch on the physical Pixel Watch via ADB; take first screencap
|
||||
|
||||
**Success criteria**: `./gradlew :wear:assembleDebug` green; app launches on the watch;
|
||||
`git diff --name-only` shows no changes under `app/src/`.
|
||||
**Result**: PASSED — installed on Pixel Watch 3 (serial 4C201JEAYW0020), launch screencap shows
|
||||
"bitchat" wordmark (green `#32D74B`, Geist Mono, black background) correctly centered on the
|
||||
round display. No `app/src/` changes.
|
||||
|
||||
---
|
||||
|
||||
### M1 — Shared core compiles on Wear
|
||||
|
||||
- [ ] Configure srcDir include list in `wear/build.gradle.kts`; resolve transitive dependencies by
|
||||
extending includes (never by copying Kotlin sources)
|
||||
- [ ] Copy Geist Mono fonts; create wear theme/palette/peer-color files mirroring `ui/theme/`
|
||||
- [ ] Wire shared unit tests (`protocol`, `noise`, `crypto`, `mesh`) into `:wear` test source set
|
||||
- [ ] `./gradlew :app:test :wear:test` green
|
||||
|
||||
**Success criteria**: the entire shared stack (protocol, noise, crypto, identity, mesh, model,
|
||||
AppStateStore) compiles into `:wear`; both modules' unit tests pass; `app/src/` untouched.
|
||||
|
||||
---
|
||||
|
||||
### M2 — BLE transport & background service on watch
|
||||
|
||||
- [ ] Wear onboarding flow: Bluetooth-enable check + runtime permission requests
|
||||
(`BLUETOOTH_SCAN/CONNECT/ADVERTISE`), watch-styled screens
|
||||
- [ ] `WearMeshService` foreground service (type `connectedDevice`); wire shared
|
||||
`BluetoothConnectionManager` + mesh components; start scanning + advertising
|
||||
- [ ] Internal debug screen: discovered peers with RSSI (temporary, replaced by real UI in M3/M4)
|
||||
- [ ] Manual interop check: watch and one phone mutually discover
|
||||
|
||||
**Success criteria**: the phone's bitchat app lists the watch as a connected peer and vice versa
|
||||
(logcat + screencap evidence); mesh survives the screen turning off (ambient mode) for 5 minutes.
|
||||
|
||||
---
|
||||
|
||||
### M3 — Global chat
|
||||
|
||||
- [ ] Nickname onboarding; identity announcement over the mesh
|
||||
- [ ] Send/receive/relay public `BitchatMessage`s (relay/TTL comes free from shared mesh code)
|
||||
- [ ] Chat timeline UI (`ScalingLazyColumn`, message bubbles per bitchat style, peer colors,
|
||||
timestamps) + composer (IME + `RecognizerIntent` dictation) + incoming-message haptics
|
||||
- [ ] Design check: ADB screencaps of onboarding, chat (empty/populated), composer; review for
|
||||
round-screen clipping/visibility/contrast
|
||||
|
||||
**Success criteria**: two-way public chat between watch and phone; messages the watch relays reach
|
||||
a second phone that is only connected through the first (relay proof); screencap set approved.
|
||||
|
||||
---
|
||||
|
||||
### M4 — Noise DMs & people screen
|
||||
|
||||
- [ ] People screen: connected peers, nicknames, RSSI, unread-DM badges
|
||||
- [ ] Tap peer → Noise XX handshake (shared `EncryptionService`/`NoiseSessionManager`) → DM thread
|
||||
- [ ] DM conversation UI; unread counters; delivery/read receipts if supported by shared code
|
||||
- [ ] Identity persistence (`EncryptedSharedPreferences`); stale-session detection & automatic
|
||||
re-handshake after watch app restart
|
||||
- [ ] Design check: screencaps of people screen, handshake state, DM thread
|
||||
|
||||
**Success criteria**: encrypted DM round trip with the phone; DMs survive a watch app restart
|
||||
(session recovery); screencap set approved.
|
||||
|
||||
---
|
||||
|
||||
### M5 — File/image receive & display — **DEFERRED**
|
||||
|
||||
> Deferred to a later day (after M7). Milestones M6 and M7 do not depend on M5 and proceed
|
||||
> without it. The mesh_lab `file` scenario for the watch is skipped until M5 is un-deferred.
|
||||
|
||||
- [ ] Receive broadcast files (`MessageType.FILE_TRANSFER`, `BitchatFilePacket` TLV decode,
|
||||
fragment reassembly — all shared)
|
||||
- [ ] Receive Noise-encrypted private files (`NoisePayloadType.FILE_TRANSFER`)
|
||||
- [ ] Inline image rendering in chat timelines; full-screen image viewer (pinch/crown zoom);
|
||||
non-image files saved with a way to open/share them
|
||||
- [ ] Transfer progress indicator; respect shared fragment/size caps
|
||||
- [ ] Design check: screencaps of inline image, full-screen viewer, transfer progress
|
||||
|
||||
**Success criteria**: phone→watch image renders inline in both global chat and DM; SHA-256 of
|
||||
received file matches sender; screencap set approved. (Sending files from the watch is out of
|
||||
scope.)
|
||||
|
||||
---
|
||||
|
||||
### M6 — ADB test hook & mesh_lab interop
|
||||
|
||||
- [ ] Wear debug-only `TestHookReceiver` (`wear/src/debug/`) mirroring the phone's command set:
|
||||
`ping`, `start`, `stop`, `whoami`, `set_nickname`, `scan`, `peers`, `connect`, `handshake`,
|
||||
`session`, `announce`, `broadcast_msg`, `dm_send`, `dm_recv`, `msg_recv`, `file_recv`, `state`,
|
||||
`clear_results` — broadcast action `com.bitchat.watch.TEST_HOOK`, same JSON-result-file protocol
|
||||
- [ ] Extend `tools/release_gate/mesh_lab.py`: `--serial-watch` argument and phone↔watch scenarios
|
||||
(`dm`, `broadcast`, `session_recovery`, `all`; `file` excluded while M5 is deferred), reusing
|
||||
the existing `Device`/`cmd` machinery
|
||||
- [ ] Run full scenario suite phone↔watch; store evidence JSON
|
||||
|
||||
**Success criteria**:
|
||||
`python3 tools/release_gate/mesh_lab.py scenario all --serial-a <phone> --serial-watch <watch>`
|
||||
exits 0 with evidence files; no manual intervention.
|
||||
|
||||
---
|
||||
|
||||
### M7 — Polish & final design pass
|
||||
|
||||
- [ ] Animations/transitions per `BitchatMotion` tokens; message-appear animations; screen
|
||||
transitions; rotary scroll feel; splash screen & app icon (bitchat wordmark style)
|
||||
- [ ] Power/battery: verify shared `PowerManager` duty-cycling behaves on Wear; ambient-mode
|
||||
behavior; memory audit (fragment/image caps)
|
||||
- [ ] Full screencap design review of every screen and state; fix all findings
|
||||
- [ ] Update this document: all milestones `done`; add a short "how to build/run/test" section
|
||||
|
||||
**Success criteria**: all milestones marked `done`; interop suite green; final screencap set
|
||||
approved; a fresh agent can build, install, and test the watch app from this document alone.
|
||||
@ -21,6 +21,10 @@ navigation-compose = "2.9.8"
|
||||
# Accompanist
|
||||
accompanist-permissions = "0.37.3"
|
||||
|
||||
# Wear OS
|
||||
wear-compose = "1.6.2"
|
||||
wear-tooling-preview = "1.0.0"
|
||||
|
||||
# Cryptography
|
||||
bouncycastle = "1.85"
|
||||
tink-android = "1.23.0"
|
||||
@ -95,6 +99,11 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose
|
||||
# Accompanist
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist-permissions" }
|
||||
|
||||
# Wear OS
|
||||
androidx-wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wear-compose" }
|
||||
androidx-wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wear-compose" }
|
||||
androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wear-tooling-preview" }
|
||||
|
||||
# Cryptography
|
||||
bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
|
||||
google-tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tink-android" }
|
||||
|
||||
@ -17,4 +17,5 @@ dependencyResolutionManagement {
|
||||
|
||||
rootProject.name = "bitchat-android"
|
||||
include(":app")
|
||||
include(":wear")
|
||||
// Using published Arti AAR; local module not included
|
||||
|
||||
89
wear/build.gradle.kts
Normal file
89
wear/build.gradle.kts
Normal file
@ -0,0 +1,89 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.bitchat.watch"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.bitchat.watch"
|
||||
minSdk = 30 // Wear OS 3 (Pixel Watch 1); BLE APIs match the phone app's usage
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
lint {
|
||||
abortOnError = false
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
// Wear Compose
|
||||
implementation(libs.androidx.wear.compose.foundation)
|
||||
implementation(libs.androidx.wear.compose.material3)
|
||||
implementation(libs.androidx.wear.tooling.preview)
|
||||
|
||||
// Lifecycle
|
||||
implementation(libs.bundles.lifecycle)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
|
||||
// Coroutines
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
// Cryptography (shared Noise/encryption stack)
|
||||
implementation(libs.bouncycastle.bcprov)
|
||||
|
||||
// JSON (BitchatMessage model)
|
||||
implementation(libs.gson)
|
||||
|
||||
// Security preferences (Noise identity persistence)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.bundles.testing)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
}
|
||||
43
wear/src/main/AndroidManifest.xml
Normal file
43
wear/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="android.hardware.type.watch" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth_le"
|
||||
android:required="true" />
|
||||
|
||||
<!-- Bluetooth mesh only. Deliberately NO android.permission.INTERNET and no location. -->
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH_SCAN"
|
||||
android:usesPermissionFlags="neverForLocation"
|
||||
tools:targetApi="s" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.DeviceDefault">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.wearable.standalone"
|
||||
android:value="true" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:taskAffinity="">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
52
wear/src/main/java/com/bitchat/watch/MainActivity.kt
Normal file
52
wear/src/main/java/com/bitchat/watch/MainActivity.kt
Normal file
@ -0,0 +1,52 @@
|
||||
package com.bitchat.watch
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.wear.compose.material3.MaterialTheme
|
||||
import androidx.wear.compose.material3.Text
|
||||
import com.bitchat.watch.ui.theme.BitchatWearTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
BitchatWearTheme {
|
||||
PlaceholderScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaceholderScreen() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "bitchat",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "mesh initializing",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.bitchat.watch.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Immutable
|
||||
data class BitchatPalette(
|
||||
val inputOutline: Color,
|
||||
val inputOutlineFocused: Color,
|
||||
val inputSurface: Color,
|
||||
val inputSurfaceFocused: Color,
|
||||
val inputButton: Color,
|
||||
val textTertiary: Color,
|
||||
val accentOrange: Color,
|
||||
val accentPurple: Color,
|
||||
val peerColors: PeerColorStyle,
|
||||
)
|
||||
|
||||
val DarkBitchatPalette = BitchatPalette(
|
||||
inputOutline = Color(0xFF333635),
|
||||
inputOutlineFocused = Color(0xFF5A605D),
|
||||
inputSurface = Color(0xFF0B0B0B),
|
||||
inputSurfaceFocused = Color(0xFF151515),
|
||||
inputButton = Color(0xFF1E1E1E),
|
||||
textTertiary = Color(0xFF6B776B),
|
||||
accentOrange = Color(0xFFFF9F0A),
|
||||
accentPurple = Color(0xFFBF5AF2),
|
||||
peerColors = PeerColorStyle.Dark,
|
||||
)
|
||||
|
||||
val LocalBitchatPalette = staticCompositionLocalOf { DarkBitchatPalette }
|
||||
|
||||
object BitchatMotion {
|
||||
const val QUICK_MS = 120
|
||||
const val STANDARD_MS = 180
|
||||
const val EMPHASIZED_MS = 240
|
||||
}
|
||||
35
wear/src/main/java/com/bitchat/watch/ui/theme/PeerColors.kt
Normal file
35
wear/src/main/java/com/bitchat/watch/ui/theme/PeerColors.kt
Normal file
@ -0,0 +1,35 @@
|
||||
package com.bitchat.watch.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlin.math.abs
|
||||
|
||||
@Immutable
|
||||
data class PeerColorStyle(
|
||||
val saturation: Float,
|
||||
val value: Float,
|
||||
) {
|
||||
companion object {
|
||||
val Dark = PeerColorStyle(saturation = 0.55f, value = 0.82f)
|
||||
}
|
||||
}
|
||||
|
||||
fun colorForPeer(stableKey: String, palette: BitchatPalette): Color {
|
||||
var hash = 5381UL
|
||||
for (byte in stableKey.toByteArray()) {
|
||||
hash = ((hash shl 5) + hash) + byte.toUByte().toULong()
|
||||
}
|
||||
|
||||
var hue = (hash % 360UL).toDouble() / 360.0
|
||||
val orange = 30.0 / 360.0
|
||||
if (abs(hue - orange) < 0.05) {
|
||||
hue = (hue + 0.12) % 1.0
|
||||
}
|
||||
|
||||
val style = palette.peerColors
|
||||
return Color.hsv(
|
||||
hue = (hue * 360).toFloat(),
|
||||
saturation = style.saturation,
|
||||
value = style.value
|
||||
)
|
||||
}
|
||||
42
wear/src/main/java/com/bitchat/watch/ui/theme/Theme.kt
Normal file
42
wear/src/main/java/com/bitchat/watch/ui/theme/Theme.kt
Normal file
@ -0,0 +1,42 @@
|
||||
package com.bitchat.watch.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.wear.compose.material3.ColorScheme
|
||||
import androidx.wear.compose.material3.MaterialTheme
|
||||
|
||||
val BitchatWearColorScheme = ColorScheme(
|
||||
primary = Color(0xFF32D74B),
|
||||
onPrimary = Color.Black,
|
||||
primaryContainer = Color(0xFF163D1D),
|
||||
onPrimaryContainer = Color(0xFFB8F5C1),
|
||||
secondary = Color(0xFF0A84FF),
|
||||
onSecondary = Color.Black,
|
||||
secondaryContainer = Color(0xFF082E54),
|
||||
onSecondaryContainer = Color(0xFFC2E0FF),
|
||||
tertiary = Color(0xFFFF9F0A),
|
||||
onTertiary = Color.Black,
|
||||
background = Color(0xFF000000),
|
||||
onBackground = Color(0xFFF5F5F5),
|
||||
surfaceContainer = Color(0xFF0E150E),
|
||||
surfaceContainerLow = Color(0xFF0B0B0B),
|
||||
surfaceContainerHigh = Color(0xFF182118),
|
||||
onSurface = Color(0xFFF5F5F5),
|
||||
onSurfaceVariant = Color(0xFF9AA69A),
|
||||
outline = Color(0xFF2A3A2A),
|
||||
outlineVariant = Color(0xFF1C271C),
|
||||
error = Color(0xFFFF453A),
|
||||
onError = Color.Black,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BitchatWearTheme(content: @Composable () -> Unit) {
|
||||
CompositionLocalProvider(LocalBitchatPalette provides DarkBitchatPalette) {
|
||||
MaterialTheme(
|
||||
colorScheme = BitchatWearColorScheme,
|
||||
typography = BitchatWearTypography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
43
wear/src/main/java/com/bitchat/watch/ui/theme/Typography.kt
Normal file
43
wear/src/main/java/com/bitchat/watch/ui/theme/Typography.kt
Normal file
@ -0,0 +1,43 @@
|
||||
package com.bitchat.watch.ui.theme
|
||||
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.wear.compose.material3.Typography
|
||||
import com.bitchat.watch.R
|
||||
|
||||
val BitchatFontFamily = FontFamily(
|
||||
Font(R.font.geist_mono_regular, FontWeight.Normal),
|
||||
Font(R.font.geist_mono_medium, FontWeight.Medium),
|
||||
Font(R.font.geist_mono_semibold, FontWeight.SemiBold),
|
||||
Font(R.font.geist_mono_bold, FontWeight.Bold),
|
||||
)
|
||||
|
||||
val BitchatWearTypography = Typography(
|
||||
defaultFontFamily = BitchatFontFamily,
|
||||
)
|
||||
|
||||
object ChatVisualTokens {
|
||||
val MessageBodyStyle = TextStyle(
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 17.sp,
|
||||
)
|
||||
|
||||
val SenderStyle = TextStyle(
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 15.sp,
|
||||
)
|
||||
|
||||
val SystemActionStyle = TextStyle(
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 14.sp,
|
||||
)
|
||||
}
|
||||
16
wear/src/main/res/drawable/ic_launcher.xml
Normal file
16
wear/src/main/res/drawable/ic_launcher.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M0,0h48v48h-48z" />
|
||||
<path
|
||||
android:fillColor="#32D74B"
|
||||
android:pathData="M10,14l10,10l-10,10v-6l5,-4l-5,-4z" />
|
||||
<path
|
||||
android:fillColor="#32D74B"
|
||||
android:pathData="M22,32h16v4h-16z" />
|
||||
</vector>
|
||||
BIN
wear/src/main/res/font/geist_mono_bold.ttf
Normal file
BIN
wear/src/main/res/font/geist_mono_bold.ttf
Normal file
Binary file not shown.
BIN
wear/src/main/res/font/geist_mono_medium.ttf
Normal file
BIN
wear/src/main/res/font/geist_mono_medium.ttf
Normal file
Binary file not shown.
BIN
wear/src/main/res/font/geist_mono_regular.ttf
Normal file
BIN
wear/src/main/res/font/geist_mono_regular.ttf
Normal file
Binary file not shown.
BIN
wear/src/main/res/font/geist_mono_semibold.ttf
Normal file
BIN
wear/src/main/res/font/geist_mono_semibold.ttf
Normal file
Binary file not shown.
4
wear/src/main/res/values/strings.xml
Normal file
4
wear/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">bitchat</string>
|
||||
</resources>
|
||||
Loading…
x
Reference in New Issue
Block a user