chore(rebrand): migrate Xune-HD to Dorado-HD under project-dorado

- Move package com.heretek.xunehd -> com.heretek.dorado_hd (namespace + applicationId)
- Full symbol rebrand: Xune* -> Dorado* across all sources and filenames
- Storage: xune.db -> dorado_hd.db; DataStore xune_settings -> dorado_hd_settings
- Theme.XuneHd -> Theme.DoradoHd; resources xune_* -> dorado_*
- Repoint remotes, badges, CI artifact, docs to project-dorado/dorado-hd
- Add CODEOWNERS + issue/PR templates
- Preserve Microsoft Zune HD canon references and original copyright attribution

Refs: Heretek-AI/xune-HD -> project-dorado/dorado-hd
This commit is contained in:
John Smith
2026-09-10 12:59:00 -04:00
parent 9c238ad563
commit 8b7f4c50be
79 changed files with 1342 additions and 1271 deletions
+2
View File
@@ -0,0 +1,2 @@
# Default owners for everything in this repository.
* @BillyOutlast
+26
View File
@@ -0,0 +1,26 @@
---
name: Bug report
about: Report a reproducible bug in Dorado-HD
title: "[Bug] "
labels: bug
assignees: ''
---
## Description
A clear and concise description of what the bug is.
## Steps to reproduce
1.
2.
3.
## Expected behavior
What you expected to happen.
## Environment
- Android version / device:
- Dorado-HD version (`0.1.0`):
- Build (debug APK / built from source):
## Logs / screenshots
Paste `adb logcat` output or attach screenshots.
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea or a Zune HD canon/parity gap for Dorado-HD
title: "[Feature] "
labels: enhancement
assignees: ''
---
## Problem
What problem does this solve, and which canon section or parity gap does it relate to?
## Proposed solution
A clear and concise description of what you want to happen.
## Canon reference
Link the relevant section in `docs/zune-hd-ui-canon.md`, or explain the citation.
## Additional context
Screenshots, references, or links.
+17
View File
@@ -0,0 +1,17 @@
# Pull Request
## Summary
Describe the change and the motivation.
## Related issues
Closes #
## Verification
- [ ] `./gradlew test` passes (unit tests + design-invariant audit)
- [ ] `./gradlew assembleDebug` succeeds
- [ ] The change respects the canon in `docs/zune-hd-ui-canon.md` (or updates the canon with a citation first)
## Checklist
- [ ] No plaintext secrets, credentials, or tokens introduced
- [ ] No new dangling references to the old `Heretek-AI` organization
- [ ] Documentation updated where behavior or canonical spec changed
+1 -1
View File
@@ -29,5 +29,5 @@ jobs:
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: xune-hd-debug
name: dorado-hd-debug
path: app/build/outputs/apk/debug/app-debug.apk
+5 -5
View File
@@ -1,7 +1,7 @@
# AGENTS.md — Xune-HD repository rules
# AGENTS.md — Dorado-HD repository rules
Xune-HD is the Zune HD on-device UI, reborn as an Android music player.
Sister project of Not-Zune (the Zune desktop re-implementation).
Dorado-HD is the Zune HD on-device UI, reborn as an Android music player.
Sister project of Dorado (the Zune desktop re-implementation).
## The canon is law
@@ -14,7 +14,7 @@ the canon first (with a source citation), then the code.
1. **Zero corner radius.** Never write `RoundedCornerShape` or set any nonzero
corner radius on UI. Squares only, like the device.
2. **No Material chrome leakage.** Screens compose our tokens
(`LocalXuneColors`, `XuneTokens`, `Selawik`). Material primitives (Icon,
(`LocalDoradoColors`, `DoradoTokens`, `Selawik`). Material primitives (Icon,
IconButton, TextField) are permitted as behaviorless shells — never for
their styling.
3. **Typography is navigation.** New navigation is text-first (the Zune HD had
@@ -23,7 +23,7 @@ the canon first (with a source citation), then the code.
5. **Back = the cropped header.** Detail screens use `CroppedHeader` /
`DetailScaffold`; Now Playing uses the explicit back arrow. Do not add
chrome back buttons elsewhere.
6. **Motion decelerates.** Use `XuneMotion` easings/durations. No springs in
6. **Motion decelerates.** Use `DoradoMotion` easings/durations. No springs in
navigation.
## Toolchain notes (AGP 9)
+4
View File
@@ -2,6 +2,10 @@ MIT License
Copyright (c) 2026 Heretek AI and contributors
Dorado-HD (formerly Xune-HD) is maintained by the project-dorado organization.
Original authorship and copyright are retained by Heretek AI and the Xune-HD
contributors.
Portions of the playback and library-scan logic are derived from MedTune by
Rizqi Tri Antomi, used under the MIT License (see NOTICE.md).
+7 -4
View File
@@ -1,10 +1,13 @@
# NOTICE — third-party materials in Xune-HD
# NOTICE — third-party materials in Dorado-HD
Dorado-HD is the renamed continuation of the **Xune-HD** project, now
maintained under the project-dorado organization.
## Code
- **MedTune** (https://github.com/rizqitri3at/MedTune) by Rizqi Tri Antomi —
MIT License. Library-scan concepts and the starting project structure are
derived from MedTune. Modifications are extensive and specific to Xune-HD.
- **Not-Zune** (https://github.com/Heretek-AI/not-zune) — MIT License. Design
derived from MedTune. Modifications are extensive and specific to Dorado-HD.
- **Dorado** (https://github.com/project-dorado/dorado) — MIT License. Design
tokens and the design-invariant audit approach are ported from its
zune-design-system skill.
- **MusicIn2001** (https://github.com/BillyOutlast/MusicIn2001) — Research-Only
@@ -19,7 +22,7 @@
## Trademarks
- Zune, Zegoe, Zune HD and Microsoft are trademarks of Microsoft Corporation.
Xune-HD is an independent, non-affiliated homage. No Microsoft software,
Dorado-HD is an independent, non-affiliated homage. No Microsoft software,
fonts, firmware, or artwork are bundled.
## Concept sources (no code)
+18 -18
View File
@@ -1,9 +1,9 @@
# Xune-HD
# Dorado-HD
<div align="center">
# 📱 Xune-HD
**The Zune HD, reborn as an Android music player — sister app to Not-Zune**
# 📱 Dorado-HD
**The Zune HD, reborn as an Android music player — sister app to Dorado**
[![Android](https://img.shields.io/badge/Platform-Android%209%2B-3DDC84)]()
[![UI](https://img.shields.io/badge/UI-Jetpack%20Compose-4285F4)]()
@@ -14,15 +14,15 @@
---
Xune-HD is a faithful re-creation of the **Microsoft Zune HD on-device
Dorado-HD is a faithful re-creation of the **Microsoft Zune HD on-device
interface** (2009) for modern Android phones: white-on-black typography-first
Metro design, a text menu cropped at the screen edge, Quickplay parked
"left and rear", crossbar navigation, swipe-to-skip Now Playing floating
over artist photography, and the tri-state heart rating.
It is the **device-side sibling** of [Not-Zune](https://github.com/Heretek-AI/not-zune)
It is the **device-side sibling** of [Dorado](https://github.com/project-dorado/dorado)
(the Zune *desktop* re-implementation). Just as the Zune HD was the device and
Zune 4.8 was the desktop, Xune-HD is the phone and Not-Zune is the desktop.
Zune 4.8 was the desktop, Dorado-HD is the phone and Dorado is the desktop.
## ✨ What's implemented
@@ -37,7 +37,7 @@ design-invariant scanner.
- **Home menu** with the full canonical 9 entries — `music · videos ·
pictures · radio · marketplace · social · podcasts · internet ·
settings` — lowercase, oversized, right-edge cropped, **kinetically
scrollable**. The "xune hd" watermark sits at the bottom-left at the
scrollable**. The "dorado hd" watermark sits at the bottom-left at the
canon's `0.08` textWatermark opacity.
- **Quickplay** slides out as a 3D-parallax "left and rear" panel using
`rotationY` + `cameraDistance` (the device's literal visual depth). Inside:
@@ -51,7 +51,7 @@ design-invariant scanner.
Every collection screen now uses a horizontal `CrossbarBar` pivot strip
with right-edge-cropped labels. The crossbar pivot transition rides the
spec'd 380 ms deceleration via `XuneMotion.PIVOT_SLIDE_MS`.
spec'd 380 ms deceleration via `DoradoMotion.PIVOT_SLIDE_MS`.
- **Music** — `albums · artists · playlists · songs · genres` (the real
device order).
@@ -100,7 +100,7 @@ spec'd 380 ms deceleration via `XuneMotion.PIVOT_SLIDE_MS`.
### Mini-app platform (canon §8 — 29 apps, behavioral re-implementation)
The `XuneApps.all` registry is a `by lazy` build so it's populated before
The `DoradoApps.all` registry is a `by lazy` build so it's populated before
the first UI lookup (including Robolectric tests).
#### Utilities (12)
@@ -157,7 +157,7 @@ mini-app platform.
## 🔒 Licensing posture
- **Not-Zune** (MIT) — design tokens, concepts. Thank you.
- **Dorado** (MIT) — design tokens, concepts. Thank you.
- **MedTune** (MIT) — starting skeleton; see NOTICE.md.
- **MusicIn2001** — Research-Only license: used strictly as a behavioral
specification. **No code was copied.**
@@ -166,14 +166,14 @@ mini-app platform.
- The official Zune HD `.zcp` packages (review/Zune HD Apps/) are the
marketplace archive; their encrypted payloads are **never** bundled, and
only the manifest metadata (title, GUID, description) is read.
- Zune, Zegoe and the Zune HD are Microsoft trademarks. Xune-HD is an
- Zune, Zegoe and the Zune HD are Microsoft trademarks. Dorado-HD is an
independent homage; nothing Microsoft is bundled.
---
## 📊 Parity summary
| Area | Device has | Xune-HD has | Notes |
| Area | Device has | Dorado-HD has | Notes |
|---|---|---|---|
| Home menu (9 entries, kinetic, edge-crop) | ✅ | ✅ | `LazyColumn` via `KineticList`; right-edge-cropped via `EdgeCropText` |
| Quickplay (parallax reveal) | ✅ | ✅ | 3D parallax `rotationY` + `cameraDistance` |
@@ -181,7 +181,7 @@ mini-app platform.
| Lock shade (wallpaper behind) | ✅ | ✅ | Real `WallpaperManager.getDrawable()` |
| Now Playing (scrub, transport, screensaver) | ✅ | ✅ | Scrubber + transport + screensaver; tap combines dismiss + overlay |
| Artist bio / photos / related | ✅ | ✅ | Wikipedia REST + MusicBrainz; related pivot has honest empty-state |
| Music crossbar (5 pivots) | ✅ | ✅ | 380 ms deceleration per `XuneMotion.PIVOT_SLIDE_MS` |
| Music crossbar (5 pivots) | ✅ | ✅ | 380 ms deceleration per `DoradoMotion.PIVOT_SLIDE_MS` |
| Album art palette wash | ✅ | ✅ | |
| Tri-state heart rating | ✅ | ✅ | Persisted in Room |
| Videos (with player) | ✅ | ✅ | MediaStore + ExoPlayer surface view + scrubber + transport |
@@ -194,7 +194,7 @@ mini-app platform.
| Crossbar pivot nav | ✅ | ✅ | All collection screens have horizontal pivot pivots |
| Screensaver / now-playing art | ✅ | ✅ | |
| Widget (home-screen) | ✅ | ❌ | Not yet — see roadmap |
| Wi-Fi sync with Not-Zune (sibling project) | ✅ | ❌ | Not yet — see roadmap |
| Wi-Fi sync with Dorado (sibling project) | ✅ | ❌ | Not yet — see roadmap |
| USB MTPZ sync to physical Zune HD | ✅ | ❌ | Stretch goal — see roadmap |
### Design-invariant scanner (enforced by `DesignInvariantTest`)
@@ -206,9 +206,9 @@ mini-app platform.
| No raw hex color literals | `ui/`, `design/components/`, `ui/apps/` | regex `Color\(0x[0-9A-Fa-f]{8}\)` |
| No named `Color.Black/White/Red/…` | `ui/`, `design/components/`, `ui/apps/` | regex `\bColor\.(Black\|White\|Red\|…)\b` (games + `PianoDrum.kt` allowlisted) |
| 62-entry catalog invariant | `OfficialCatalog.all.size == 62` | unit test |
| `XuneApps` registry resolves every `installedId` | unit test | |
| `DoradoApps` registry resolves every `installedId` | unit test | |
### Token table (enforced via `XuneTokens` consumption)
### Token table (enforced via `DoradoTokens` consumption)
| Token | Value | Used by |
|---|---|---|
@@ -280,8 +280,8 @@ The release build minifies with R8; keep rules in `app/proguard-rules.pro`.
### Future (post-M7)
- **Wi-Fi sync with Not-Zune** — the phone enrolls as a Zune-HD-like
device in Not-Zune's sync engine (JSON manifest, ZMDB-style database).
- **Wi-Fi sync with Dorado** — the phone enrolls as a Zune-HD-like
device in Dorado's sync engine (JSON manifest, ZMDB-style database).
- **Real MTP/MTPZ sync to a physical Zune HD over USB host** — stretch.
- **Home-screen widget** — Zune-style Now Playing with transport.
- **Live radio cache** (M6) is contingent on a real-time streaming
+2 -2
View File
@@ -5,11 +5,11 @@ plugins {
}
android {
namespace = "com.heretek.xunehd"
namespace = "com.heretek.dorado_hd"
compileSdk = 37
defaultConfig {
applicationId = "com.heretek.xunehd"
applicationId = "com.heretek.dorado_hd"
minSdk = 26
targetSdk = 37
versionCode = 1
+1 -1
View File
@@ -2,5 +2,5 @@
-keep class androidx.media3.** { *; }
-keep interface androidx.media3.** { *; }
# Room entities are accessed reflectively by generated code.
-keep class com.heretek.xunehd.data.db.** { *; }
-keep class com.heretek.dorado_hd.data.db.** { *; }
-dontwarn org.checkerframework.**
+4 -4
View File
@@ -13,12 +13,12 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:name=".XuneApp"
android:name=".DoradoApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.XuneHd"
android:theme="@style/Theme.DoradoHd"
tools:targetApi="tiramisu">
<activity
@@ -26,7 +26,7 @@
android:exported="true"
android:launchMode="singleTask"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:theme="@style/Theme.XuneHd">
android:theme="@style/Theme.DoradoHd">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -34,7 +34,7 @@
</activity>
<service
android:name=".media.XunePlaybackService"
android:name=".media.DoradoPlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
@@ -1,4 +1,4 @@
package com.heretek.xunehd
package com.heretek.dorado_hd
import android.app.Application
import android.database.ContentObserver
@@ -6,13 +6,13 @@ import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import com.heretek.xunehd.data.db.XuneDatabase
import com.heretek.xunehd.data.repo.LibraryRepository
import com.heretek.xunehd.data.repo.QuickplayRepository
import com.heretek.xunehd.data.repo.SettingsRepository
import com.heretek.xunehd.media.PlaybackController
import com.heretek.xunehd.net.ArtistImageService
import com.heretek.xunehd.ui.nav.XuneNav
import com.heretek.dorado_hd.data.db.DoradoDatabase
import com.heretek.dorado_hd.data.repo.LibraryRepository
import com.heretek.dorado_hd.data.repo.QuickplayRepository
import com.heretek.dorado_hd.data.repo.SettingsRepository
import com.heretek.dorado_hd.media.PlaybackController
import com.heretek.dorado_hd.net.ArtistImageService
import com.heretek.dorado_hd.ui.nav.DoradoNav
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -26,48 +26,48 @@ import kotlinx.coroutines.launch
* The application graph. Deliberately small and hand-wired: no DI framework,
* mirroring the lean structure of the Zune-era codebase we emulate.
*/
class XuneGraph(
class DoradoGraph(
val library: LibraryRepository,
val quickplay: QuickplayRepository,
val settings: SettingsRepository,
val settingsFlow: kotlinx.coroutines.flow.Flow<com.heretek.xunehd.data.repo.XuneSettings>,
val settingsFlow: kotlinx.coroutines.flow.Flow<com.heretek.dorado_hd.data.repo.DoradoSettings>,
val controller: PlaybackController,
val nav: XuneNav,
val nav: DoradoNav,
val artistImages: ArtistImageService,
val artistBios: com.heretek.xunehd.net.ArtistBioService,
val notes: com.heretek.xunehd.data.repo.NotesRepository,
val calendar: com.heretek.xunehd.data.repo.CalendarRepository,
val alarms: com.heretek.xunehd.data.repo.AlarmRepository,
val radio: com.heretek.xunehd.data.repo.RadioRepository,
val podcasts: com.heretek.xunehd.data.repo.PodcastRepository,
val games: com.heretek.xunehd.data.repo.GameRepository,
val artistBios: com.heretek.dorado_hd.net.ArtistBioService,
val notes: com.heretek.dorado_hd.data.repo.NotesRepository,
val calendar: com.heretek.dorado_hd.data.repo.CalendarRepository,
val alarms: com.heretek.dorado_hd.data.repo.AlarmRepository,
val radio: com.heretek.dorado_hd.data.repo.RadioRepository,
val podcasts: com.heretek.dorado_hd.data.repo.PodcastRepository,
val games: com.heretek.dorado_hd.data.repo.GameRepository,
)
class XuneApp : Application() {
class DoradoApp : Application() {
lateinit var graph: XuneGraph
lateinit var graph: DoradoGraph
private set
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
override fun onCreate() {
super.onCreate()
val db = XuneDatabase.get(this)
val db = DoradoDatabase.get(this)
val library = LibraryRepository(this, db)
val quickplay = QuickplayRepository(db)
val settings = SettingsRepository(this)
val controller = PlaybackController(this, library, quickplay)
val nav = XuneNav()
val nav = DoradoNav()
val artistImages = ArtistImageService(this, db, settings)
val artistBios = com.heretek.xunehd.net.ArtistBioService(this)
val notes = com.heretek.xunehd.data.repo.NotesRepository(db)
val calendar = com.heretek.xunehd.data.repo.CalendarRepository(db)
val alarms = com.heretek.xunehd.data.repo.AlarmRepository(db)
val radio = com.heretek.xunehd.data.repo.RadioRepository(db)
val podcasts = com.heretek.xunehd.data.repo.PodcastRepository(db)
val games = com.heretek.xunehd.data.repo.GameRepository(db)
val artistBios = com.heretek.dorado_hd.net.ArtistBioService(this)
val notes = com.heretek.dorado_hd.data.repo.NotesRepository(db)
val calendar = com.heretek.dorado_hd.data.repo.CalendarRepository(db)
val alarms = com.heretek.dorado_hd.data.repo.AlarmRepository(db)
val radio = com.heretek.dorado_hd.data.repo.RadioRepository(db)
val podcasts = com.heretek.dorado_hd.data.repo.PodcastRepository(db)
val games = com.heretek.dorado_hd.data.repo.GameRepository(db)
graph = XuneGraph(
graph = DoradoGraph(
library, quickplay, settings, settings.settings, controller, nav,
artistImages, artistBios, notes, calendar, alarms, radio, podcasts, games,
)
@@ -120,8 +120,8 @@ class XuneApp : Application() {
// Seed radio station defaults and register mini-apps (canon §8).
appScope.launch { radio.seedDefaultsIfEmpty() }
// Touch XuneApps so its lazy registry is materialized before any UI
// Touch DoradoApps so its lazy registry is materialized before any UI
// looks it up; equivalent to the prior eager assignment.
com.heretek.xunehd.ui.apps.XuneApps.all.size
com.heretek.dorado_hd.ui.apps.DoradoApps.all.size
}
}
@@ -1,22 +1,22 @@
package com.heretek.xunehd
package com.heretek.dorado_hd
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.CompositionLocalProvider
import com.heretek.xunehd.ui.XuneRoot
import com.heretek.dorado_hd.ui.DoradoRoot
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val graph = (application as XuneApp).graph
val graph = (application as DoradoApp).graph
setContent {
CompositionLocalProvider(
com.heretek.xunehd.ui.LocalXuneGraph provides graph,
com.heretek.dorado_hd.ui.LocalDoradoGraph provides graph,
) {
XuneRoot()
DoradoRoot()
}
}
}
@@ -1,13 +1,13 @@
package com.heretek.xunehd.data.db
package com.heretek.dorado_hd.data.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import com.heretek.xunehd.data.model.Album
import com.heretek.xunehd.data.model.Artist
import com.heretek.xunehd.data.model.Genre
import com.heretek.dorado_hd.data.model.Album
import com.heretek.dorado_hd.data.model.Artist
import com.heretek.dorado_hd.data.model.Genre
import kotlinx.coroutines.flow.Flow
data class AlbumRow(
@@ -168,7 +168,7 @@ interface PlaylistDao {
}
data class PlaylistRow(val id: Long, val name: String, val dateCreated: Long, val trackCount: Int) {
fun toPlaylist() = com.heretek.xunehd.data.model.Playlist(id, name, dateCreated, trackCount)
fun toPlaylist() = com.heretek.dorado_hd.data.model.Playlist(id, name, dateCreated, trackCount)
}
}
@@ -298,7 +298,7 @@ interface PodcastDao {
"FROM podcast_episodes e JOIN podcast_feeds f ON e.feedId = f.id " +
"ORDER BY e.pubAt DESC"
)
fun episodesFlat(): Flow<List<com.heretek.xunehd.data.db.PodcastEpisodeFlat>>
fun episodesFlat(): Flow<List<com.heretek.dorado_hd.data.db.PodcastEpisodeFlat>>
@Query("SELECT * FROM podcast_episodes WHERE id = :id")
suspend fun episode(id: Long): PodcastEpisodeEntity?
@@ -1,4 +1,4 @@
package com.heretek.xunehd.data.db
package com.heretek.dorado_hd.data.db
import android.content.Context
import androidx.room.Database
@@ -25,7 +25,7 @@ import androidx.room.RoomDatabase
version = 2,
exportSchema = false,
)
abstract class XuneDatabase : RoomDatabase() {
abstract class DoradoDatabase : RoomDatabase() {
abstract fun trackDao(): TrackDao
abstract fun ratingDao(): RatingDao
abstract fun playlistDao(): PlaylistDao
@@ -41,11 +41,11 @@ abstract class XuneDatabase : RoomDatabase() {
companion object {
@Volatile
private var instance: XuneDatabase? = null
private var instance: DoradoDatabase? = null
fun get(context: Context): XuneDatabase =
fun get(context: Context): DoradoDatabase =
instance ?: synchronized(this) {
instance ?: Room.databaseBuilder(context, XuneDatabase::class.java, "xune.db")
instance ?: Room.databaseBuilder(context, DoradoDatabase::class.java, "dorado_hd.db")
.addMigrations(MIGRATION_1_2)
.build()
.also { instance = it }
@@ -1,4 +1,4 @@
package com.heretek.xunehd.data.db
package com.heretek.dorado_hd.data.db
import androidx.room.Entity
import androidx.room.Index
@@ -1,4 +1,4 @@
package com.heretek.xunehd.data.db
package com.heretek.dorado_hd.data.db
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@@ -1,4 +1,4 @@
package com.heretek.xunehd.data.model
package com.heretek.dorado_hd.data.model
import android.net.Uri
@@ -1,10 +1,10 @@
package com.heretek.xunehd.data.official
package com.heretek.dorado_hd.data.official
/**
* The frozen official Zune HD marketplace catalog (62 packages — see
* `docs/zcp-inventory.md` and `tools/zcp_inventory.py`). Each entry maps to
* a real ZuneRedux-installable package; the `installedId` field, when
* non-null, names the Xune-HD mini-app that re-implements it canonically.
* non-null, names the Dorado-HD mini-app that re-implements it canonically.
*/
data class OfficialApp(
val title: String,
@@ -1,4 +1,4 @@
package com.heretek.xunehd.data.repo
package com.heretek.dorado_hd.data.repo
import android.content.ContentUris
import android.content.Context
@@ -7,15 +7,15 @@ import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import androidx.documentfile.provider.DocumentFile
import com.heretek.xunehd.data.db.TrackEntity
import com.heretek.xunehd.data.db.XuneDatabase
import com.heretek.xunehd.data.model.Album
import com.heretek.xunehd.data.model.Artist
import com.heretek.xunehd.data.model.Genre
import com.heretek.xunehd.data.model.Playlist
import com.heretek.xunehd.data.model.Track
import com.heretek.xunehd.data.scan.MediaLibraryScanner
import com.heretek.xunehd.data.scan.toModel
import com.heretek.dorado_hd.data.db.TrackEntity
import com.heretek.dorado_hd.data.db.DoradoDatabase
import com.heretek.dorado_hd.data.model.Album
import com.heretek.dorado_hd.data.model.Artist
import com.heretek.dorado_hd.data.model.Genre
import com.heretek.dorado_hd.data.model.Playlist
import com.heretek.dorado_hd.data.model.Track
import com.heretek.dorado_hd.data.scan.MediaLibraryScanner
import com.heretek.dorado_hd.data.scan.toModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -24,7 +24,7 @@ import kotlinx.coroutines.withContext
class LibraryRepository(
private val context: Context,
private val db: XuneDatabase,
private val db: DoradoDatabase,
) {
val scanning = MutableStateFlow(false)
val lastScanResult = MutableStateFlow<MediaLibraryScanner.ScanResult?>(null)
@@ -57,7 +57,7 @@ class LibraryRepository(
suspend fun trackCount(): Int = db.trackDao().count()
/** The path to the Room database on disk — surfaced in Settings > collection. */
fun databasePath(): String = context.getDatabasePath("xune.db").absolutePath
fun databasePath(): String = context.getDatabasePath("dorado_hd.db").absolutePath
suspend fun refresh(): MediaLibraryScanner.ScanResult {
scanning.value = true
@@ -180,13 +180,13 @@ class LibraryRepository(
}
suspend fun createPlaylist(name: String): Long = db.playlistDao().create(
com.heretek.xunehd.data.db.PlaylistEntity(name = name, dateCreated = System.currentTimeMillis()),
com.heretek.dorado_hd.data.db.PlaylistEntity(name = name, dateCreated = System.currentTimeMillis()),
)
suspend fun addToPlaylist(playlistId: Long, track: Track) {
val max = db.playlistDao().maxPosition(playlistId) ?: -1
db.playlistDao().addToPlaylist(
com.heretek.xunehd.data.db.PlaylistTrackEntity(playlistId, track.mediaId, max + 1),
com.heretek.dorado_hd.data.db.PlaylistTrackEntity(playlistId, track.mediaId, max + 1),
)
}
@@ -1,18 +1,18 @@
package com.heretek.xunehd.data.repo
package com.heretek.dorado_hd.data.repo
import com.heretek.xunehd.data.db.AlarmEntity
import com.heretek.xunehd.data.db.AppointmentEntity
import com.heretek.xunehd.data.db.GameScoreEntity
import com.heretek.xunehd.data.db.NoteEntity
import com.heretek.xunehd.data.db.PodcastEpisodeEntity
import com.heretek.xunehd.data.db.PodcastEpisodeFlat
import com.heretek.xunehd.data.db.PodcastFeedEntity
import com.heretek.xunehd.data.db.RadioStationEntity
import com.heretek.xunehd.data.db.XuneDatabase
import com.heretek.dorado_hd.data.db.AlarmEntity
import com.heretek.dorado_hd.data.db.AppointmentEntity
import com.heretek.dorado_hd.data.db.GameScoreEntity
import com.heretek.dorado_hd.data.db.NoteEntity
import com.heretek.dorado_hd.data.db.PodcastEpisodeEntity
import com.heretek.dorado_hd.data.db.PodcastEpisodeFlat
import com.heretek.dorado_hd.data.db.PodcastFeedEntity
import com.heretek.dorado_hd.data.db.RadioStationEntity
import com.heretek.dorado_hd.data.db.DoradoDatabase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class NotesRepository(private val db: XuneDatabase) {
class NotesRepository(private val db: DoradoDatabase) {
fun notes(): Flow<List<NoteEntity>> = db.noteDao().all()
suspend fun add(title: String, body: String): Long =
db.noteDao().insert(NoteEntity(title = title, body = body, modifiedAt = System.currentTimeMillis()))
@@ -22,7 +22,7 @@ class NotesRepository(private val db: XuneDatabase) {
suspend fun delete(id: Long) = db.noteDao().delete(id)
}
class CalendarRepository(private val db: XuneDatabase) {
class CalendarRepository(private val db: DoradoDatabase) {
fun appointments(): Flow<List<AppointmentEntity>> = db.appointmentDao().all()
suspend fun inRange(from: Long, to: Long): List<AppointmentEntity> = db.appointmentDao().inRange(from, to)
suspend fun add(title: String, notes: String?, startAt: Long, endAt: Long?): Long =
@@ -30,7 +30,7 @@ class CalendarRepository(private val db: XuneDatabase) {
suspend fun delete(id: Long) = db.appointmentDao().delete(id)
}
class AlarmRepository(private val db: XuneDatabase) {
class AlarmRepository(private val db: DoradoDatabase) {
fun alarms(): Flow<List<AlarmEntity>> = db.alarmDao().all()
suspend fun byId(id: Long): AlarmEntity? = db.alarmDao().byId(id)
suspend fun add(alarm: AlarmEntity): Long = db.alarmDao().insert(alarm)
@@ -38,7 +38,7 @@ class AlarmRepository(private val db: XuneDatabase) {
suspend fun delete(id: Long) = db.alarmDao().delete(id)
}
class RadioRepository(private val db: XuneDatabase) {
class RadioRepository(private val db: DoradoDatabase) {
fun stations(): Flow<List<RadioStationEntity>> = db.radioDao().all()
suspend fun add(station: RadioStationEntity): Long = db.radioDao().insert(station)
suspend fun delete(id: Long) = db.radioDao().delete(id)
@@ -63,7 +63,7 @@ class RadioRepository(private val db: XuneDatabase) {
}
}
class PodcastRepository(private val db: XuneDatabase) {
class PodcastRepository(private val db: DoradoDatabase) {
fun feeds(): Flow<List<PodcastFeedEntity>> = db.podcastDao().feeds()
suspend fun feed(id: Long): PodcastFeedEntity? = db.podcastDao().feed(id)
suspend fun addFeed(f: PodcastFeedEntity): Long = db.podcastDao().insertFeed(f)
@@ -76,7 +76,7 @@ class PodcastRepository(private val db: XuneDatabase) {
suspend fun markPlayed(id: Long) = db.podcastDao().markPlayed(id)
}
class GameRepository(private val db: XuneDatabase) {
class GameRepository(private val db: DoradoDatabase) {
fun top(game: String, limit: Int = 10): Flow<List<GameScoreEntity>> = db.gameScoreDao().top(game, limit)
suspend fun record(game: String, score: Int, meta: String?): Long =
db.gameScoreDao().insert(
@@ -1,11 +1,11 @@
package com.heretek.xunehd.data.repo
package com.heretek.dorado_hd.data.repo
import com.heretek.xunehd.data.db.HistoryEntity
import com.heretek.xunehd.data.db.PinEntity
import com.heretek.xunehd.data.db.RatingEntity
import com.heretek.xunehd.data.db.XuneDatabase
import com.heretek.xunehd.data.model.PinKind
import com.heretek.xunehd.data.model.Rating
import com.heretek.dorado_hd.data.db.HistoryEntity
import com.heretek.dorado_hd.data.db.PinEntity
import com.heretek.dorado_hd.data.db.RatingEntity
import com.heretek.dorado_hd.data.db.DoradoDatabase
import com.heretek.dorado_hd.data.model.PinKind
import com.heretek.dorado_hd.data.model.Rating
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -17,7 +17,7 @@ data class QuickplayCard(
val artAlbumId: Long,
)
class QuickplayRepository(private val db: XuneDatabase) {
class QuickplayRepository(private val db: DoradoDatabase) {
fun pins(): Flow<List<QuickplayCard>> = db.pinDao().pins().map { list ->
list.map { it.toCard() }
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.data.repo
package com.heretek.dorado_hd.data.repo
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
@@ -6,14 +6,14 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.heretek.xunehd.design.XuneAccent
import com.heretek.dorado_hd.design.DoradoAccent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore by preferencesDataStore(name = "xune_settings")
private val Context.dataStore by preferencesDataStore(name = "dorado_hd_settings")
data class XuneSettings(
val accent: XuneAccent = XuneAccent.PINK,
data class DoradoSettings(
val accent: DoradoAccent = DoradoAccent.PINK,
val deviceMode: Boolean = false,
val artistImagesEnabled: Boolean = true,
val artistImageTemplate: String = "",
@@ -35,9 +35,9 @@ class SettingsRepository(private val context: Context) {
val IMPORTED_TREE_URI = stringPreferencesKey("imported_tree_uri")
}
val settings: Flow<XuneSettings> = context.dataStore.data.map { p ->
XuneSettings(
accent = XuneAccent.entries.firstOrNull { it.id == p[Keys.ACCENT] } ?: XuneAccent.PINK,
val settings: Flow<DoradoSettings> = context.dataStore.data.map { p ->
DoradoSettings(
accent = DoradoAccent.entries.firstOrNull { it.id == p[Keys.ACCENT] } ?: DoradoAccent.PINK,
deviceMode = p[Keys.DEVICE_MODE] ?: false,
artistImagesEnabled = p[Keys.ARTIST_IMAGES] ?: true,
artistImageTemplate = p[Keys.ARTIST_IMAGE_TEMPLATE] ?: "",
@@ -47,7 +47,7 @@ class SettingsRepository(private val context: Context) {
)
}
suspend fun setAccent(accent: XuneAccent) = context.dataStore.edit { it[Keys.ACCENT] = accent.id }
suspend fun setAccent(accent: DoradoAccent) = context.dataStore.edit { it[Keys.ACCENT] = accent.id }
suspend fun setDeviceMode(enabled: Boolean) = context.dataStore.edit { it[Keys.DEVICE_MODE] = enabled }
suspend fun setArtistImages(enabled: Boolean) = context.dataStore.edit { it[Keys.ARTIST_IMAGES] = enabled }
suspend fun setArtistImageTemplate(template: String) = context.dataStore.edit { it[Keys.ARTIST_IMAGE_TEMPLATE] = template }
@@ -1,11 +1,11 @@
package com.heretek.xunehd.data.scan
package com.heretek.dorado_hd.data.scan
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import com.heretek.xunehd.data.db.TrackEntity
import com.heretek.xunehd.data.db.XuneDatabase
import com.heretek.dorado_hd.data.db.TrackEntity
import com.heretek.dorado_hd.data.db.DoradoDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -16,7 +16,7 @@ import kotlinx.coroutines.withContext
*/
class MediaLibraryScanner(
private val context: Context,
private val db: XuneDatabase,
private val db: DoradoDatabase,
) {
suspend fun scan(): ScanResult = withContext(Dispatchers.IO) {
val found = mutableListOf<TrackEntity>()
@@ -109,8 +109,8 @@ class MediaLibraryScanner(
data class ScanResult(val scanned: Int, val removed: Int)
}
fun TrackEntity.toModel(): com.heretek.xunehd.data.model.Track =
com.heretek.xunehd.data.model.Track(
fun TrackEntity.toModel(): com.heretek.dorado_hd.data.model.Track =
com.heretek.dorado_hd.data.model.Track(
mediaId = mediaId,
title = title,
artist = artist,
@@ -1,9 +1,9 @@
package com.heretek.xunehd.design
package com.heretek.dorado_hd.design
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
enum class XuneAccent(val id: Int, val label: String, val primary: Color, val bright: Color) {
enum class DoradoAccent(val id: Int, val label: String, val primary: Color, val bright: Color) {
PINK(0, "zune pink", Color(0xFFFA2A55), Color(0xFFFF4D79)),
ORANGE(1, "zune orange", Color(0xFFF09609), Color(0xFFFFA726)),
CYAN(2, "electric cyan", Color(0xFF1BA1E2), Color(0xFF33B5E5)),
@@ -12,7 +12,7 @@ enum class XuneAccent(val id: Int, val label: String, val primary: Color, val br
}
@Immutable
data class XuneColors(
data class DoradoColors(
val background: Color = Color(0xFF111111),
val elevated: Color = Color(0xFF181818),
val tile: Color = Color(0xFF202020),
@@ -1,4 +1,4 @@
package com.heretek.xunehd.design
package com.heretek.dorado_hd.design
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
@@ -9,7 +9,7 @@ import androidx.compose.animation.core.tween
* Motion spec: movement starts fast and decelerates gently — the signature
* Zune "kinetic" curve. All navigation slides use these durations.
*/
object XuneMotion {
object DoradoMotion {
val Decelerate: Easing = CubicBezierEasing(0.1f, 0.9f, 0.2f, 1.0f)
const val PIVOT_SLIDE_MS = 380
@@ -1,4 +1,4 @@
package com.heretek.xunehd.design
package com.heretek.dorado_hd.design
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -6,7 +6,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import com.heretek.xunehd.R
import com.heretek.dorado_hd.R
/**
* Selawik — the OFL Segoe-metric font family that stands in for Zegoe UI.
@@ -20,17 +20,17 @@ val Selawik = FontFamily(
Font(R.font.selawkb, FontWeight.Bold),
)
val LocalXuneColors = staticCompositionLocalOf<XuneColors> {
error("XuneColors not provided")
val LocalDoradoColors = staticCompositionLocalOf<DoradoColors> {
error("DoradoColors not provided")
}
@Composable
fun XuneTheme(
accent: XuneAccent = XuneAccent.PINK,
fun DoradoTheme(
accent: DoradoAccent = DoradoAccent.PINK,
content: @Composable () -> Unit,
) {
val colors = XuneColors(accent = accent.primary, accentBright = accent.bright)
CompositionLocalProvider(LocalXuneColors provides colors) {
val colors = DoradoColors(accent = accent.primary, accentBright = accent.bright)
CompositionLocalProvider(LocalDoradoColors provides colors) {
content()
}
}
@@ -1,10 +1,10 @@
package com.heretek.xunehd.design
package com.heretek.dorado_hd.design
/**
* Design tokens from docs/design-tokens.md. Screens must consume these;
* raw literals in screen code violate the design-invariant audit.
*/
object XuneTokens {
object DoradoTokens {
// Canvas
const val CANVAS_WIDTH = 480
const val CANVAS_HEIGHT = 272
@@ -1,4 +1,4 @@
package com.heretek.xunehd.design.components
package com.heretek.dorado_hd.design.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@@ -18,8 +18,8 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.XuneMotion
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.DoradoMotion
import kotlinx.coroutines.delay
/**
@@ -32,7 +32,7 @@ fun AlbumArt(
contentDescription: String?,
modifier: Modifier = Modifier,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
Box(
modifier = modifier
.background(colors.tile)
@@ -61,11 +61,11 @@ fun StaggerEntrance(
var appeared by remember { mutableStateOf(false) }
val progress by animateFloatAsState(
targetValue = if (appeared) 1f else 0f,
animationSpec = tween(durationMillis = 220, easing = XuneMotion.Decelerate),
animationSpec = tween(durationMillis = 220, easing = DoradoMotion.Decelerate),
label = "stagger",
)
LaunchedEffect(Unit) {
delay(index * XuneMotion.STAGGER_MS)
delay(index * DoradoMotion.STAGGER_MS)
appeared = true
}
Box(
@@ -1,4 +1,4 @@
package com.heretek.xunehd.design.components
package com.heretek.dorado_hd.design.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
@@ -16,9 +16,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneTokens
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoTokens
/**
* The Zune crossbar: lowercase pivot labels across the top of the screen,
@@ -33,13 +33,13 @@ fun CrossbarBar(
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
Row(
modifier = modifier
.fillMaxWidth()
.height(XuneTokens.CROSSBAR_HEIGHT.dp)
.height(DoradoTokens.CROSSBAR_HEIGHT.dp)
.horizontalScroll(rememberScrollState())
.padding(start = XuneTokens.CROSSBAR_LEAD.dp),
.padding(start = DoradoTokens.CROSSBAR_LEAD.dp),
verticalAlignment = Alignment.CenterVertically,
) {
labels.forEachIndexed { index, label ->
@@ -48,7 +48,7 @@ fun CrossbarBar(
// EdgeCropText implements the device's right-edge signature clipping.
EdgeCropText(
text = label,
fontSize = XuneTokens.TYPE_CROSSBAR.dp,
fontSize = DoradoTokens.TYPE_CROSSBAR.dp,
color = color,
modifier = Modifier
.clickable(onClick = { onSelect(index) })
@@ -1,4 +1,4 @@
package com.heretek.xunehd.design.components
package com.heretek.dorado_hd.design.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -12,8 +12,8 @@ import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.XuneTokens
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.DoradoTokens
import kotlin.math.min
/**
@@ -33,26 +33,26 @@ fun DeviceCanvas(
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(LocalXuneColors.current.background),
.background(LocalDoradoColors.current.background),
) {
if (!deviceMode) {
content(maxWidth, maxHeight)
} else {
val scale = min(
maxWidth.value / XuneTokens.CANVAS_WIDTH,
maxHeight.value / XuneTokens.CANVAS_HEIGHT,
maxWidth.value / DoradoTokens.CANVAS_WIDTH,
maxHeight.value / DoradoTokens.CANVAS_HEIGHT,
)
Box(
modifier = Modifier
.align(Alignment.Center)
.size(XuneTokens.CANVAS_WIDTH.dp, XuneTokens.CANVAS_HEIGHT.dp)
.size(DoradoTokens.CANVAS_WIDTH.dp, DoradoTokens.CANVAS_HEIGHT.dp)
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.clipToBounds(),
) {
content(XuneTokens.CANVAS_WIDTH.dp, XuneTokens.CANVAS_HEIGHT.dp)
content(DoradoTokens.CANVAS_WIDTH.dp, DoradoTokens.CANVAS_HEIGHT.dp)
}
}
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.design.components
package com.heretek.dorado_hd.design.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -26,9 +26,9 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneTokens
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoTokens
/**
* Text that deliberately overflows and is cropped at the right edge — the
@@ -39,7 +39,7 @@ fun EdgeCropText(
text: String,
fontSize: Dp,
modifier: Modifier = Modifier,
color: Color = LocalXuneColors.current.textPrimary,
color: Color = LocalDoradoColors.current.textPrimary,
fontWeight: FontWeight = FontWeight.Light,
alpha: Float = 1f,
) {
@@ -51,7 +51,7 @@ fun EdgeCropText(
fontWeight = fontWeight,
fontSize = fontSize.value.sp,
color = color.copy(alpha = alpha),
letterSpacing = XuneTokens.LETTER_SPACING_EDGE.sp,
letterSpacing = DoradoTokens.LETTER_SPACING_EDGE.sp,
),
maxLines = 1,
softWrap = false,
@@ -70,10 +70,10 @@ fun CroppedHeader(
text: String,
onBack: () -> Unit,
modifier: Modifier = Modifier,
visibleHeight: Dp = XuneTokens.TYPE_HEADER_CROP_VISIBLE.dp,
fontSize: Dp = XuneTokens.TYPE_HEADER_CROPPED.dp,
visibleHeight: Dp = DoradoTokens.TYPE_HEADER_CROP_VISIBLE.dp,
fontSize: Dp = DoradoTokens.TYPE_HEADER_CROPPED.dp,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
Box(
modifier = modifier
.fillMaxWidth()
@@ -89,7 +89,7 @@ fun CroppedHeader(
fontWeight = FontWeight.Light,
fontSize = fontSize.value.sp,
color = colors.textPrimary.copy(alpha = 0.85f),
letterSpacing = XuneTokens.LETTER_SPACING_HEADER.sp,
letterSpacing = DoradoTokens.LETTER_SPACING_HEADER.sp,
),
maxLines = 1,
softWrap = false,
@@ -103,13 +103,13 @@ fun CroppedHeader(
text = "<-",
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_NOW_META.sp,
fontSize = DoradoTokens.TYPE_NOW_META.sp,
color = colors.accent.copy(alpha = 0.6f),
),
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = XuneTokens.EDGE.dp)
.offset(y = (visibleHeight - XuneTokens.TYPE_NOW_META.dp).coerceAtLeast(0.dp) / 2),
.padding(end = DoradoTokens.EDGE.dp)
.offset(y = (visibleHeight - DoradoTokens.TYPE_NOW_META.dp).coerceAtLeast(0.dp) / 2),
)
}
}
@@ -126,11 +126,11 @@ fun HomeMenuItem(
) {
EdgeCropText(
text = label,
fontSize = XuneTokens.TYPE_MENU_ITEM.dp,
fontSize = DoradoTokens.TYPE_MENU_ITEM.dp,
alpha = alpha,
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(start = XuneTokens.EDGE.dp, top = 10.dp, bottom = 10.dp),
.padding(start = DoradoTokens.EDGE.dp, top = 10.dp, bottom = 10.dp),
)
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.design.components
package com.heretek.dorado_hd.design.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
@@ -41,10 +41,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneMotion
import com.heretek.xunehd.design.XuneTokens
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoMotion
import com.heretek.dorado_hd.design.DoradoTokens
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -64,7 +64,7 @@ fun <T> KineticList(
listState: LazyListState = rememberLazyListState(),
bottomPadding: Dp = 0.dp,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val scope = rememberCoroutineScope()
val letters = remember(items) {
@@ -83,7 +83,7 @@ fun <T> KineticList(
if (index >= 0) scope.launch { listState.scrollToItem(index) }
scope.launch {
delay(350)
overlayAlpha.animateTo(0f, XuneMotion.pivot())
overlayAlpha.animateTo(0f, DoradoMotion.pivot())
}
}
@@ -118,7 +118,7 @@ fun <T> KineticList(
onDragEnd = {
scope.launch {
delay(350)
overlayAlpha.animateTo(0f, XuneMotion.pivot())
overlayAlpha.animateTo(0f, DoradoMotion.pivot())
}
},
)
@@ -145,8 +145,8 @@ fun <T> KineticList(
// The full A–Z index, canon §3.5.
AnimatedVisibility(
visible = indexOpen,
enter = fadeIn(XuneMotion.pivot()),
exit = fadeOut(XuneMotion.pivot()),
enter = fadeIn(DoradoMotion.pivot()),
exit = fadeOut(DoradoMotion.pivot()),
) {
AlphabetIndex(
present = present,
@@ -170,7 +170,7 @@ private fun AlphabetIndex(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val alphabet = remember { ('A'..'Z').toList() + '#' }
Box(
modifier
@@ -195,7 +195,7 @@ private fun AlphabetIndex(
style = TextStyle(
fontFamily = Selawik,
fontWeight = FontWeight.Light,
fontSize = XuneTokens.TYPE_CROSSBAR.sp,
fontSize = DoradoTokens.TYPE_CROSSBAR.sp,
color = if (letterChar in present) colors.textPrimary else colors.textInactive,
textAlign = TextAlign.Center,
),
@@ -216,7 +216,7 @@ private fun AlphabetRail(
onLetterFocus: (Char) -> Unit,
onDragEnd: () -> Unit,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
Column(modifier = modifier) {
present.sorted().forEach { letterChar ->
Box(
@@ -235,7 +235,7 @@ private fun AlphabetRail(
text = letterChar.toString(),
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_ALPHABET.sp,
fontSize = DoradoTokens.TYPE_ALPHABET.sp,
color = colors.textSecondary,
textAlign = TextAlign.Center,
),
@@ -1,4 +1,4 @@
package com.heretek.xunehd.design.components
package com.heretek.dorado_hd.design.components
import android.annotation.SuppressLint
import android.app.WallpaperManager
@@ -38,10 +38,10 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneMotion
import com.heretek.xunehd.design.XuneTokens
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoMotion
import com.heretek.dorado_hd.design.DoradoTokens
import java.util.Date
import kotlinx.coroutines.launch
@@ -57,7 +57,7 @@ fun LockShade(
onUnlock: () -> Unit,
modifier: Modifier = Modifier,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
val offset = remember { Animatable(0f) }
@@ -81,8 +81,8 @@ fun LockShade(
AnimatedVisibility(
visible = visible,
enter = fadeIn(XuneMotion.instant()),
exit = fadeOut(XuneMotion.pivot()),
enter = fadeIn(DoradoMotion.instant()),
exit = fadeOut(DoradoMotion.pivot()),
modifier = modifier,
) {
Box(
@@ -98,11 +98,11 @@ fun LockShade(
onDragEnd = {
if (offset.value > unlockPx) {
scope.launch {
offset.animateTo(unlockPx * 1.6f, XuneMotion.pivot())
offset.animateTo(unlockPx * 1.6f, DoradoMotion.pivot())
}
onUnlock()
} else {
scope.launch { offset.animateTo(0f, XuneMotion.pivot()) }
scope.launch { offset.animateTo(0f, DoradoMotion.pivot()) }
}
},
)
@@ -125,7 +125,7 @@ fun LockShade(
Modifier
.align(Alignment.Center)
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
BasicText(
@@ -133,7 +133,7 @@ fun LockShade(
style = TextStyle(
fontFamily = Selawik,
fontWeight = FontWeight.Light,
fontSize = XuneTokens.TYPE_HEADER_CROPPED.sp,
fontSize = DoradoTokens.TYPE_HEADER_CROPPED.sp,
color = colors.textPrimary,
),
)
@@ -142,7 +142,7 @@ fun LockShade(
text = DateFormat.getDateFormat(context).format(Date(now)),
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_LIST.sp,
fontSize = DoradoTokens.TYPE_LIST.sp,
color = colors.textSecondary,
),
)
@@ -151,7 +151,7 @@ fun LockShade(
text = "slide up to unlock",
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_NOW_META.sp,
fontSize = DoradoTokens.TYPE_NOW_META.sp,
color = colors.textSecondary,
),
modifier = Modifier
@@ -160,16 +160,16 @@ fun LockShade(
.padding(bottom = 32.dp),
)
BasicText(
text = "xune hd",
text = "dorado hd",
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_CROSSBAR.sp,
fontSize = DoradoTokens.TYPE_CROSSBAR.sp,
color = colors.textWatermark,
),
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(start = XuneTokens.EDGE.dp, bottom = XuneTokens.EDGE.dp),
.padding(start = DoradoTokens.EDGE.dp, bottom = DoradoTokens.EDGE.dp),
)
}
}
@@ -1,9 +1,9 @@
package com.heretek.xunehd.media
package com.heretek.dorado_hd.media
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.heretek.xunehd.XuneApp
import com.heretek.dorado_hd.DoradoApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -20,7 +20,7 @@ class AlarmReceiver : BroadcastReceiver() {
val alarmKind = intent.getIntExtra("alarmKind", 0)
val refId = intent.getLongExtra("refId", -1)
if (alarmId < 0) return
val graph = (context.applicationContext as XuneApp).graph
val graph = (context.applicationContext as DoradoApp).graph
val am = context.getSystemService(Context.ALARM_SERVICE) as android.app.AlarmManager
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@@ -1,4 +1,4 @@
package com.heretek.xunehd.media
package com.heretek.dorado_hd.media
import java.util.Calendar
@@ -1,4 +1,4 @@
package com.heretek.xunehd.media
package com.heretek.dorado_hd.media
import android.app.PendingIntent
import android.content.Intent
@@ -9,9 +9,9 @@ import androidx.media3.common.MediaMetadata
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.heretek.xunehd.MainActivity
import com.heretek.xunehd.XuneApp
import com.heretek.xunehd.data.model.Track
import com.heretek.dorado_hd.MainActivity
import com.heretek.dorado_hd.DoradoApp
import com.heretek.dorado_hd.data.model.Track
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.guava.future
@@ -21,7 +21,7 @@ import kotlinx.coroutines.withContext
* Foreground Media3 session service. Holds the ExoPlayer instance and the
* MediaSession that powers the system notification / lock screen controls.
*/
class XunePlaybackService : MediaSessionService() {
class DoradoPlaybackService : MediaSessionService() {
private var session: MediaSession? = null
@@ -67,7 +67,7 @@ class XunePlaybackService : MediaSessionService() {
*/
private fun resolveMediaItems(requested: List<MediaItem>): com.google.common.util.concurrent.ListenableFuture<MutableList<MediaItem>> =
GlobalScope.future(Dispatchers.IO) {
val repo = (applicationContext as XuneApp).graph.library
val repo = (applicationContext as DoradoApp).graph.library
requested.map { item ->
if (item.localConfiguration != null) {
item
@@ -1,4 +1,4 @@
package com.heretek.xunehd.media
package com.heretek.dorado_hd.media
import android.content.ComponentName
import android.content.Context
@@ -8,11 +8,11 @@ import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.heretek.xunehd.data.model.RepeatMode
import com.heretek.xunehd.data.model.Rating
import com.heretek.xunehd.data.model.Track
import com.heretek.xunehd.data.repo.QuickplayRepository
import com.heretek.xunehd.data.repo.LibraryRepository
import com.heretek.dorado_hd.data.model.RepeatMode
import com.heretek.dorado_hd.data.model.Rating
import com.heretek.dorado_hd.data.model.Track
import com.heretek.dorado_hd.data.repo.QuickplayRepository
import com.heretek.dorado_hd.data.repo.LibraryRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -68,7 +68,7 @@ class PlaybackController(
suspend fun connect() {
if (controller != null) return
val token = SessionToken(context, ComponentName(context, XunePlaybackService::class.java))
val token = SessionToken(context, ComponentName(context, DoradoPlaybackService::class.java))
controller = MediaController.Builder(context, token).buildAsync().await()
controller?.addListener(listener)
positionTicker()
@@ -95,7 +95,7 @@ class PlaybackController(
scope.launch {
_currentRating.value = quickplay.ratingOf(track.mediaId).first()
quickplay.recordHistory(
kind = com.heretek.xunehd.data.model.PinKind.TRACK,
kind = com.heretek.dorado_hd.data.model.PinKind.TRACK,
refId = track.mediaId,
label = track.title,
subLabel = track.artist,
@@ -1,4 +1,4 @@
package com.heretek.xunehd.net
package com.heretek.dorado_hd.net
import android.content.Context
import java.io.File
@@ -59,6 +59,6 @@ class ArtistBioService(private val context: Context) {
companion object {
private const val SEVEN_DAYS = 7L * 24 * 60 * 60 * 1000
private const val USER_AGENT = "XuneHD/0.1 (android; sister-app to not-zune)"
private const val USER_AGENT = "DoradoHD/0.1 (android; sister-app to dorado)"
}
}
@@ -1,9 +1,9 @@
package com.heretek.xunehd.net
package com.heretek.dorado_hd.net
import android.content.Context
import com.heretek.xunehd.data.db.ArtistImageEntity
import com.heretek.xunehd.data.db.XuneDatabase
import com.heretek.xunehd.data.repo.SettingsRepository
import com.heretek.dorado_hd.data.db.ArtistImageEntity
import com.heretek.dorado_hd.data.db.DoradoDatabase
import com.heretek.dorado_hd.data.repo.SettingsRepository
import java.io.File
import java.security.MessageDigest
import kotlinx.coroutines.Dispatchers
@@ -19,7 +19,7 @@ import kotlinx.coroutines.withContext
*/
class ArtistImageService(
private val context: Context,
private val db: XuneDatabase,
private val db: DoradoDatabase,
private val settings: SettingsRepository,
) {
private val mbidCache = mutableMapOf<String, String>()
@@ -57,7 +57,7 @@ class ArtistImageService(
}
@Volatile
var settingsSnapshot: com.heretek.xunehd.data.repo.XuneSettings? = null
var settingsSnapshot: com.heretek.dorado_hd.data.repo.DoradoSettings? = null
private suspend fun lookupMbid(artist: String): String? {
mbidCache[artist.lowercase()]?.let { return it }
@@ -96,6 +96,6 @@ class ArtistImageService(
companion object {
private const val SEVEN_DAYS = 7L * 24 * 60 * 60 * 1000
private const val USER_AGENT = "XuneHD/0.1 (android; sister-app to not-zune)"
private const val USER_AGENT = "DoradoHD/0.1 (android; sister-app to dorado)"
}
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui
package com.heretek.dorado_hd.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
@@ -37,40 +37,40 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.heretek.xunehd.R
import com.heretek.xunehd.data.repo.XuneSettings
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.XuneMotion
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.AlbumArt
import com.heretek.xunehd.design.components.DeviceCanvas
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.design.components.LockShade
import com.heretek.xunehd.ui.components.MenuController
import com.heretek.xunehd.ui.nav.XuneDestination
import com.heretek.xunehd.ui.screens.AlbumDetailScreen
import com.heretek.xunehd.ui.screens.ArtistDetailScreen
import com.heretek.xunehd.ui.screens.GenreScreen
import com.heretek.xunehd.ui.screens.HomePages
import com.heretek.xunehd.ui.screens.InternetScreen
import com.heretek.xunehd.ui.screens.MarketplaceScreen
import com.heretek.xunehd.ui.screens.MiniAppScreen
import com.heretek.xunehd.ui.screens.MusicScreen
import com.heretek.xunehd.ui.screens.NowPlayingScreen
import com.heretek.xunehd.ui.screens.PicturesScreen
import com.heretek.xunehd.ui.screens.PictureDetailScreen
import com.heretek.xunehd.ui.screens.PodcastFeedScreen
import com.heretek.xunehd.ui.screens.PodcastsScreen
import com.heretek.xunehd.ui.screens.PlaylistDetailScreen
import com.heretek.xunehd.ui.screens.RadioScreen
import com.heretek.xunehd.ui.screens.SettingsScreen
import com.heretek.xunehd.ui.screens.SocialScreen
import com.heretek.xunehd.ui.screens.VideosScreen
import com.heretek.dorado_hd.R
import com.heretek.dorado_hd.data.repo.DoradoSettings
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.DoradoMotion
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.AlbumArt
import com.heretek.dorado_hd.design.components.DeviceCanvas
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.design.components.LockShade
import com.heretek.dorado_hd.ui.components.MenuController
import com.heretek.dorado_hd.ui.nav.DoradoDestination
import com.heretek.dorado_hd.ui.screens.AlbumDetailScreen
import com.heretek.dorado_hd.ui.screens.ArtistDetailScreen
import com.heretek.dorado_hd.ui.screens.GenreScreen
import com.heretek.dorado_hd.ui.screens.HomePages
import com.heretek.dorado_hd.ui.screens.InternetScreen
import com.heretek.dorado_hd.ui.screens.MarketplaceScreen
import com.heretek.dorado_hd.ui.screens.MiniAppScreen
import com.heretek.dorado_hd.ui.screens.MusicScreen
import com.heretek.dorado_hd.ui.screens.NowPlayingScreen
import com.heretek.dorado_hd.ui.screens.PicturesScreen
import com.heretek.dorado_hd.ui.screens.PictureDetailScreen
import com.heretek.dorado_hd.ui.screens.PodcastFeedScreen
import com.heretek.dorado_hd.ui.screens.PodcastsScreen
import com.heretek.dorado_hd.ui.screens.PlaylistDetailScreen
import com.heretek.dorado_hd.ui.screens.RadioScreen
import com.heretek.dorado_hd.ui.screens.SettingsScreen
import com.heretek.dorado_hd.ui.screens.SocialScreen
import com.heretek.dorado_hd.ui.screens.VideosScreen
@Composable
fun XuneRoot() {
val graph = LocalXuneGraph.current
val settings by graph.settingsFlow.collectAsState(initial = XuneSettings())
fun DoradoRoot() {
val graph = LocalDoradoGraph.current
val settings by graph.settingsFlow.collectAsState(initial = DoradoSettings())
val menus = remember { MenuController() }
val activity = androidx.activity.compose.LocalActivity.current
var shaded by remember { mutableStateOf(false) }
@@ -90,9 +90,9 @@ fun XuneRoot() {
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
com.heretek.xunehd.design.XuneTheme(accent = settings.accent) {
com.heretek.dorado_hd.design.DoradoTheme(accent = settings.accent) {
androidx.compose.runtime.CompositionLocalProvider(
com.heretek.xunehd.ui.components.LocalContextMenu provides menus,
com.heretek.dorado_hd.ui.components.LocalContextMenu provides menus,
) {
DeviceCanvas(deviceMode = settings.deviceMode) { canvasWidth, canvasHeight ->
// safeDrawingPadding reserves status-bar + navigation-bar + cutout
@@ -111,7 +111,7 @@ fun XuneRoot() {
) {
MiniPlayer(canvasWidth)
}
com.heretek.xunehd.ui.components.ContextMenuOverlay(menus)
com.heretek.dorado_hd.ui.components.ContextMenuOverlay(menus)
LockShade(visible = shaded, onUnlock = { shaded = false })
}
}
@@ -120,7 +120,7 @@ fun XuneRoot() {
BackHandler {
val nav = graph.nav
if (nav.current == XuneDestination.Home) {
if (nav.current == DoradoDestination.Home) {
// Let the system move the task to background.
activity?.moveTaskToBack(false)
} else {
@@ -131,7 +131,7 @@ fun XuneRoot() {
@Composable
private fun NavHost(canvasWidth: Dp, canvasHeight: Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val nav = graph.nav
val target = nav.current to nav.depth
@@ -140,53 +140,53 @@ private fun NavHost(canvasWidth: Dp, canvasHeight: Dp) {
transitionSpec = {
val pushing = targetState.second > initialState.second
if (pushing) {
(slideInHorizontally(XuneMotion.pivot()) { it / 2 } + fadeIn(XuneMotion.pivot())) togetherWith
(slideOutHorizontally(XuneMotion.pivot()) { -it / 8 } + fadeOut(XuneMotion.pivot()))
(slideInHorizontally(DoradoMotion.pivot()) { it / 2 } + fadeIn(DoradoMotion.pivot())) togetherWith
(slideOutHorizontally(DoradoMotion.pivot()) { -it / 8 } + fadeOut(DoradoMotion.pivot()))
} else {
(slideInHorizontally(XuneMotion.pivot()) { -it / 8 } + fadeIn(XuneMotion.pivot())) togetherWith
(slideOutHorizontally(XuneMotion.pivot()) { it / 2 } + fadeOut(XuneMotion.pivot()))
(slideInHorizontally(DoradoMotion.pivot()) { -it / 8 } + fadeIn(DoradoMotion.pivot())) togetherWith
(slideOutHorizontally(DoradoMotion.pivot()) { it / 2 } + fadeOut(DoradoMotion.pivot()))
}
},
label = "nav",
) { (destination, _) ->
when (destination) {
XuneDestination.Home, XuneDestination.Quickplay -> HomePages(canvasWidth)
XuneDestination.Music -> MusicScreen(canvasWidth)
is XuneDestination.Album -> AlbumDetailScreen(destination.albumId, canvasWidth)
is XuneDestination.Artist -> ArtistDetailScreen(destination.artistId, canvasWidth)
is XuneDestination.Genre -> GenreScreen(destination.genre, canvasWidth)
is XuneDestination.PlaylistDetail -> PlaylistDetailScreen(destination.playlistId, canvasWidth)
XuneDestination.NowPlaying -> NowPlayingScreen(canvasWidth)
XuneDestination.Settings -> SettingsScreen(canvasWidth)
XuneDestination.Videos -> VideosScreen(canvasWidth)
XuneDestination.Pictures -> PicturesScreen(canvasWidth)
XuneDestination.Radio -> RadioScreen(canvasWidth)
XuneDestination.Podcasts -> PodcastsScreen(canvasWidth)
is XuneDestination.PodcastFeed -> PodcastFeedScreen(destination.feedId, canvasWidth)
XuneDestination.Marketplace -> MarketplaceScreen(canvasWidth)
XuneDestination.Social -> SocialScreen(canvasWidth)
XuneDestination.Internet -> InternetScreen(canvasWidth)
is XuneDestination.MiniApp -> MiniAppScreen(destination.appId, canvasWidth)
is XuneDestination.PictureDetail -> PictureDetailScreen(destination.uri)
DoradoDestination.Home, DoradoDestination.Quickplay -> HomePages(canvasWidth)
DoradoDestination.Music -> MusicScreen(canvasWidth)
is DoradoDestination.Album -> AlbumDetailScreen(destination.albumId, canvasWidth)
is DoradoDestination.Artist -> ArtistDetailScreen(destination.artistId, canvasWidth)
is DoradoDestination.Genre -> GenreScreen(destination.genre, canvasWidth)
is DoradoDestination.PlaylistDetail -> PlaylistDetailScreen(destination.playlistId, canvasWidth)
DoradoDestination.NowPlaying -> NowPlayingScreen(canvasWidth)
DoradoDestination.Settings -> SettingsScreen(canvasWidth)
DoradoDestination.Videos -> VideosScreen(canvasWidth)
DoradoDestination.Pictures -> PicturesScreen(canvasWidth)
DoradoDestination.Radio -> RadioScreen(canvasWidth)
DoradoDestination.Podcasts -> PodcastsScreen(canvasWidth)
is DoradoDestination.PodcastFeed -> PodcastFeedScreen(destination.feedId, canvasWidth)
DoradoDestination.Marketplace -> MarketplaceScreen(canvasWidth)
DoradoDestination.Social -> SocialScreen(canvasWidth)
DoradoDestination.Internet -> InternetScreen(canvasWidth)
is DoradoDestination.MiniApp -> MiniAppScreen(destination.appId, canvasWidth)
is DoradoDestination.PictureDetail -> PictureDetailScreen(destination.uri)
}
}
}
@Composable
private fun MiniPlayer(canvasWidth: Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val nav = graph.nav
val controller = graph.controller
val nowPlaying by controller.nowPlaying.collectAsState()
val isPlaying by controller.isPlaying.collectAsState()
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
if (nav.current == XuneDestination.NowPlaying || nowPlaying == null) return
if (nav.current == DoradoDestination.NowPlaying || nowPlaying == null) return
Box(
Modifier
.fillMaxWidth()
.height(XuneTokens.MINI_PLAYER_HEIGHT.dp)
.height(DoradoTokens.MINI_PLAYER_HEIGHT.dp)
.background(colors.elevated.copy(alpha = 0.95f)),
) {
Box(
@@ -199,8 +199,8 @@ private fun MiniPlayer(canvasWidth: Dp) {
Row(
Modifier
.fillMaxSize()
.clickable { nav.push(XuneDestination.NowPlaying) }
.padding(horizontal = XuneTokens.EDGE.dp),
.clickable { nav.push(DoradoDestination.NowPlaying) }
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
AlbumArt(
@@ -210,7 +210,7 @@ private fun MiniPlayer(canvasWidth: Dp) {
)
EdgeCropText(
text = "${nowPlaying?.title ?: ""} — ${nowPlaying?.artist ?: ""}",
fontSize = XuneTokens.TYPE_LIST_SECONDARY.dp,
fontSize = DoradoTokens.TYPE_LIST_SECONDARY.dp,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp, end = 8.dp),
@@ -0,0 +1,8 @@
package com.heretek.dorado_hd.ui
import androidx.compose.runtime.staticCompositionLocalOf
import com.heretek.dorado_hd.DoradoGraph
val LocalDoradoGraph = staticCompositionLocalOf<DoradoGraph> {
error("DoradoGraph not provided")
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.apps
package com.heretek.dorado_hd.ui.apps
/**
* Pure expression evaluator used by the Calculator mini-app. Supports
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.apps
package com.heretek.dorado_hd.ui.apps
/**
* Static guitar chord data for the Chord Finder mini-app (canon §8 — a
@@ -0,0 +1,67 @@
package com.heretek.dorado_hd.ui.apps
import androidx.compose.runtime.Composable
import com.heretek.dorado_hd.ui.LocalDoradoGraph
/**
* A single mini-app: id, label, category, and a composable renderer that
* reads the DoradoGraph from [LocalDoradoGraph] and calls `nav.pop()` itself
* (e.g. via the cropped-header scaffold).
*/
data class DoradoMiniApp(
val id: String,
val title: String,
val category: String,
val render: @Composable () -> Unit,
)
/**
* The registry of installed mini-apps (canon §8). Built once at first access;
* `byId` is used by the marketplace apps pivot and the destination router.
*/
object DoradoApps {
val all: List<DoradoMiniApp> by lazy { buildAppRegistry() }
fun byId(id: String): DoradoMiniApp? = all.firstOrNull { it.id == id }
}
/** The master list of installed mini-apps (Phase 3/4 append here). */
fun buildAppRegistry(): List<DoradoMiniApp> = listOf(
DoradoMiniApp("calculator", "calculator", "utilities", { CalculatorApp() }),
DoradoMiniApp("notes", "notes", "utilities", { NotesApp() }),
DoradoMiniApp("stopwatch", "stopwatch", "utilities", { StopwatchApp() }),
DoradoMiniApp("metronome", "metronome", "music", { MetronomeApp() }),
DoradoMiniApp("alarm", "alarm clock", "utilities", { AlarmClockApp() }),
DoradoMiniApp("calendar", "calendar", "utilities", { CalendarApp() }),
DoradoMiniApp("level", "level", "utilities", { LevelApp() }),
DoradoMiniApp("piano", "piano", "music", { PianoApp() }),
DoradoMiniApp("drummachine", "drum machine", "music", { DrumMachineApp() }),
DoradoMiniApp("chordfinder", "chord finder", "music", { ChordFinderApp() }),
DoradoMiniApp("musicquiz", "music quiz", "music", { MusicQuizApp() }),
DoradoMiniApp("shufflebyalbum", "shuffle by album", "music", { ShuffleByAlbumApp() }),
DoradoMiniApp("solitaire", "solitaire", "games", { com.heretek.dorado_hd.ui.apps.games.SolitaireApp() }),
DoradoMiniApp("sudoku", "sudoku", "games", { com.heretek.dorado_hd.ui.apps.games.SudokuApp() }),
DoradoMiniApp("hexic", "hexic", "games", { com.heretek.dorado_hd.ui.apps.games.HexicApp() }),
DoradoMiniApp("reversi", "reversi", "games", { com.heretek.dorado_hd.ui.apps.games.ReversiApp() }),
DoradoMiniApp("hearts", "hearts", "games", { com.heretek.dorado_hd.ui.apps.games.HeartsApp() }),
DoradoMiniApp("spades", "spades", "games", { com.heretek.dorado_hd.ui.apps.games.SpadesApp() }),
DoradoMiniApp("checkers", "checkers", "games", { com.heretek.dorado_hd.ui.apps.games.CheckersApp() }),
DoradoMiniApp("chess", "chess", "games", { com.heretek.dorado_hd.ui.apps.games.ChessApp() }),
DoradoMiniApp("texasholdem", "texas hold 'em", "games", { com.heretek.dorado_hd.ui.apps.games.TexasHoldemApp() }),
DoradoMiniApp("weather", "weather", "reading", { com.heretek.dorado_hd.ui.apps.mocks.WeatherMock() }),
DoradoMiniApp("twitter", "twitter", "social", { com.heretek.dorado_hd.ui.apps.mocks.TwitterMock() }),
DoradoMiniApp("facebook", "facebook", "social", { com.heretek.dorado_hd.ui.apps.mocks.FacebookMock() }),
DoradoMiniApp("email", "email", "networking", { com.heretek.dorado_hd.ui.apps.mocks.EmailMock() }),
DoradoMiniApp("messenger", "messenger", "networking", { com.heretek.dorado_hd.ui.apps.mocks.MessengerMock() }),
DoradoMiniApp("msnmoney", "msn money", "reading", { com.heretek.dorado_hd.ui.apps.mocks.MsnMoneyMock() }),
DoradoMiniApp("zunereader", "zune reader", "reading", { com.heretek.dorado_hd.ui.apps.mocks.ZuneReaderMock() }),
DoradoMiniApp("zunesocial", "social", "social", { com.heretek.dorado_hd.ui.apps.mocks.SocialMock() }),
)
/** Mini-app scaffold: cropped-header back, fullscreen content. */
@Composable
fun MiniAppScaffold(title: String, onBack: () -> Unit, content: @Composable () -> Unit) {
com.heretek.dorado_hd.ui.components.DetailScaffold(title = title) {
content()
}
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.apps
package com.heretek.dorado_hd.ui.apps
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@@ -35,17 +35,17 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.data.db.NoteEntity
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.design.components.KineticList
import com.heretek.xunehd.design.components.firstLetterOf
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.xunehd.ui.components.LocalContextMenu
import com.heretek.xunehd.ui.components.MenuAction
import com.heretek.dorado_hd.data.db.NoteEntity
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.design.components.KineticList
import com.heretek.dorado_hd.design.components.firstLetterOf
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.DetailScaffold
import com.heretek.dorado_hd.ui.components.LocalContextMenu
import com.heretek.dorado_hd.ui.components.MenuAction
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.abs
@@ -54,7 +54,7 @@ import kotlin.math.abs
@Composable
fun CalculatorApp() {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
var expr by remember { mutableStateOf("") }
var result by remember { mutableStateOf<Double?>(null) }
var scientific by remember { mutableStateOf(false) }
@@ -62,13 +62,13 @@ fun CalculatorApp() {
val rows = if (scientific) SCIENTIFIC_KEYS else BASIC_KEYS
DetailScaffold(title = "calculator") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(
text = result?.let { formatNumber(it) } ?: expr.ifEmpty { "0" },
style = TextStyle(
fontFamily = Selawik,
fontWeight = FontWeight.Light,
fontSize = XuneTokens.TYPE_HEADER_CROP_VISIBLE.sp * 1.4f,
fontSize = DoradoTokens.TYPE_HEADER_CROP_VISIBLE.sp * 1.4f,
color = if (result != null) colors.accent else colors.textPrimary,
),
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
@@ -143,7 +143,7 @@ private val SCIENTIFIC_KEYS = listOf(
@Composable
private fun KeyButton(label: String, onClick: (String) -> Unit) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val accent = label in setOf("=", "C", "sci")
Box(
Modifier
@@ -157,7 +157,7 @@ private fun KeyButton(label: String, onClick: (String) -> Unit) {
text = label,
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_LIST.sp,
fontSize = DoradoTokens.TYPE_LIST.sp,
color = if (accent) colors.accent else colors.textPrimary,
),
)
@@ -168,7 +168,7 @@ private fun KeyButton(label: String, onClick: (String) -> Unit) {
@Composable
fun NotesApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val menus = LocalContextMenu.current
var selected by remember { mutableStateOf<NoteEntity?>(null) }
val notes by graph.notes.notes().collectAsState(initial = emptyList())
@@ -179,7 +179,7 @@ fun NotesApp() {
var title by remember(s.id) { mutableStateOf(s.title) }
var body by remember(s.id) { mutableStateOf(s.body) }
DetailScaffold(title = "notes") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
EditableLine(value = title, onChange = { title = it }, placeholder = "title")
Spacer(Modifier.height(8.dp))
EditableLine(value = body, onChange = { body = it }, placeholder = "body", multiLine = true, modifier = Modifier.weight(1f))
@@ -190,7 +190,7 @@ fun NotesApp() {
},
onLongClick = {},
).padding(vertical = 8.dp)) {
EdgeCropText(text = "save", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = "save", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
Spacer(Modifier.height(4.dp))
Box(Modifier.fillMaxWidth().combinedClickable(
@@ -199,7 +199,7 @@ fun NotesApp() {
},
onLongClick = {},
).padding(vertical = 8.dp)) {
EdgeCropText(text = "delete", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.textInactive)
EdgeCropText(text = "delete", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.textInactive)
}
}
}
@@ -211,13 +211,13 @@ fun NotesApp() {
Box(
Modifier
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp)
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp)
.combinedClickable(
onClick = { scope.launch { graph.notes.add("untitled", "") } },
onLongClick = {},
),
) {
EdgeCropText(text = "+ new note", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = "+ new note", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
KineticList(
items = notes,
@@ -227,7 +227,7 @@ fun NotesApp() {
Box(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = { selected = n },
onLongClick = {
@@ -237,10 +237,10 @@ fun NotesApp() {
)
},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
contentAlignment = Alignment.CenterStart,
) {
EdgeCropText(text = n.title, fontSize = XuneTokens.TYPE_LIST.dp, modifier = Modifier.fillMaxWidth())
EdgeCropText(text = n.title, fontSize = DoradoTokens.TYPE_LIST.dp, modifier = Modifier.fillMaxWidth())
}
},
)
@@ -256,7 +256,7 @@ fun EditableLine(
multiLine: Boolean = false,
modifier: Modifier = Modifier,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
var local by remember(value) { mutableStateOf(value) }
androidx.compose.foundation.text.BasicTextField(
value = local,
@@ -267,7 +267,7 @@ fun EditableLine(
singleLine = !multiLine,
textStyle = TextStyle(
fontFamily = Selawik,
fontSize = if (multiLine) XuneTokens.TYPE_LIST.sp else XuneTokens.TYPE_NOW_META.sp,
fontSize = if (multiLine) DoradoTokens.TYPE_LIST.sp else DoradoTokens.TYPE_NOW_META.sp,
color = colors.textPrimary,
),
cursorBrush = androidx.compose.ui.graphics.SolidColor(colors.accent),
@@ -277,7 +277,7 @@ fun EditableLine(
.padding(8.dp),
)
if (local.isEmpty() && placeholder.isNotEmpty()) {
EdgeCropText(text = placeholder, fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.4f, modifier = Modifier.padding(start = 8.dp, top = 4.dp))
EdgeCropText(text = placeholder, fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.4f, modifier = Modifier.padding(start = 8.dp, top = 4.dp))
}
}
@@ -285,7 +285,7 @@ fun EditableLine(
@Composable
fun StopwatchApp() {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
var running by remember { mutableStateOf(false) }
var startedAt by remember { mutableStateOf(0L) }
var accumulated by remember { mutableStateOf(0L) }
@@ -300,13 +300,13 @@ fun StopwatchApp() {
}
DetailScaffold(title = "stopwatch") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(
text = formatTime(nowMs / 1000),
style = TextStyle(
fontFamily = Selawik,
fontWeight = FontWeight.Light,
fontSize = XuneTokens.TYPE_HEADER_CROPPED.sp,
fontSize = DoradoTokens.TYPE_HEADER_CROPPED.sp,
color = colors.textPrimary,
),
modifier = Modifier.fillMaxWidth(),
@@ -315,7 +315,7 @@ fun StopwatchApp() {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
EdgeCropText(
text = if (running) "stop" else "start",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = colors.accent,
modifier = Modifier.combinedClickable(
onClick = {
@@ -332,7 +332,7 @@ fun StopwatchApp() {
)
EdgeCropText(
text = "lap",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = if (running) colors.accent else colors.textInactive,
modifier = Modifier.combinedClickable(
enabled = running,
@@ -342,7 +342,7 @@ fun StopwatchApp() {
)
EdgeCropText(
text = "reset",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = colors.textInactive,
modifier = Modifier.combinedClickable(
onClick = { running = false; accumulated = 0; laps.clear(); nowMs = 0 },
@@ -353,7 +353,7 @@ fun StopwatchApp() {
Spacer(Modifier.height(8.dp))
// Show laps in reverse order.
laps.asReversed().forEach { lap ->
EdgeCropText(text = formatTime(lap / 1000), fontSize = XuneTokens.TYPE_LIST.dp, alpha = 0.7f, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp))
EdgeCropText(text = formatTime(lap / 1000), fontSize = DoradoTokens.TYPE_LIST.dp, alpha = 0.7f, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp))
}
}
}
@@ -369,7 +369,7 @@ private fun formatTime(ms: Long): String {
@Composable
fun MetronomeApp() {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
var bpm by remember { mutableStateOf(120) }
var beatsPerBar by remember { mutableStateOf(4) }
var running by remember { mutableStateOf(false) }
@@ -383,13 +383,13 @@ fun MetronomeApp() {
}
DetailScaffold(title = "metronome") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(
text = "$bpm bpm",
style = TextStyle(
fontFamily = Selawik,
fontWeight = FontWeight.Light,
fontSize = XuneTokens.TYPE_HEADER_CROP_VISIBLE.sp * 2,
fontSize = DoradoTokens.TYPE_HEADER_CROP_VISIBLE.sp * 2,
color = colors.textPrimary,
),
modifier = Modifier.fillMaxWidth(),
@@ -399,7 +399,7 @@ fun MetronomeApp() {
(-5..5 step 1).forEach { step ->
EdgeCropText(
text = if (step > 0) "+$step" else "$step",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = if (step == 0) colors.textInactive else colors.accent,
modifier = Modifier.combinedClickable(
onClick = { bpm = (bpm + step * 2).coerceIn(30, 300) },
@@ -413,7 +413,7 @@ fun MetronomeApp() {
listOf(2, 3, 4, 6).forEach { n ->
EdgeCropText(
text = "$n/4",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = if (n == beatsPerBar) colors.accent else colors.textInactive,
modifier = Modifier.combinedClickable(
onClick = { beatsPerBar = n },
@@ -425,7 +425,7 @@ fun MetronomeApp() {
Spacer(Modifier.height(12.dp))
EdgeCropText(
text = if (running) "stop" else "start",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = colors.accent,
modifier = Modifier.combinedClickable(
onClick = {
@@ -442,7 +442,7 @@ fun MetronomeApp() {
).padding(vertical = 4.dp),
)
Spacer(Modifier.height(8.dp))
EdgeCropText(text = "beat $beat", fontSize = XuneTokens.TYPE_LIST.dp, alpha = 0.6f, modifier = Modifier.fillMaxWidth())
EdgeCropText(text = "beat $beat", fontSize = DoradoTokens.TYPE_LIST.dp, alpha = 0.6f, modifier = Modifier.fillMaxWidth())
// Lightweight beat pulse: every click increments beat via a separate ticker.
LaunchedEffect(running) {
while (running) {
@@ -458,7 +458,7 @@ fun MetronomeApp() {
@Composable
fun LevelApp() {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
var xDeg by remember { mutableStateOf(0.0) }
var yDeg by remember { mutableStateOf(0.0) }
var surface by remember { mutableStateOf(false) }
@@ -483,10 +483,10 @@ fun LevelApp() {
}
DetailScaffold(title = "level") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
EdgeCropText(
text = if (surface) "surface" else "bubble",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = colors.accent,
modifier = Modifier.combinedClickable(onClick = { surface = !surface }, onLongClick = {}),
)
@@ -500,7 +500,7 @@ fun LevelApp() {
style = TextStyle(
fontFamily = Selawik,
fontWeight = FontWeight.Light,
fontSize = XuneTokens.TYPE_HEADER_CROPPED.sp,
fontSize = DoradoTokens.TYPE_HEADER_CROPPED.sp,
color = colors.textPrimary,
),
)
@@ -514,7 +514,7 @@ fun LevelApp() {
@Composable
private fun BubbleLevel(xDeg: Double, yDeg: Double) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
Box(
Modifier
.fillMaxSize()
@@ -544,17 +544,17 @@ fun ChordFinderApp() {
var root by remember { mutableStateOf("C") }
var quality by remember { mutableStateOf("major") }
DetailScaffold(title = "chord finder") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ChordData.ROOTS.forEach { r ->
EdgeCropText(text = r, fontSize = XuneTokens.TYPE_LIST.dp, color = if (r == root) LocalXuneColors.current.accent else LocalXuneColors.current.textPrimary,
EdgeCropText(text = r, fontSize = DoradoTokens.TYPE_LIST.dp, color = if (r == root) LocalDoradoColors.current.accent else LocalDoradoColors.current.textPrimary,
modifier = Modifier.combinedClickable(onClick = { root = r }, onLongClick = {}))
}
}
Spacer(Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ChordData.QUALITIES.forEach { q ->
EdgeCropText(text = q, fontSize = XuneTokens.TYPE_LIST.dp, color = if (q == quality) LocalXuneColors.current.accent else LocalXuneColors.current.textPrimary,
EdgeCropText(text = q, fontSize = DoradoTokens.TYPE_LIST.dp, color = if (q == quality) LocalDoradoColors.current.accent else LocalDoradoColors.current.textPrimary,
modifier = Modifier.combinedClickable(onClick = { quality = q }, onLongClick = {}))
}
}
@@ -566,7 +566,7 @@ fun ChordFinderApp() {
@Composable
private fun Fretboard(frets: IntArray) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val width = 200.dp
val height = 90.dp
androidx.compose.foundation.Canvas(modifier = Modifier.fillMaxWidth().height(height)) {
@@ -598,17 +598,17 @@ private fun Fretboard(frets: IntArray) {
@Composable
fun ShuffleByAlbumApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val albums by graph.library.albums().collectAsState(initial = emptyList())
var pick by remember { mutableStateOf<com.heretek.xunehd.data.model.Album?>(null) }
var pick by remember { mutableStateOf<com.heretek.dorado_hd.data.model.Album?>(null) }
DetailScaffold(title = "shuffle by album") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
EdgeCropText(
text = "shuffle",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = colors.accent,
modifier = Modifier.combinedClickable(
onClick = {
@@ -618,16 +618,16 @@ fun ShuffleByAlbumApp() {
),
)
pick?.let { album ->
com.heretek.xunehd.design.components.AlbumArt(
com.heretek.dorado_hd.design.components.AlbumArt(
model = album.albumArtUri,
contentDescription = album.title,
modifier = Modifier.size(120.dp),
)
EdgeCropText(text = album.title, fontSize = XuneTokens.TYPE_NOW_META.dp)
EdgeCropText(text = album.artist, fontSize = XuneTokens.TYPE_LIST.dp, color = colors.textSecondary)
EdgeCropText(text = album.title, fontSize = DoradoTokens.TYPE_NOW_META.dp)
EdgeCropText(text = album.artist, fontSize = DoradoTokens.TYPE_LIST.dp, color = colors.textSecondary)
EdgeCropText(
text = "play",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = colors.accent,
modifier = Modifier.combinedClickable(
onClick = {
@@ -648,7 +648,7 @@ fun ShuffleByAlbumApp() {
@Composable
fun MusicQuizApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val tracks by graph.library.tracks().collectAsState(initial = emptyList())
var question by remember { mutableStateOf<QuizEngine.Question?>(null) }
@@ -662,10 +662,10 @@ fun MusicQuizApp() {
}
DetailScaffold(title = "music quiz") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
val q = question
if (q == null) {
EdgeCropText(text = "loading…", fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.4f)
EdgeCropText(text = "loading…", fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.4f)
return@Column
}
BasicText(
@@ -673,8 +673,8 @@ fun MusicQuizApp() {
style = TextStyle(
fontFamily = Selawik,
fontWeight = FontWeight.Light,
fontSize = XuneTokens.TYPE_HEADER_CROP_VISIBLE.sp * 1.2f,
color = LocalXuneColors.current.textPrimary,
fontSize = DoradoTokens.TYPE_HEADER_CROP_VISIBLE.sp * 1.2f,
color = LocalDoradoColors.current.textPrimary,
),
modifier = Modifier.fillMaxWidth(),
)
@@ -682,8 +682,8 @@ fun MusicQuizApp() {
q.options.forEachIndexed { i, opt ->
EdgeCropText(
text = "${i + 1}. $opt",
fontSize = XuneTokens.TYPE_LIST.dp,
color = LocalXuneColors.current.accent,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = LocalDoradoColors.current.accent,
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
@@ -701,7 +701,7 @@ fun MusicQuizApp() {
)
}
Spacer(Modifier.height(8.dp))
EdgeCropText(text = "correct $correct — wrong $wrong", fontSize = XuneTokens.TYPE_CAPTION.dp, alpha = 0.6f)
EdgeCropText(text = "correct $correct — wrong $wrong", fontSize = DoradoTokens.TYPE_CAPTION.dp, alpha = 0.6f)
}
}
}
@@ -710,19 +710,19 @@ fun MusicQuizApp() {
@Composable
fun AlarmClockApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val context = androidx.compose.ui.platform.LocalContext.current
val menus = LocalContextMenu.current
val alarms by graph.alarms.alarms().collectAsState(initial = emptyList())
fun schedule(alarm: com.heretek.xunehd.data.db.AlarmEntity) {
fun schedule(alarm: com.heretek.dorado_hd.data.db.AlarmEntity) {
val am = context.getSystemService(android.content.Context.ALARM_SERVICE) as android.app.AlarmManager
val triggerAt = com.heretek.xunehd.media.AlarmScheduler.nextFireMs(alarm.hour, alarm.minute)
val triggerAt = com.heretek.dorado_hd.media.AlarmScheduler.nextFireMs(alarm.hour, alarm.minute)
val pi = android.app.PendingIntent.getBroadcast(
context,
alarm.id.toInt(),
android.content.Intent(context, com.heretek.xunehd.media.AlarmReceiver::class.java).apply {
android.content.Intent(context, com.heretek.dorado_hd.media.AlarmReceiver::class.java).apply {
putExtra("alarmId", alarm.id)
putExtra("alarmKind", alarm.alarmKind)
putExtra("refId", alarm.refId)
@@ -737,23 +737,23 @@ fun AlarmClockApp() {
Box(
Modifier
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp)
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp)
.combinedClickable(
onClick = {
scope.launch {
val id = graph.alarms.add(
com.heretek.xunehd.data.db.AlarmEntity(
com.heretek.dorado_hd.data.db.AlarmEntity(
hour = 7, minute = 0, enabled = true,
label = "alarm", alarmKind = 0, refId = 0, daysOfWeek = 0,
),
)
schedule(com.heretek.xunehd.data.db.AlarmEntity(id = id, hour = 7, minute = 0, enabled = true, label = "alarm", alarmKind = 0, refId = 0, daysOfWeek = 0))
schedule(com.heretek.dorado_hd.data.db.AlarmEntity(id = id, hour = 7, minute = 0, enabled = true, label = "alarm", alarmKind = 0, refId = 0, daysOfWeek = 0))
}
},
onLongClick = {},
),
) {
EdgeCropText(text = "+ new alarm", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = "+ new alarm", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
KineticList(
items = alarms,
@@ -763,7 +763,7 @@ fun AlarmClockApp() {
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = {
scope.launch {
@@ -774,7 +774,7 @@ fun AlarmClockApp() {
android.app.PendingIntent.getBroadcast(
context,
alarm.id.toInt(),
android.content.Intent(context, com.heretek.xunehd.media.AlarmReceiver::class.java),
android.content.Intent(context, com.heretek.dorado_hd.media.AlarmReceiver::class.java),
android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE,
),
)
@@ -798,18 +798,18 @@ fun AlarmClockApp() {
)
},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(
text = "%02d:%02d".format(alarm.hour, alarm.minute),
fontSize = XuneTokens.TYPE_NOW_META.dp,
color = if (alarm.enabled) LocalXuneColors.current.textPrimary else LocalXuneColors.current.textInactive,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
color = if (alarm.enabled) LocalDoradoColors.current.textPrimary else LocalDoradoColors.current.textInactive,
)
EdgeCropText(text = alarm.label, fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = alarm.label, fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.textSecondary)
}
EdgeCropText(text = if (alarm.enabled) "on" else "off", fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = if (alarm.enabled) "on" else "off", fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
},
)
@@ -821,20 +821,20 @@ fun AlarmClockApp() {
@Composable
fun CalendarApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val menus = LocalContextMenu.current
var monthStart by remember { mutableStateOf(java.time.YearMonth.now()) }
val appts by graph.calendar.appointments().collectAsState(initial = emptyList())
DetailScaffold(title = "calendar") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
EdgeCropText(text = "<", fontSize = XuneTokens.TYPE_NOW_META.dp, color = LocalXuneColors.current.accent,
EdgeCropText(text = "<", fontSize = DoradoTokens.TYPE_NOW_META.dp, color = LocalDoradoColors.current.accent,
modifier = Modifier.combinedClickable(onClick = { monthStart = monthStart.minusMonths(1) }, onLongClick = {}))
EdgeCropText(text = "%s %d".format(monthStart.month.name.lowercase().replaceFirstChar { it.uppercase() }, monthStart.year), fontSize = XuneTokens.TYPE_NOW_META.dp,
EdgeCropText(text = "%s %d".format(monthStart.month.name.lowercase().replaceFirstChar { it.uppercase() }, monthStart.year), fontSize = DoradoTokens.TYPE_NOW_META.dp,
modifier = Modifier.weight(1f))
EdgeCropText(text = ">", fontSize = XuneTokens.TYPE_NOW_META.dp, color = LocalXuneColors.current.accent,
EdgeCropText(text = ">", fontSize = DoradoTokens.TYPE_NOW_META.dp, color = LocalDoradoColors.current.accent,
modifier = Modifier.combinedClickable(onClick = { monthStart = monthStart.plusMonths(1) }, onLongClick = {}))
}
Spacer(Modifier.height(8.dp))
@@ -844,7 +844,7 @@ fun CalendarApp() {
}
@Composable
private fun MonthGrid(monthStart: java.time.YearMonth, appts: List<com.heretek.xunehd.data.db.AppointmentEntity>) {
private fun MonthGrid(monthStart: java.time.YearMonth, appts: List<com.heretek.dorado_hd.data.db.AppointmentEntity>) {
val firstOfMonth = monthStart.atDay(1)
val daysInMonth = monthStart.lengthOfMonth()
val firstWeekday = (firstOfMonth.dayOfWeek.value % 7) // Sunday=0
@@ -853,9 +853,9 @@ private fun MonthGrid(monthStart: java.time.YearMonth, appts: List<com.heretek.x
val apptsByDay = appts.groupBy {
java.time.Instant.ofEpochMilli(it.startAt).atZone(java.time.ZoneId.systemDefault()).toLocalDate().dayOfMonth
}
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val scope = rememberCoroutineScope()
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val menus = LocalContextMenu.current
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
@@ -891,7 +891,7 @@ private fun MonthGrid(monthStart: java.time.YearMonth, appts: List<com.heretek.x
text = if (valid) dayIndex.toString() else "",
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_LIST.sp,
fontSize = DoradoTokens.TYPE_LIST.sp,
color = if (valid && apptsByDay.containsKey(dayIndex)) colors.accent else colors.textPrimary,
),
)
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.apps
package com.heretek.dorado_hd.ui.apps
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
@@ -28,10 +28,10 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.ui.components.DetailScaffold
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
@@ -44,7 +44,7 @@ private val PIANO_WHITE_FREQS = listOf(
@Composable
fun PianoApp() {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val scope = rememberCoroutineScope()
val synth = remember { MiniSynth(scope) }
DisposableEffect(Unit) {
@@ -54,13 +54,13 @@ fun PianoApp() {
var lastPlayed by remember { mutableStateOf<Int?>(null) }
DetailScaffold(title = "piano") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Box(Modifier.fillMaxWidth().height(XuneTokens.PIANO_KEY_H.dp).background(colors.elevated)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Box(Modifier.fillMaxWidth().height(DoradoTokens.PIANO_KEY_H.dp).background(colors.elevated)) {
Row(Modifier.fillMaxSize()) {
repeat(14) { i ->
Box(
Modifier
.size(width = XuneTokens.PIANO_KEY_W.dp, height = XuneTokens.PIANO_KEY_H.dp)
.size(width = DoradoTokens.PIANO_KEY_W.dp, height = DoradoTokens.PIANO_KEY_H.dp)
.background(Color.White)
.pointerInput(i) {
detectTapGestures(onPress = {
@@ -77,7 +77,7 @@ fun PianoApp() {
if (lastPlayed != null) {
BasicText(
text = "playing ${(lastPlayed ?: 0) + 1}",
style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.accent),
style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.accent),
)
}
}
@@ -88,7 +88,7 @@ fun PianoApp() {
@Composable
fun DrumMachineApp() {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val scope = rememberCoroutineScope()
val synth = remember { MiniSynth(scope) }
var bpm by remember { mutableStateOf(120) }
@@ -123,16 +123,16 @@ fun DrumMachineApp() {
}
DetailScaffold(title = "drum machine") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
BasicText(
text = "$bpm bpm",
style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.textPrimary),
style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.textPrimary),
modifier = Modifier.weight(1f),
)
BasicText(
text = if (playing) "stop" else "start",
style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.accent),
style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.accent),
modifier = Modifier.pointerInput(Unit) { detectTapGestures { playing = !playing } },
)
}
@@ -144,7 +144,7 @@ fun DrumMachineApp() {
) {
BasicText(
text = name,
style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = colors.textSecondary),
style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = colors.textSecondary),
modifier = Modifier.size(width = 32.dp, height = 24.dp),
)
repeat(16) { stepIdx ->
@@ -1,6 +1,6 @@
package com.heretek.xunehd.ui.apps
package com.heretek.dorado_hd.ui.apps
import com.heretek.xunehd.data.model.Track
import com.heretek.dorado_hd.data.model.Track
import kotlin.random.Random
/**
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.apps
package com.heretek.dorado_hd.ui.apps
import android.media.AudioAttributes
import android.media.AudioFormat
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.apps.games
package com.heretek.dorado_hd.ui.apps.games
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
@@ -31,11 +31,11 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.DetailScaffold
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.random.Random
@@ -237,7 +237,7 @@ object ReversiEngine {
@Composable
fun SolitaireApp() {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
var piles by remember { mutableStateOf(SolitaireEngine.newGame()) }
val tabs = piles.subList(0, 7)
val stock = piles[7]; val waste = piles[8]; val founds = piles.subList(9, 13)
@@ -288,16 +288,16 @@ fun SolitaireApp() {
}
DetailScaffold(title = "solitaire") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
BasicText(
text = if (stock.cards.isEmpty()) "recycle" else "deal",
style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.accent),
style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.accent),
modifier = Modifier.pointerInput(Unit) { detectTapGestures(onTap = { tapStock() }) }.padding(end = 12.dp),
)
BasicText(
text = "waste ${waste.cards.size}",
style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = colors.textSecondary),
style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = colors.textSecondary),
)
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(2.dp)) {
@@ -330,12 +330,12 @@ fun SolitaireApp() {
@Composable
fun SolCardView(card: SolCard, onClick: () -> Unit) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val bg = if (card.faceUp) Color.White else colors.tile
val fg = if (card.suit.red) colors.accent else Color.White
Box(
Modifier
.size(width = XuneTokens.CARD_W.dp, height = XuneTokens.CARD_H.dp)
.size(width = DoradoTokens.CARD_W.dp, height = DoradoTokens.CARD_H.dp)
.background(bg)
.padding(2.dp)
.pointerInput(Unit) { detectTapGestures(onTap = { onClick() }) },
@@ -351,8 +351,8 @@ fun SolCardView(card: SolCard, onClick: () -> Unit) {
@Composable
fun SudokuApp() {
val colors = LocalXuneColors.current
val graph = LocalXuneGraph.current
val colors = LocalDoradoColors.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val board by remember { mutableStateOf(SudokuEngine.newGame()) }
val cells = remember { mutableStateListOf<Int>().apply { repeat(81) { add(board.rows.flatten()[it]) } } }
@@ -390,10 +390,10 @@ fun SudokuApp() {
}
DetailScaffold(title = "sudoku") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(
text = "%02d:%02d".format(elapsed / 60, elapsed % 60),
style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.textPrimary),
style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.textPrimary),
)
Spacer(Modifier.height(4.dp))
for (r in 0 until 9) {
@@ -405,7 +405,7 @@ fun SudokuApp() {
val isSel = selected == idx
Box(
Modifier
.size(width = XuneTokens.SUDOKU_CELL.dp, height = XuneTokens.SUDOKU_CELL.dp)
.size(width = DoradoTokens.SUDOKU_CELL.dp, height = DoradoTokens.SUDOKU_CELL.dp)
.background(
when {
isSel -> colors.accent
@@ -420,7 +420,7 @@ fun SudokuApp() {
text = v.toString(),
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_LIST.sp,
fontSize = DoradoTokens.TYPE_LIST.sp,
color = if (isGiven) colors.textPrimary else colors.accent,
),
)
@@ -434,22 +434,22 @@ fun SudokuApp() {
(1..9).forEach { n ->
Box(
Modifier
.size(width = XuneTokens.SUDOKU_CELL.dp, height = XuneTokens.SUDOKU_CELL.dp)
.size(width = DoradoTokens.SUDOKU_CELL.dp, height = DoradoTokens.SUDOKU_CELL.dp)
.background(colors.elevated)
.pointerInput(n) { detectTapGestures(onTap = { if (selected >= 0) setCell(selected, n) }) },
contentAlignment = androidx.compose.ui.Alignment.Center,
) {
BasicText(text = n.toString(), style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.textPrimary))
BasicText(text = n.toString(), style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.textPrimary))
}
}
Box(
Modifier
.size(width = XuneTokens.SUDOKU_CELL.dp, height = XuneTokens.SUDOKU_CELL.dp)
.size(width = DoradoTokens.SUDOKU_CELL.dp, height = DoradoTokens.SUDOKU_CELL.dp)
.background(colors.elevated)
.pointerInput(Unit) { detectTapGestures(onTap = { if (selected >= 0) setCell(selected, 0) }) },
contentAlignment = androidx.compose.ui.Alignment.Center,
) {
BasicText(text = "x", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.textSecondary))
BasicText(text = "x", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.textSecondary))
}
}
}
@@ -458,8 +458,8 @@ fun SudokuApp() {
@Composable
fun HexicApp() {
val colors = LocalXuneColors.current
val graph = LocalXuneGraph.current
val colors = LocalDoradoColors.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val n = 7
val board = remember { HexicEngine.newBoard(n) }
@@ -480,10 +480,10 @@ fun HexicApp() {
}
DetailScaffold(title = "hexic") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
BasicText(text = "score $score best ${bestFlow.firstOrNull()?.score ?: 0}", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.accent))
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(text = "score $score best ${bestFlow.firstOrNull()?.score ?: 0}", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.accent))
Spacer(Modifier.height(8.dp))
val hexicAccent = LocalXuneColors.current.accent
val hexicAccent = LocalDoradoColors.current.accent
val hexicBg = colors.elevated
androidx.compose.foundation.Canvas(Modifier.fillMaxSize()) {
val w = size.width; val h = size.height
@@ -507,7 +507,7 @@ fun HexicApp() {
Box(
Modifier
.weight(1f)
.height(XuneTokens.HEX_RADIUS.dp * 3)
.height(DoradoTokens.HEX_RADIUS.dp * 3)
.pointerInput(r to c) { detectTapGestures(onTap = { tap(r, c) }) },
)
}
@@ -515,15 +515,15 @@ fun HexicApp() {
}
}
Spacer(Modifier.height(8.dp))
BasicText(text = "+ rotate tile • clear 3+ same-color adjacents", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = colors.textSecondary))
BasicText(text = "+ rotate tile • clear 3+ same-color adjacents", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = colors.textSecondary))
}
}
}
@Composable
fun ReversiApp() {
val colors = LocalXuneColors.current
val graph = LocalXuneGraph.current
val colors = LocalDoradoColors.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
var board by remember { mutableStateOf(ReversiEngine.newBoard()) }
var player by remember { mutableStateOf(ReversiEngine.BLACK) }
@@ -557,8 +557,8 @@ fun ReversiApp() {
val (bs, ws) = ReversiEngine.score(board)
DetailScaffold(title = "reversi") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
BasicText(text = "you $bs — ai $ws ${if (player == ReversiEngine.BLACK) "your move" else "ai…"}", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.textPrimary))
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(text = "you $bs — ai $ws ${if (player == ReversiEngine.BLACK) "your move" else "ai…"}", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.textPrimary))
Spacer(Modifier.height(8.dp))
for (r in 0 until 8) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(1.dp)) {
@@ -581,7 +581,7 @@ fun ReversiApp() {
.background(if (v == ReversiEngine.BLACK) Color.Black else Color.White),
)
} else if (isLegal) {
BasicText(text = "·", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.accent))
BasicText(text = "·", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.accent))
}
}
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.apps.games
package com.heretek.dorado_hd.ui.apps.games
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
@@ -35,11 +35,11 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.DetailScaffold
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.abs
@@ -916,10 +916,10 @@ object PokerEngine {
@Composable
fun HeartsApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
var state by remember { mutableStateOf(HeartsEngine.newGame()) }
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val seat = HeartsSeat.SOUTH
fun humanAction(s: HeartsState, action: HeartsState.() -> HeartsState) {
@@ -933,8 +933,8 @@ fun HeartsApp() {
val passCards = state.pendingPassFrom == seat
DetailScaffold(title = "hearts") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
BasicText(text = "scores: ${state.scores.entries.joinToString { "${it.key.name.lowercase()}=${it.value}" }}", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.textSecondary))
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(text = "scores: ${state.scores.entries.joinToString { "${it.key.name.lowercase()}=${it.value}" }}", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.textSecondary))
Spacer(Modifier.height(8.dp))
// N/W/E hands
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
@@ -944,7 +944,7 @@ fun HeartsApp() {
Spacer(Modifier.height(8.dp))
// Trick area
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
state.trick.forEach { (s, c) -> Column { BasicText(s.name.lowercase(), style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = colors.textSecondary)); SolCardView(c) {} } }
state.trick.forEach { (s, c) -> Column { BasicText(s.name.lowercase(), style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = colors.textSecondary)); SolCardView(c) {} } }
}
Spacer(Modifier.height(8.dp))
PlayerHand(
@@ -956,12 +956,12 @@ fun HeartsApp() {
)
if (passCards) {
Spacer(Modifier.height(8.dp))
BasicText(text = "pass 3 cards", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.accent))
BasicText(text = "pass 3 cards", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.accent))
PassPicker(hand = state.hands[seat]!!, onConfirm = { cards -> state = HeartsEngine.passCards(state, seat, cards) })
}
if (state.done) {
Spacer(Modifier.height(8.dp))
BasicText(text = "round complete", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.accent))
BasicText(text = "round complete", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.accent))
}
}
if (state.done) {
@@ -975,7 +975,7 @@ fun HeartsApp() {
@Composable
private fun PlayerHand(hand: List<SolCard>, onCard: (SolCard) -> Unit = {}, align: Alignment.Horizontal, label: String, interactive: Boolean = true) {
Column(horizontalAlignment = align) {
BasicText(text = label, style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = LocalXuneColors.current.textSecondary))
BasicText(text = label, style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = LocalDoradoColors.current.textSecondary))
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
hand.forEach { c ->
if (interactive) SolCardView(c) { onCard(c) } else SolCardView(c) {}
@@ -997,17 +997,17 @@ private fun PassPicker(hand: List<SolCard>, onConfirm: (List<SolCard>) -> Unit)
}
}
Spacer(Modifier.height(4.dp))
BasicText(text = "pass ${picked.size}/3 — ${if (picked.size == 3) "tap confirm" else "pick three"}", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = LocalXuneColors.current.accent),
BasicText(text = "pass ${picked.size}/3 — ${if (picked.size == 3) "tap confirm" else "pick three"}", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = LocalDoradoColors.current.accent),
modifier = Modifier.pointerInput(picked.toList()) { detectTapGestures(onTap = { if (picked.size == 3) onConfirm(picked.toList()) }) })
}
}
@Composable
fun SpadesApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
var state by remember { mutableStateOf(SpadesEngine.newGame()) }
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val seat = SpadesSeat.SOUTH
fun human(s: SpadesState, action: (SpadesState) -> SpadesState) {
var cur = action(s)
@@ -1023,8 +1023,8 @@ fun SpadesApp() {
state = cur
}
DetailScaffold(title = "spades") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
BasicText(text = "score ${state.score.first} / ${state.score.second}", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.accent))
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(text = "score ${state.score.first} / ${state.score.second}", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.accent))
Spacer(Modifier.height(4.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
SpadesBidSeat(state, SpadesSeat.NORTH, seat) { human(state) { s -> s } }
@@ -1033,19 +1033,19 @@ fun SpadesApp() {
Spacer(Modifier.height(4.dp))
if (!state.biddingDone) {
if (state.currentPlayer == seat) {
BasicText(text = "your bid:", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.textPrimary))
BasicText(text = "your bid:", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.textPrimary))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
(1..13).forEach { n ->
BasicText(text = "$n", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.accent),
BasicText(text = "$n", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.accent),
modifier = Modifier.pointerInput(n) { detectTapGestures(onTap = { human(state) { SpadesEngine.bid(it, n) } }) })
}
}
} else {
BasicText(text = "ai bidding…", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.textSecondary))
BasicText(text = "ai bidding…", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.textSecondary))
}
} else {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
state.trick.forEach { (s, c) -> Column { BasicText(s.name.lowercase(), style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = colors.textSecondary)); SolCardView(c) {} } }
state.trick.forEach { (s, c) -> Column { BasicText(s.name.lowercase(), style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = colors.textSecondary)); SolCardView(c) {} } }
}
Spacer(Modifier.height(4.dp))
if (state.currentPlayer == seat) {
@@ -1054,7 +1054,7 @@ fun SpadesApp() {
legal.forEach { c -> SolCardView(c) { human(state) { SpadesEngine.play(it, c) } } }
}
} else {
BasicText(text = "ai playing…", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.textSecondary))
BasicText(text = "ai playing…", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.textSecondary))
}
}
Spacer(Modifier.height(4.dp))
@@ -1072,7 +1072,7 @@ fun SpadesApp() {
@Composable
private fun SpadesBidSeat(state: SpadesState, seat: SpadesSeat, viewer: SpadesSeat, onInteract: () -> Unit) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
BasicText(text = "${seat.name.lowercase()}: ${state.bids[seat]}", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = LocalXuneColors.current.textSecondary))
BasicText(text = "${seat.name.lowercase()}: ${state.bids[seat]}", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = LocalDoradoColors.current.textSecondary))
Row(horizontalArrangement = Arrangement.spacedBy(1.dp)) {
state.hands[seat]!!.take(5).forEach { SolCardView(it) {} }
}
@@ -1081,11 +1081,11 @@ private fun SpadesBidSeat(state: SpadesState, seat: SpadesSeat, viewer: SpadesSe
@Composable
fun CheckersApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
var state by remember { mutableStateOf(CheckersEngine.newGame()) }
var selected by remember { mutableStateOf<Pair<Int, Int>?>(null) }
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val legalMoves = remember(state) { CheckersEngine.legalMoves(state, state.turn) }
val moveTo = remember(selected, legalMoves) {
selected?.let { sel -> legalMoves.filter { it.first == sel }.map { it.second } } ?: emptyList()
@@ -1096,8 +1096,8 @@ fun CheckersApp() {
selected = null
}
DetailScaffold(title = "checkers") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
BasicText(text = if (state.winner != null) "${state.winner} wins" else "${state.turn} move", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.accent))
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(text = if (state.winner != null) "${state.winner} wins" else "${state.turn} move", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.accent))
Spacer(Modifier.height(8.dp))
val ckSelected = selected
val ckMoveTo = moveTo
@@ -1136,7 +1136,7 @@ fun CheckersApp() {
}
Spacer(Modifier.height(8.dp))
Row {
BasicText(text = "you are red", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = colors.textSecondary))
BasicText(text = "you are red", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = colors.textSecondary))
}
}
}
@@ -1161,10 +1161,10 @@ fun CheckersApp() {
@Composable
fun ChessApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
var state by remember { mutableStateOf(ChessEngine.newGame()) }
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
var selected by remember { mutableStateOf<Pair<Int, Int>?>(null) }
val moves = remember(state) { ChessEngine.legalMoves(state) }
val targets = remember(selected, moves) { selected?.let { sel -> moves.filter { it.fromR == sel.first && it.fromC == sel.second }.map { it.toR to it.toC } } ?: emptyList() }
@@ -1185,8 +1185,8 @@ fun ChessApp() {
}
}
DetailScaffold(title = "chess") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
BasicText(text = if (state.status.isNotEmpty()) state.status else if (state.turn == ChessColor.WHITE) "your move" else "ai thinking…", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.accent))
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(text = if (state.status.isNotEmpty()) state.status else if (state.turn == ChessColor.WHITE) "your move" else "ai thinking…", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.accent))
Spacer(Modifier.height(4.dp))
val selectedSq = selected
val targetSquares = targets
@@ -1211,7 +1211,7 @@ fun ChessApp() {
}
}
Spacer(Modifier.height(4.dp))
BasicText(text = "tap a piece, then a target", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = colors.textSecondary))
BasicText(text = "tap a piece, then a target", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = colors.textSecondary))
}
}
if (state.turn == ChessColor.BLACK && state.status.isEmpty()) {
@@ -1236,39 +1236,39 @@ fun ChessApp() {
@Composable
fun TexasHoldemApp() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
var state by remember { mutableStateOf(PokerEngine.newGame()) }
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
DetailScaffold(title = "texas hold 'em") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
BasicText(text = "pot ${state.pot} • you ${state.playerStack} • ai ${state.aiStack} • ${state.phase}", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.accent))
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
BasicText(text = "pot ${state.pot} • you ${state.playerStack} • ai ${state.aiStack} • ${state.phase}", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.accent))
Spacer(Modifier.height(8.dp))
BasicText(text = "ai: ${state.aiHole.joinToString("") { it.toString().take(2) }}", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_CAPTION.sp, color = colors.textSecondary))
BasicText(text = "ai: ${state.aiHole.joinToString("") { it.toString().take(2) }}", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_CAPTION.sp, color = colors.textSecondary))
Spacer(Modifier.height(4.dp))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
state.community.forEach { SolCardView(it) {} }
}
Spacer(Modifier.height(8.dp))
BasicText(text = "you: ${state.playerHole.joinToString("") { it.toString().take(2) }}", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.textPrimary))
BasicText(text = "you: ${state.playerHole.joinToString("") { it.toString().take(2) }}", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.textPrimary))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
state.playerHole.forEach { SolCardView(it) {} }
}
Spacer(Modifier.height(8.dp))
if (state.phase == PokerPhase.SHOWDOWN || state.phase == PokerPhase.DONE) {
BasicText(text = if (state.winner == "tie") "tie" else "${state.winner} wins", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.accent))
BasicText(text = "new hand", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.accent),
BasicText(text = if (state.winner == "tie") "tie" else "${state.winner} wins", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.accent))
BasicText(text = "new hand", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.accent),
modifier = Modifier.pointerInput(Unit) { detectTapGestures(onTap = { state = PokerEngine.newGame() }) })
} else if (state.actor) {
val legal = PokerEngine.legalActions(state)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
legal.forEach { a ->
BasicText(text = a.name.lowercase(), style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.accent),
BasicText(text = a.name.lowercase(), style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.accent),
modifier = Modifier.pointerInput(a) { detectTapGestures(onTap = { state = PokerEngine.applyAction(state, a) }) })
}
}
} else {
BasicText(text = "ai thinking…", style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = colors.textSecondary))
BasicText(text = "ai thinking…", style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = colors.textSecondary))
}
}
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.apps.mocks
package com.heretek.dorado_hd.ui.apps.mocks
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -12,10 +12,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.ui.components.DetailScaffold
/**
* Mock shells for the dead-marketplace apps (canon §8 + §9). Era-faithful
@@ -24,10 +24,10 @@ import com.heretek.xunehd.ui.components.DetailScaffold
@Composable fun WeatherMock() {
DetailScaffold(title = "weather") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
EdgeCropText(text = "Seattle, WA", fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.6f)
EdgeCropText(text = "62°", fontSize = XuneTokens.TYPE_HEADER_CROPPED.dp)
EdgeCropText(text = "light rain — last seen 2012", fontSize = XuneTokens.TYPE_NOW_META.dp, color = LocalXuneColors.current.textSecondary)
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
EdgeCropText(text = "Seattle, WA", fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.6f)
EdgeCropText(text = "62°", fontSize = DoradoTokens.TYPE_HEADER_CROPPED.dp)
EdgeCropText(text = "light rain — last seen 2012", fontSize = DoradoTokens.TYPE_NOW_META.dp, color = LocalDoradoColors.current.textSecondary)
Spacer(Modifier.height(8.dp))
// Frozen 7-day forecast (mock data).
listOf(
@@ -40,8 +40,8 @@ import com.heretek.xunehd.ui.components.DetailScaffold
"sun" to "66° / 53°",
).forEach { (day, hiLo) ->
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
EdgeCropText(text = day, fontSize = XuneTokens.TYPE_LIST.dp, modifier = Modifier.weight(1f))
EdgeCropText(text = hiLo, fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = day, fontSize = DoradoTokens.TYPE_LIST.dp, modifier = Modifier.weight(1f))
EdgeCropText(text = hiLo, fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
}
}
@@ -50,15 +50,15 @@ import com.heretek.xunehd.ui.components.DetailScaffold
@Composable fun TwitterMock() {
DetailScaffold(title = "twitter") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
listOf(
"@zune" to "the social feed has been quiet since 2012. here's to the years we shared.",
"@britney" to "listening to a whole album for the first time in ages. the shuffle is off. the world is right.",
"@radio" to "tuning in from somewhere off the grid. signal's fine.",
).forEach { (handle, text) ->
Column {
EdgeCropText(text = handle, fontSize = XuneTokens.TYPE_NOW_META.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = text, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = handle, fontSize = DoradoTokens.TYPE_NOW_META.dp, color = LocalDoradoColors.current.accent)
EdgeCropText(text = text, fontSize = DoradoTokens.TYPE_LIST.dp)
}
}
}
@@ -67,16 +67,16 @@ import com.heretek.xunehd.ui.components.DetailScaffold
@Composable fun FacebookMock() {
DetailScaffold(title = "facebook") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
EdgeCropText(text = "your zune hd wall: 7 friends still listen", fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.6f)
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
EdgeCropText(text = "your zune hd wall: 7 friends still listen", fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.6f)
listOf(
"alex" to "shared: a playlist called 'morning'.",
"jordan" to "shared: a photo from the live show.",
"sam" to "pinned a new track.",
).forEach { (name, what) ->
Column {
EdgeCropText(text = name, fontSize = XuneTokens.TYPE_NOW_META.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = what, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = name, fontSize = DoradoTokens.TYPE_NOW_META.dp, color = LocalDoradoColors.current.accent)
EdgeCropText(text = what, fontSize = DoradoTokens.TYPE_LIST.dp)
}
}
}
@@ -85,15 +85,15 @@ import com.heretek.xunehd.ui.components.DetailScaffold
@Composable fun EmailMock() {
DetailScaffold(title = "email") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
listOf(
"windows live" to "your inbox has been frozen since august 2012.",
"exchange admin" to "policy reminder: please rotate your calendar password.",
"noreply" to "no new messages — but it is still possible to send.",
).forEach { (from, body) ->
Column {
EdgeCropText(text = from, fontSize = XuneTokens.TYPE_NOW_META.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = body, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = from, fontSize = DoradoTokens.TYPE_NOW_META.dp, color = LocalDoradoColors.current.accent)
EdgeCropText(text = body, fontSize = DoradoTokens.TYPE_LIST.dp)
}
}
}
@@ -102,15 +102,15 @@ import com.heretek.xunehd.ui.components.DetailScaffold
@Composable fun MessengerMock() {
DetailScaffold(title = "messenger") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
listOf(
"alex" to "are you still on zune?",
"alex" to "anyway — saw this and thought of you. miss the old days.",
"you" to "(sending is disabled — service frozen)",
).forEach { (who, msg) ->
Column {
EdgeCropText(text = who, fontSize = XuneTokens.TYPE_NOW_META.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = msg, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = who, fontSize = DoradoTokens.TYPE_NOW_META.dp, color = LocalDoradoColors.current.accent)
EdgeCropText(text = msg, fontSize = DoradoTokens.TYPE_LIST.dp)
}
}
}
@@ -119,9 +119,9 @@ import com.heretek.xunehd.ui.components.DetailScaffold
@Composable fun MsnMoneyMock() {
DetailScaffold(title = "msn money") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
EdgeCropText(text = "dow jones (frozen)", fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.6f)
EdgeCropText(text = "13,583.93 +27.59", fontSize = XuneTokens.TYPE_HEADER_CROP_VISIBLE.dp * 1.4f)
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
EdgeCropText(text = "dow jones (frozen)", fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.6f)
EdgeCropText(text = "13,583.93 +27.59", fontSize = DoradoTokens.TYPE_HEADER_CROP_VISIBLE.dp * 1.4f)
Spacer(Modifier.height(8.dp))
listOf(
"tech" to "Microsoft announces the end of Zune hardware sales.",
@@ -129,8 +129,8 @@ import com.heretek.xunehd.ui.components.DetailScaffold
"world" to "Markets end mixed after central bank statement.",
).forEach { (sec, headline) ->
Column {
EdgeCropText(text = sec, fontSize = XuneTokens.TYPE_NOW_META.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = headline, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = sec, fontSize = DoradoTokens.TYPE_NOW_META.dp, color = LocalDoradoColors.current.accent)
EdgeCropText(text = headline, fontSize = DoradoTokens.TYPE_LIST.dp)
}
}
}
@@ -139,8 +139,8 @@ import com.heretek.xunehd.ui.components.DetailScaffold
@Composable fun ZuneReaderMock() {
DetailScaffold(title = "zune reader") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
EdgeCropText(text = "your library", fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.6f)
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
EdgeCropText(text = "your library", fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.6f)
listOf(
"The Brief Wondrous Life of Oscar Wao" to "junot díaz",
"Fingersmith" to "sarah waters",
@@ -149,8 +149,8 @@ import com.heretek.xunehd.ui.components.DetailScaffold
"House of Leaves" to "mark z. danielewski",
).forEach { (title, author) ->
Column {
EdgeCropText(text = title, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = author, fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = title, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(text = author, fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
}
}
@@ -159,9 +159,9 @@ import com.heretek.xunehd.ui.components.DetailScaffold
@Composable fun SocialMock() {
DetailScaffold(title = "social") {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
EdgeCropText(text = "the social feed is frozen.", fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.6f)
EdgeCropText(text = "the zune social service shut down in 2012. what you see here is a memorial.", fontSize = XuneTokens.TYPE_LIST.dp)
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
EdgeCropText(text = "the social feed is frozen.", fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.6f)
EdgeCropText(text = "the zune social service shut down in 2012. what you see here is a memorial.", fontSize = DoradoTokens.TYPE_LIST.dp)
}
}
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.components
package com.heretek.dorado_hd.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@@ -38,14 +38,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneMotion
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.AlbumArt
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.data.model.Track
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoMotion
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.AlbumArt
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.data.model.Track
import com.heretek.dorado_hd.ui.LocalDoradoGraph
/** One action in a context menu. */
data class MenuAction(val label: String, val action: () -> Unit)
@@ -85,17 +85,17 @@ val LocalContextMenu = androidx.compose.runtime.staticCompositionLocalOf<MenuCon
@Composable
fun ContextMenuOverlay(controller: MenuController) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val request = controller.menu
AnimatedVisibility(
visible = request != null,
enter = fadeIn(XuneMotion.pivot()),
exit = fadeOut(XuneMotion.pivot()),
enter = fadeIn(DoradoMotion.pivot()),
exit = fadeOut(DoradoMotion.pivot()),
) {
Box(
Modifier
.fillMaxSize()
.background(LocalXuneColors.current.background.copy(alpha = 0.55f))
.background(LocalDoradoColors.current.background.copy(alpha = 0.55f))
.pointerInput(Unit) {
detectTapGestures { controller.dismiss() }
},
@@ -103,8 +103,8 @@ fun ContextMenuOverlay(controller: MenuController) {
}
AnimatedVisibility(
visible = request != null,
enter = slideInVertically(XuneMotion.pivot()) { it } + fadeIn(XuneMotion.pivot()),
exit = slideOutVertically(XuneMotion.pivot()) { it } + fadeOut(XuneMotion.pivot()),
enter = slideInVertically(DoradoMotion.pivot()) { it } + fadeIn(DoradoMotion.pivot()),
exit = slideOutVertically(DoradoMotion.pivot()) { it } + fadeOut(DoradoMotion.pivot()),
) {
Column(
Modifier
@@ -114,8 +114,8 @@ fun ContextMenuOverlay(controller: MenuController) {
) {
EdgeCropText(
text = request?.title ?: "",
fontSize = XuneTokens.TYPE_NOW_META.dp,
modifier = Modifier.padding(horizontal = XuneTokens.EDGE.dp, vertical = 10.dp),
fontSize = DoradoTokens.TYPE_NOW_META.dp,
modifier = Modifier.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 10.dp),
color = colors.textSecondary,
)
request?.actions?.forEach { action ->
@@ -126,20 +126,20 @@ fun ContextMenuOverlay(controller: MenuController) {
controller.dismiss()
action.action()
}
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 12.dp),
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 12.dp),
) {
EdgeCropText(text = action.label, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = action.label, fontSize = DoradoTokens.TYPE_LIST.dp)
}
}
Spacer(Modifier.height(XuneTokens.EDGE.dp))
Spacer(Modifier.height(DoradoTokens.EDGE.dp))
}
}
val prompt = controller.prompt
AnimatedVisibility(
visible = prompt != null,
enter = slideInVertically(XuneMotion.pivot()) { it } + fadeIn(XuneMotion.pivot()),
exit = slideOutVertically(XuneMotion.pivot()) { it } + fadeOut(XuneMotion.pivot()),
enter = slideInVertically(DoradoMotion.pivot()) { it } + fadeIn(DoradoMotion.pivot()),
exit = slideOutVertically(DoradoMotion.pivot()) { it } + fadeOut(DoradoMotion.pivot()),
) {
PromptPanel(controller)
}
@@ -147,7 +147,7 @@ fun ContextMenuOverlay(controller: MenuController) {
@Composable
private fun PromptPanel(controller: MenuController) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
var text by remember { mutableStateOf("") }
val request = controller.prompt ?: return
Column(
@@ -156,11 +156,11 @@ private fun PromptPanel(controller: MenuController) {
.imePadding()
.background(colors.elevated)
.border(0.5.dp, colors.border)
.padding(XuneTokens.EDGE.dp),
.padding(DoradoTokens.EDGE.dp),
) {
EdgeCropText(
text = request.title,
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
color = colors.textSecondary,
)
BasicTextField(
@@ -169,7 +169,7 @@ private fun PromptPanel(controller: MenuController) {
singleLine = true,
textStyle = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_NOW_META.sp,
fontSize = DoradoTokens.TYPE_NOW_META.sp,
color = colors.textPrimary,
),
cursorBrush = SolidColor(colors.accent),
@@ -201,9 +201,9 @@ fun DetailScaffold(
title: String,
content: @Composable () -> Unit,
) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
Column(Modifier.fillMaxSize()) {
com.heretek.xunehd.design.components.CroppedHeader(
com.heretek.dorado_hd.design.components.CroppedHeader(
text = title,
onBack = { graph.nav.pop() },
)
@@ -226,26 +226,26 @@ fun TrackRow(
playing: Boolean = false,
showSubLabel: Boolean = true,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(
text = track.title,
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = if (playing) colors.accent else colors.textPrimary,
fontWeight = if (playing) FontWeight.SemiBold else FontWeight.Normal,
)
if (showSubLabel) {
EdgeCropText(
text = "${track.artist} — ${track.album}",
fontSize = XuneTokens.TYPE_LIST_SECONDARY.dp,
fontSize = DoradoTokens.TYPE_LIST_SECONDARY.dp,
color = colors.textSecondary,
)
}
@@ -256,12 +256,12 @@ fun TrackRow(
/** Small section label used across quickplay and details. */
@Composable
fun SectionLabel(text: String, modifier: Modifier = Modifier) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
EdgeCropText(
text = text,
fontSize = XuneTokens.TYPE_CROSSBAR.dp,
fontSize = DoradoTokens.TYPE_CROSSBAR.dp,
alpha = 0.6f,
modifier = modifier.padding(horizontal = XuneTokens.EDGE.dp, vertical = 6.dp),
modifier = modifier.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 6.dp),
)
}
@@ -0,0 +1,64 @@
package com.heretek.dorado_hd.ui.nav
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.flow.MutableStateFlow
/**
* Lightweight hierarchical navigation. The Zune HD has no hardware back:
* going back is "tap the cut-off header", which calls [pop]. Slides are
* horizontal with the Zune deceleration curve.
*/
sealed interface DoradoDestination {
data object Home : DoradoDestination
data object Quickplay : DoradoDestination
data object Music : DoradoDestination
data class Album(val albumId: Long) : DoradoDestination
data class Artist(val artistId: Long) : DoradoDestination
data class Genre(val genre: String) : DoradoDestination
data class PlaylistDetail(val playlistId: Long) : DoradoDestination
data object NowPlaying : DoradoDestination
data object Settings : DoradoDestination
// Media pivots (canon §3.1, Phase 5 surfaces).
data object Videos : DoradoDestination
data object Pictures : DoradoDestination
data object Radio : DoradoDestination
data object Podcasts : DoradoDestination
data class PodcastFeed(val feedId: Long) : DoradoDestination
data object Marketplace : DoradoDestination
data object Social : DoradoDestination
data object Internet : DoradoDestination
// Mini-app platform (canon §8).
data class MiniApp(val appId: String) : DoradoDestination
// A single pinned picture viewer — URI is stored in the pin's subLabel.
data class PictureDetail(val uri: String) : DoradoDestination
}
class DoradoNav {
private val stack = mutableStateListOf<DoradoDestination>(DoradoDestination.Home)
val current: DoradoDestination
get() = stack.lastOrNull() ?: DoradoDestination.Home
val depth: Int
get() = stack.size
fun push(destination: DoradoDestination) {
stack.add(destination)
}
fun pop() {
if (stack.size > 1) stack.removeAt(stack.lastIndex)
}
fun toHome() {
stack.clear()
stack.add(DoradoDestination.Home)
}
fun replaceTop(destination: DoradoDestination) {
if (stack.isNotEmpty()) stack[stack.lastIndex] = destination else stack.add(destination)
}
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.screens
package com.heretek.dorado_hd.ui.screens
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -31,23 +31,23 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.XuneMotion
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.AlbumArt
import com.heretek.xunehd.design.components.CrossbarBar
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.design.components.KineticList
import com.heretek.xunehd.design.components.firstLetterOf
import com.heretek.xunehd.data.model.Album
import com.heretek.xunehd.data.model.PinKind
import com.heretek.xunehd.data.model.Track
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.xunehd.ui.components.LocalContextMenu
import com.heretek.xunehd.ui.components.MenuAction
import com.heretek.xunehd.ui.components.TrackRow
import com.heretek.xunehd.ui.nav.XuneDestination
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.DoradoMotion
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.AlbumArt
import com.heretek.dorado_hd.design.components.CrossbarBar
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.design.components.KineticList
import com.heretek.dorado_hd.design.components.firstLetterOf
import com.heretek.dorado_hd.data.model.Album
import com.heretek.dorado_hd.data.model.PinKind
import com.heretek.dorado_hd.data.model.Track
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.DetailScaffold
import com.heretek.dorado_hd.ui.components.LocalContextMenu
import com.heretek.dorado_hd.ui.components.MenuAction
import com.heretek.dorado_hd.ui.components.TrackRow
import com.heretek.dorado_hd.ui.nav.DoradoDestination
import kotlinx.coroutines.launch
/**
@@ -57,7 +57,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AlbumDetailScreen(albumId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val menus = LocalContextMenu.current
var album by remember { mutableStateOf<Album?>(null) }
@@ -67,7 +67,7 @@ fun AlbumDetailScreen(albumId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
LaunchedEffect(albumId) {
tracks = graph.library.tracksByAlbum(albumId)
album = tracks.firstOrNull()?.let {
com.heretek.xunehd.data.model.Album(
com.heretek.dorado_hd.data.model.Album(
albumId = it.albumId,
title = it.album,
artist = it.artist,
@@ -85,7 +85,7 @@ fun AlbumDetailScreen(albumId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
) {
AlbumArt(
model = album?.albumArtUri,
@@ -95,19 +95,19 @@ fun AlbumDetailScreen(albumId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
Column(Modifier.padding(start = 12.dp)) {
EdgeCropText(
text = album?.artist ?: "",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
)
EdgeCropText(
text = listOfNotNull(album?.year?.takeIf { it != "unknown" }, "${album?.trackCount ?: 0} songs").joinToString(" — "),
fontSize = XuneTokens.TYPE_LIST.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = LocalDoradoColors.current.textSecondary,
)
Spacer(Modifier.height(6.dp))
Row {
EdgeCropText(
text = "play",
fontSize = XuneTokens.TYPE_LIST.dp,
color = LocalXuneColors.current.accent,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = LocalDoradoColors.current.accent,
modifier = Modifier
.combinedClickable(onClick = {
tracks.firstOrNull()?.let { graph.controller.play(tracks, 0) }
@@ -116,8 +116,8 @@ fun AlbumDetailScreen(albumId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
)
EdgeCropText(
text = "shuffle",
fontSize = XuneTokens.TYPE_LIST.dp,
color = LocalXuneColors.current.accent,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = LocalDoradoColors.current.accent,
modifier = Modifier.combinedClickable(
onClick = {
tracks.firstOrNull()?.let {
@@ -152,7 +152,7 @@ fun AlbumDetailScreen(albumId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
graph.quickplay.pin(PinKind.TRACK, track.mediaId, track.title, track.artist, track.albumId)
}
},
MenuAction("view artist") { graph.nav.push(XuneDestination.Artist(track.artistId)) },
MenuAction("view artist") { graph.nav.push(DoradoDestination.Artist(track.artistId)) },
),
)
},
@@ -171,13 +171,13 @@ fun AlbumDetailScreen(albumId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ArtistDetailScreen(artistId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val menus = LocalContextMenu.current
var albums by remember { mutableStateOf<List<Album>>(emptyList()) }
var tracks by remember { mutableStateOf<List<Track>>(emptyList()) }
var artistName by remember { mutableStateOf<String?>(null) }
var related by remember { mutableStateOf<List<com.heretek.xunehd.data.model.Artist>>(emptyList()) }
var related by remember { mutableStateOf<List<com.heretek.dorado_hd.data.model.Artist>>(emptyList()) }
var loaded by remember { mutableStateOf(false) }
LaunchedEffect(artistId) {
@@ -220,8 +220,8 @@ fun ArtistDetailScreen(artistId: Long, canvasWidth: androidx.compose.ui.unit.Dp)
@Composable
private fun ArtistBio(artistName: String?) {
val graph = LocalXuneGraph.current
val colors = LocalXuneColors.current
val graph = LocalDoradoGraph.current
val colors = LocalDoradoColors.current
if (artistName == null) return
var bio by remember(artistName) { mutableStateOf<String?>(null) }
var failed by remember(artistName) { mutableStateOf(false) }
@@ -241,27 +241,27 @@ private fun ArtistBio(artistName: String?) {
androidx.compose.foundation.text.BasicText(
text = text,
style = androidx.compose.ui.text.TextStyle(
fontFamily = com.heretek.xunehd.design.Selawik,
fontSize = XuneTokens.TYPE_LIST.sp,
fontFamily = com.heretek.dorado_hd.design.Selawik,
fontSize = DoradoTokens.TYPE_LIST.sp,
color = colors.textPrimary.copy(alpha = 0.9f),
lineHeight = XuneTokens.ROW_HEIGHT.sp * 0.5f,
lineHeight = DoradoTokens.ROW_HEIGHT.sp * 0.5f,
),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 12.dp),
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 12.dp),
)
}
failed -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(
text = "no biography",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
)
}
else -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(
text = "loading…",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
)
}
@@ -270,7 +270,7 @@ private fun ArtistBio(artistName: String?) {
@Composable
private fun ArtistPhotos(artistName: String?) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
var photo by remember(artistName) { mutableStateOf<java.io.File?>(null) }
LaunchedEffect(artistName) {
@@ -281,7 +281,7 @@ private fun ArtistPhotos(artistName: String?) {
// catalog.zune.net is gone, so we surface a single available photo (the
// wallpaper pulled from MusicBrainz) and an honest caption explaining
// where the rest would have come from.
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp)) {
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp)) {
if (photo != null) {
coil3.compose.AsyncImage(
model = android.net.Uri.fromFile(photo),
@@ -291,21 +291,21 @@ private fun ArtistPhotos(artistName: String?) {
)
EdgeCropText(
text = artistName ?: "",
fontSize = XuneTokens.TYPE_NOW_TITLE.dp,
color = LocalXuneColors.current.textPrimary,
fontSize = DoradoTokens.TYPE_NOW_TITLE.dp,
color = LocalDoradoColors.current.textPrimary,
modifier = Modifier.padding(top = 8.dp),
)
EdgeCropText(
text = "photos from catalog.zune.net (frozen 2012)",
fontSize = XuneTokens.TYPE_CAPTION.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
color = LocalDoradoColors.current.textSecondary,
alpha = 0.6f,
)
} else {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(
text = "no photos — enable artist photos in settings",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
)
}
@@ -314,35 +314,35 @@ private fun ArtistPhotos(artistName: String?) {
}
@Composable
private fun ArtistRelated(related: List<com.heretek.xunehd.data.model.Artist>, artistName: String? = null) {
private fun ArtistRelated(related: List<com.heretek.dorado_hd.data.model.Artist>, artistName: String? = null) {
val context = androidx.compose.ui.platform.LocalContext.current
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
if (related.isEmpty()) {
// The Zune HD surfaced 'similar artists' from Last.fm + your friends'
// listening history. We have neither offline, so the empty state
// points the user at MusicBrainz, which the XuneSocial community used
// points the user at MusicBrainz, which the DoradoSocial community used
// as a fallback for related-artist data.
Column(
Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp),
Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp),
verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(6.dp),
) {
EdgeCropText(
text = "no related artists yet.",
fontSize = XuneTokens.TYPE_NOW_META.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
color = LocalDoradoColors.current.textSecondary,
)
EdgeCropText(
text = "your zune would have drawn these from the social feed and last.fm similarity. without those signals, the only related artists we can show are the ones who share genres with this one in your library — a thin slice.",
fontSize = XuneTokens.TYPE_CAPTION.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
color = LocalDoradoColors.current.textSecondary,
alpha = 0.6f,
)
if (artistName != null) {
val url = "https://musicbrainz.org/artist/" + (artistName.replace(" ", "%20")) + "?query="
EdgeCropText(
text = "open musicbrainz for “$artistName”",
fontSize = XuneTokens.TYPE_NOW_META.dp,
color = LocalXuneColors.current.accent,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
color = LocalDoradoColors.current.accent,
modifier = Modifier
.pointerInput(artistName) {
detectTapGestures(onTap = {
@@ -357,25 +357,25 @@ private fun ArtistRelated(related: List<com.heretek.xunehd.data.model.Artist>, a
}
return
}
com.heretek.xunehd.design.components.KineticList(
com.heretek.dorado_hd.design.components.KineticList(
items = related,
key = { it.artistId },
letter = { com.heretek.xunehd.design.components.firstLetterOf(it.name) },
letter = { com.heretek.dorado_hd.design.components.firstLetterOf(it.name) },
rowContent = { artist, _ ->
Box(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = { graph.nav.push(XuneDestination.Artist(artist.artistId)) },
onClick = { graph.nav.push(DoradoDestination.Artist(artist.artistId)) },
onLongClick = {},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
contentAlignment = Alignment.CenterStart,
) {
EdgeCropText(
text = artist.name,
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
modifier = Modifier.fillMaxWidth(),
)
}
@@ -386,17 +386,17 @@ private fun ArtistRelated(related: List<com.heretek.xunehd.data.model.Artist>, a
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ArtistAlbums(albums: List<Album>) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
LazyRow(
contentPadding = androidx.compose.foundation.layout.PaddingValues(XuneTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(XuneTokens.GRID_GUTTER.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(DoradoTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(DoradoTokens.GRID_GUTTER.dp),
modifier = Modifier.fillMaxSize(),
) {
items(albums, key = { it.albumId }) { album ->
Column(
Modifier
.combinedClickable(
onClick = { graph.nav.push(XuneDestination.Album(album.albumId)) },
onClick = { graph.nav.push(DoradoDestination.Album(album.albumId)) },
onLongClick = {},
)
.width(92.dp),
@@ -406,11 +406,11 @@ private fun ArtistAlbums(albums: List<Album>) {
contentDescription = album.title,
modifier = Modifier.size(92.dp),
)
EdgeCropText(text = album.title, fontSize = XuneTokens.TYPE_CAPTION.dp, modifier = Modifier.fillMaxWidth())
EdgeCropText(text = album.title, fontSize = DoradoTokens.TYPE_CAPTION.dp, modifier = Modifier.fillMaxWidth())
EdgeCropText(
text = album.year,
fontSize = XuneTokens.TYPE_CAPTION.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
color = LocalDoradoColors.current.textSecondary,
modifier = Modifier.fillMaxWidth(),
)
}
@@ -422,8 +422,8 @@ private fun ArtistAlbums(albums: List<Album>) {
@Composable
private fun ArtistSongs(
tracks: List<Track>,
menus: com.heretek.xunehd.ui.components.MenuController,
graph: com.heretek.xunehd.XuneGraph,
menus: com.heretek.dorado_hd.ui.components.MenuController,
graph: com.heretek.dorado_hd.DoradoGraph,
) {
val nowPlayingId by graph.controller.nowPlaying.collectAsState()
KineticList(
@@ -439,7 +439,7 @@ private fun ArtistSongs(
menus.show(
title = track.title,
actions = listOf(
MenuAction("view album") { graph.nav.push(XuneDestination.Album(track.albumId)) },
MenuAction("view album") { graph.nav.push(DoradoDestination.Album(track.albumId)) },
),
)
},
@@ -451,7 +451,7 @@ private fun ArtistSongs(
/** Genre detail: every track in a genre. */
@Composable
fun GenreScreen(genre: String, canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
var tracks by remember { mutableStateOf<List<Track>>(emptyList()) }
val nowPlayingId by graph.controller.nowPlaying.collectAsState()
@@ -479,7 +479,7 @@ fun GenreScreen(genre: String, canvasWidth: androidx.compose.ui.unit.Dp) {
/** Playlist detail: tracks with removal. */
@Composable
fun PlaylistDetailScreen(playlistId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
var tracks by remember { mutableStateOf<List<Track>>(emptyList()) }
val nowPlayingId by graph.controller.nowPlaying.collectAsState()
@@ -493,7 +493,7 @@ fun PlaylistDetailScreen(playlistId: Long, canvasWidth: androidx.compose.ui.unit
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(
text = "empty playlist",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
)
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.screens
package com.heretek.dorado_hd.ui.screens
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -31,22 +31,22 @@ import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.heretek.xunehd.XuneGraph
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.media.PlaybackController
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.AlbumArt
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.design.components.HomeMenuItem
import com.heretek.xunehd.design.components.KineticList
import com.heretek.xunehd.design.components.StaggerEntrance
import com.heretek.xunehd.design.components.firstLetterOf
import com.heretek.xunehd.data.model.PinKind
import com.heretek.xunehd.data.repo.QuickplayCard
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.LocalContextMenu
import com.heretek.xunehd.ui.components.SectionLabel
import com.heretek.xunehd.ui.nav.XuneDestination
import com.heretek.dorado_hd.DoradoGraph
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.media.PlaybackController
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.AlbumArt
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.design.components.HomeMenuItem
import com.heretek.dorado_hd.design.components.KineticList
import com.heretek.dorado_hd.design.components.StaggerEntrance
import com.heretek.dorado_hd.design.components.firstLetterOf
import com.heretek.dorado_hd.data.model.PinKind
import com.heretek.dorado_hd.data.repo.QuickplayCard
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.LocalContextMenu
import com.heretek.dorado_hd.ui.components.SectionLabel
import com.heretek.dorado_hd.ui.nav.DoradoDestination
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -87,23 +87,23 @@ fun HomePages(canvasWidth: Dp) {
}
/** The home menu: 9 entries listed canonically, in a kinetic scrolling column. */
private data class HomeEntry(val id: String, val label: String, val destination: XuneDestination)
private data class HomeEntry(val id: String, val label: String, val destination: DoradoDestination)
private val HOME_ENTRIES = listOf(
HomeEntry("music", "music", XuneDestination.Music),
HomeEntry("videos", "videos", XuneDestination.Videos),
HomeEntry("pictures", "pictures", XuneDestination.Pictures),
HomeEntry("radio", "radio", XuneDestination.Radio),
HomeEntry("marketplace", "marketplace", XuneDestination.Marketplace),
HomeEntry("social", "social", XuneDestination.Social),
HomeEntry("podcasts", "podcasts", XuneDestination.Podcasts),
HomeEntry("internet", "internet", XuneDestination.Internet),
HomeEntry("settings", "settings", XuneDestination.Settings),
HomeEntry("music", "music", DoradoDestination.Music),
HomeEntry("videos", "videos", DoradoDestination.Videos),
HomeEntry("pictures", "pictures", DoradoDestination.Pictures),
HomeEntry("radio", "radio", DoradoDestination.Radio),
HomeEntry("marketplace", "marketplace", DoradoDestination.Marketplace),
HomeEntry("social", "social", DoradoDestination.Social),
HomeEntry("podcasts", "podcasts", DoradoDestination.Podcasts),
HomeEntry("internet", "internet", DoradoDestination.Internet),
HomeEntry("settings", "settings", DoradoDestination.Settings),
)
@Composable
fun HomeMenuScreen(canvasWidth: Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
Box(Modifier.fillMaxSize()) {
Column(Modifier.fillMaxSize()) {
Spacer(Modifier.height(48.dp))
@@ -123,12 +123,12 @@ fun HomeMenuScreen(canvasWidth: Dp) {
)
// Bottom watermark — canon §2 (textWatermark 0.08).
EdgeCropText(
text = "xune hd",
fontSize = XuneTokens.TYPE_CROSSBAR.dp,
text = "dorado hd",
fontSize = DoradoTokens.TYPE_CROSSBAR.dp,
alpha = 0.08f,
modifier = Modifier
.fillMaxWidth()
.padding(start = XuneTokens.EDGE.dp, bottom = XuneTokens.EDGE.dp),
.padding(start = DoradoTokens.EDGE.dp, bottom = DoradoTokens.EDGE.dp),
)
}
}
@@ -137,7 +137,7 @@ fun HomeMenuScreen(canvasWidth: Dp) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun QuickplayScreen(canvasWidth: Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val controller = graph.controller
val scope = rememberCoroutineScope()
val menus = LocalContextMenu.current
@@ -147,7 +147,7 @@ fun QuickplayScreen(canvasWidth: Dp) {
val history by graph.quickplay.history(12).collectAsState(initial = emptyList())
Column(Modifier.fillMaxSize()) {
Spacer(Modifier.height(XuneTokens.EDGE.dp))
Spacer(Modifier.height(DoradoTokens.EDGE.dp))
SectionLabel("now playing")
val track = nowPlaying
@@ -156,7 +156,7 @@ fun QuickplayScreen(canvasWidth: Dp) {
.fillMaxWidth()
.height(72.dp)
.combinedClickable(
onClick = { if (track != null) graph.nav.push(XuneDestination.NowPlaying) },
onClick = { if (track != null) graph.nav.push(DoradoDestination.NowPlaying) },
onLongClick = {},
),
verticalAlignment = Alignment.CenterVertically,
@@ -166,23 +166,23 @@ fun QuickplayScreen(canvasWidth: Dp) {
model = track.albumArtUri,
contentDescription = track.album,
modifier = Modifier
.padding(start = XuneTokens.EDGE.dp)
.padding(start = DoradoTokens.EDGE.dp)
.size(64.dp),
)
Column(Modifier.padding(start = 12.dp)) {
EdgeCropText(text = track.title, fontSize = XuneTokens.TYPE_NOW_META.dp)
EdgeCropText(text = track.title, fontSize = DoradoTokens.TYPE_NOW_META.dp)
EdgeCropText(
text = track.artist,
fontSize = XuneTokens.TYPE_LIST.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = LocalDoradoColors.current.textSecondary,
)
}
} else {
EdgeCropText(
text = "nothing playing",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
modifier = Modifier.padding(start = XuneTokens.EDGE.dp),
modifier = Modifier.padding(start = DoradoTokens.EDGE.dp),
)
}
}
@@ -212,13 +212,13 @@ fun QuickplayScreen(canvasWidth: Dp) {
* sorts hearts-first / broken-skipped via [PlaybackController.smartShuffleOrder],
* caps at 25 tracks. */
@Composable
private fun SmartDjRow(graph: XuneGraph, scope: CoroutineScope) {
val colors = LocalXuneColors.current
private fun SmartDjRow(graph: DoradoGraph, scope: CoroutineScope) {
val colors = LocalDoradoColors.current
var building by remember { mutableStateOf(false) }
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
enabled = !building,
onClick = {
@@ -239,12 +239,12 @@ private fun SmartDjRow(graph: XuneGraph, scope: CoroutineScope) {
},
onLongClick = {},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
EdgeCropText(
text = if (building) "building mix…" else "play smart dj mix",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
color = colors.accent,
)
}
@@ -252,20 +252,20 @@ private fun SmartDjRow(graph: XuneGraph, scope: CoroutineScope) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun QuickplayRow(graph: XuneGraph, scope: CoroutineScope, cards: List<QuickplayCard>) {
private fun QuickplayRow(graph: DoradoGraph, scope: CoroutineScope, cards: List<QuickplayCard>) {
val menus = LocalContextMenu.current
if (cards.isEmpty()) {
EdgeCropText(
text = "empty",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
alpha = 0.4f,
modifier = Modifier.padding(horizontal = XuneTokens.EDGE.dp, vertical = 4.dp),
modifier = Modifier.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 4.dp),
)
return
}
LazyRow(
contentPadding = PaddingValues(horizontal = XuneTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(XuneTokens.GRID_GUTTER.dp),
contentPadding = PaddingValues(horizontal = DoradoTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(DoradoTokens.GRID_GUTTER.dp),
) {
items(cards, key = { "${it.kind}:${it.refId}:${it.label}" }) { card ->
Column(
@@ -277,7 +277,7 @@ private fun QuickplayRow(graph: XuneGraph, scope: CoroutineScope, cards: List<Qu
menus.show(
title = card.label,
actions = listOf(
com.heretek.xunehd.ui.components.MenuAction("unpin") {
com.heretek.dorado_hd.ui.components.MenuAction("unpin") {
scope.launch { graph.quickplay.unpin(card.kind, card.refId) }
},
),
@@ -293,7 +293,7 @@ private fun QuickplayRow(graph: XuneGraph, scope: CoroutineScope, cards: List<Qu
)
EdgeCropText(
text = card.label,
fontSize = XuneTokens.TYPE_CAPTION.dp,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
modifier = Modifier.fillMaxWidth(),
)
}
@@ -304,20 +304,20 @@ private fun QuickplayRow(graph: XuneGraph, scope: CoroutineScope, cards: List<Qu
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun NewRow() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val albums by graph.library.albums().collectAsState(initial = emptyList())
val recent = albums.sortedByDescending { it.dateAdded }.take(12)
LazyRow(
contentPadding = PaddingValues(horizontal = XuneTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(XuneTokens.GRID_GUTTER.dp),
contentPadding = PaddingValues(horizontal = DoradoTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(DoradoTokens.GRID_GUTTER.dp),
) {
items(recent, key = { it.albumId }) { album ->
Column(
Modifier
.width(72.dp)
.combinedClickable(
onClick = { graph.nav.push(XuneDestination.Album(album.albumId)) },
onClick = { graph.nav.push(DoradoDestination.Album(album.albumId)) },
onLongClick = {},
),
) {
@@ -328,7 +328,7 @@ private fun NewRow() {
)
EdgeCropText(
text = album.title,
fontSize = XuneTokens.TYPE_CAPTION.dp,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
modifier = Modifier.fillMaxWidth(),
)
}
@@ -336,18 +336,18 @@ private fun NewRow() {
}
}
private fun openCard(graph: XuneGraph, scope: CoroutineScope, card: QuickplayCard) {
private fun openCard(graph: DoradoGraph, scope: CoroutineScope, card: QuickplayCard) {
when (card.kind) {
PinKind.TRACK -> scope.launch {
graph.library.track(card.refId)?.let { graph.controller.play(listOf(it)) }
}
PinKind.ALBUM -> graph.nav.push(XuneDestination.Album(card.refId))
PinKind.ARTIST -> graph.nav.push(XuneDestination.Artist(card.refId))
PinKind.PLAYLIST -> graph.nav.push(XuneDestination.PlaylistDetail(card.refId))
PinKind.ALBUM -> graph.nav.push(DoradoDestination.Album(card.refId))
PinKind.ARTIST -> graph.nav.push(DoradoDestination.Artist(card.refId))
PinKind.PLAYLIST -> graph.nav.push(DoradoDestination.PlaylistDetail(card.refId))
PinKind.PICTURE -> {
val uri = card.subLabel.takeIf { it.startsWith("content://") || it.startsWith("file://") }
if (uri != null) graph.nav.push(XuneDestination.PictureDetail(uri))
if (uri != null) graph.nav.push(DoradoDestination.PictureDetail(uri))
}
PinKind.RADIO -> graph.nav.push(XuneDestination.Radio)
PinKind.RADIO -> graph.nav.push(DoradoDestination.Radio)
}
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.screens
package com.heretek.dorado_hd.ui.screens
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@@ -27,21 +27,21 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.heretek.xunehd.data.official.OfficialApp
import com.heretek.xunehd.data.official.OfficialCatalog
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.XuneMotion
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.AlbumArt
import com.heretek.xunehd.design.components.CrossbarBar
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.design.components.KineticList
import com.heretek.xunehd.design.components.firstLetterOf
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.apps.MiniAppScaffold
import com.heretek.xunehd.ui.apps.XuneApps
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.xunehd.ui.nav.XuneDestination
import com.heretek.dorado_hd.data.official.OfficialApp
import com.heretek.dorado_hd.data.official.OfficialCatalog
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.DoradoMotion
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.AlbumArt
import com.heretek.dorado_hd.design.components.CrossbarBar
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.design.components.KineticList
import com.heretek.dorado_hd.design.components.firstLetterOf
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.apps.MiniAppScaffold
import com.heretek.dorado_hd.ui.apps.DoradoApps
import com.heretek.dorado_hd.ui.components.DetailScaffold
import com.heretek.dorado_hd.ui.nav.DoradoDestination
import kotlinx.coroutines.launch
/**
@@ -51,7 +51,7 @@ import kotlinx.coroutines.launch
*/
@Composable
fun MarketplaceScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val pagerState = androidx.compose.foundation.pager.rememberPagerState(
initialPage = 0,
@@ -79,33 +79,33 @@ fun MarketplaceScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
@Composable
private fun MarketplaceMusic() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val albums by graph.library.albums().collectAsState(initial = emptyList())
val featured = remember(albums) { albums.take(12) }
Column(Modifier.fillMaxSize().padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp)) {
Column(Modifier.fillMaxSize().padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp)) {
EdgeCropText(
text = "featured",
fontSize = XuneTokens.TYPE_CROSSBAR.dp,
fontSize = DoradoTokens.TYPE_CROSSBAR.dp,
alpha = 0.6f,
)
Spacer(Modifier.height(8.dp))
LazyRow(
contentPadding = PaddingValues(0.dp),
horizontalArrangement = Arrangement.spacedBy(XuneTokens.GRID_GUTTER.dp),
horizontalArrangement = Arrangement.spacedBy(DoradoTokens.GRID_GUTTER.dp),
) {
items(featured, key = { it.albumId }) { album ->
Column(
Modifier.width(XuneTokens.APP_TILE.dp).combinedClickable(
onClick = { graph.nav.push(XuneDestination.Album(album.albumId)) },
Modifier.width(DoradoTokens.APP_TILE.dp).combinedClickable(
onClick = { graph.nav.push(DoradoDestination.Album(album.albumId)) },
onLongClick = {},
),
) {
AlbumArt(
model = album.albumArtUri,
contentDescription = album.title,
modifier = Modifier.size(XuneTokens.APP_TILE.dp),
modifier = Modifier.size(DoradoTokens.APP_TILE.dp),
)
EdgeCropText(text = album.title, fontSize = XuneTokens.TYPE_CAPTION.dp, modifier = Modifier.fillMaxWidth())
EdgeCropText(text = album.title, fontSize = DoradoTokens.TYPE_CAPTION.dp, modifier = Modifier.fillMaxWidth())
}
}
}
@@ -114,44 +114,44 @@ private fun MarketplaceMusic() {
@Composable
private fun EmptyPivot(label: String, note: String) {
Column(Modifier.fillMaxSize().padding(XuneTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
EdgeCropText(text = label, fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.4f)
EdgeCropText(text = note, fontSize = XuneTokens.TYPE_LIST.dp, alpha = 0.4f)
Column(Modifier.fillMaxSize().padding(DoradoTokens.EDGE.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
EdgeCropText(text = label, fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.4f)
EdgeCropText(text = note, fontSize = DoradoTokens.TYPE_LIST.dp, alpha = 0.4f)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun AppsPivot() {
val graph = LocalXuneGraph.current
val installed = XuneApps.all
val graph = LocalDoradoGraph.current
val installed = DoradoApps.all
val catalog = OfficialCatalog.all
Column(Modifier.fillMaxSize()) {
if (installed.isNotEmpty()) {
EdgeCropText(
text = "installed",
fontSize = XuneTokens.TYPE_CROSSBAR.dp,
fontSize = DoradoTokens.TYPE_CROSSBAR.dp,
alpha = 0.6f,
modifier = Modifier.padding(start = XuneTokens.EDGE.dp, top = 8.dp),
modifier = Modifier.padding(start = DoradoTokens.EDGE.dp, top = 8.dp),
)
Spacer(Modifier.height(8.dp))
LazyRow(
contentPadding = PaddingValues(horizontal = XuneTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(XuneTokens.GRID_GUTTER.dp),
contentPadding = PaddingValues(horizontal = DoradoTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(DoradoTokens.GRID_GUTTER.dp),
) {
items(installed, key = { it.id }) { app ->
Column(
Modifier.width(XuneTokens.APP_TILE.dp).combinedClickable(
onClick = { graph.nav.push(XuneDestination.MiniApp(app.id)) },
Modifier.width(DoradoTokens.APP_TILE.dp).combinedClickable(
onClick = { graph.nav.push(DoradoDestination.MiniApp(app.id)) },
onLongClick = {},
),
) {
AlbumArt(
model = null,
contentDescription = app.title,
modifier = Modifier.size(XuneTokens.APP_TILE.dp),
modifier = Modifier.size(DoradoTokens.APP_TILE.dp),
)
EdgeCropText(text = app.title, fontSize = XuneTokens.TYPE_CAPTION.dp, modifier = Modifier.fillMaxWidth())
EdgeCropText(text = app.title, fontSize = DoradoTokens.TYPE_CAPTION.dp, modifier = Modifier.fillMaxWidth())
}
}
}
@@ -159,9 +159,9 @@ private fun AppsPivot() {
}
EdgeCropText(
text = "frozen catalog",
fontSize = XuneTokens.TYPE_CROSSBAR.dp,
fontSize = DoradoTokens.TYPE_CROSSBAR.dp,
alpha = 0.6f,
modifier = Modifier.padding(start = XuneTokens.EDGE.dp),
modifier = Modifier.padding(start = DoradoTokens.EDGE.dp),
)
KineticList(
items = catalog,
@@ -176,36 +176,36 @@ private fun AppsPivot() {
@Composable
private fun AppsCatalogRow(entry: OfficialApp) {
val graph = LocalXuneGraph.current
val colors = LocalXuneColors.current
val graph = LocalDoradoGraph.current
val colors = LocalDoradoColors.current
val isInstalled = entry.installedId != null
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
enabled = isInstalled,
onClick = { entry.installedId?.let { graph.nav.push(XuneDestination.MiniApp(it)) } },
onClick = { entry.installedId?.let { graph.nav.push(DoradoDestination.MiniApp(it)) } },
onLongClick = {},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(
text = entry.title,
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = if (isInstalled) colors.textPrimary else colors.textInactive,
)
EdgeCropText(
text = entry.category,
fontSize = XuneTokens.TYPE_CAPTION.dp,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
color = colors.textSecondary,
)
}
EdgeCropText(
text = if (isInstalled) "open" else "unavailable",
fontSize = XuneTokens.TYPE_CAPTION.dp,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
color = if (isInstalled) colors.accent else colors.textInactive,
)
}
@@ -214,14 +214,14 @@ private fun AppsCatalogRow(entry: OfficialApp) {
/** Mini-app host screen: looks up the renderer and shows the mini-app fullscreen. */
@Composable
fun MiniAppScreen(appId: String, canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val app = remember(appId) { XuneApps.byId(appId) }
val graph = LocalDoradoGraph.current
val app = remember(appId) { DoradoApps.byId(appId) }
if (app == null) {
DetailScaffold(title = "app") {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(
text = "unavailable",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
)
}
@@ -1,6 +1,6 @@
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class, androidx.compose.material3.ExperimentalMaterial3Api::class)
package com.heretek.xunehd.ui.screens
package com.heretek.dorado_hd.ui.screens
import android.content.ContentUris
import android.net.Uri
@@ -48,16 +48,16 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import coil3.compose.AsyncImage
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.AlbumArt
import com.heretek.xunehd.design.components.CrossbarBar
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.design.components.firstLetterOf
import com.heretek.xunehd.design.components.KineticList
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.AlbumArt
import com.heretek.dorado_hd.design.components.CrossbarBar
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.design.components.firstLetterOf
import com.heretek.dorado_hd.design.components.KineticList
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.DetailScaffold
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -73,7 +73,7 @@ private enum class VideoPivot { ALL, MOVIES, TV, MUSIC_VIDEOS }
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable
fun VideosScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
var videos by remember { mutableStateOf<List<VideoItem>>(emptyList()) }
@@ -153,7 +153,7 @@ private fun VideoListContent(videos: List<VideoItem>, onPlay: (VideoItem) -> Uni
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(
text = "no videos here",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
)
}
@@ -167,14 +167,14 @@ private fun VideoListContent(videos: List<VideoItem>, onPlay: (VideoItem) -> Uni
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(onClick = { onPlay(v) }, onLongClick = {})
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = v.title, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = v.artist, fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = v.title, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(text = v.artist, fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
}
},
@@ -225,7 +225,7 @@ private fun VideoPlayerScreen(item: VideoItem, onExit: () -> Unit) {
.padding(8.dp)
.combinedClickable(onClick = onExit, onLongClick = {}),
) {
EdgeCropText(text = "<- back", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = "<- back", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
Box(
Modifier
@@ -234,12 +234,12 @@ private fun VideoPlayerScreen(item: VideoItem, onExit: () -> Unit) {
) {
BasicText(
text = item.title,
style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = LocalXuneColors.current.textPrimary),
style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = LocalDoradoColors.current.textPrimary),
)
}
}
// Transport + scrubber bar.
Column(Modifier.padding(horizontal = XuneTokens.EDGE.dp, vertical = 6.dp)) {
Column(Modifier.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 6.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Box(
Modifier
@@ -253,14 +253,14 @@ private fun VideoPlayerScreen(item: VideoItem, onExit: () -> Unit) {
) {
EdgeCropText(
text = if (isPlaying) "pause" else "play",
fontSize = XuneTokens.TYPE_NOW_META.dp,
color = LocalXuneColors.current.accent,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
color = LocalDoradoColors.current.accent,
)
}
EdgeCropText(
text = "${positionMs / 1000}s / ${if (durationMs > 0) durationMs / 1000 else "?"}s",
fontSize = XuneTokens.TYPE_CAPTION.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
color = LocalDoradoColors.current.textSecondary,
modifier = Modifier.weight(1f),
)
}
@@ -269,7 +269,7 @@ private fun VideoPlayerScreen(item: VideoItem, onExit: () -> Unit) {
Modifier
.fillMaxWidth()
.height(6.dp)
.background(LocalXuneColors.current.tile)
.background(LocalDoradoColors.current.tile)
.combinedClickable(
onClick = {},
onLongClick = {},
@@ -280,7 +280,7 @@ private fun VideoPlayerScreen(item: VideoItem, onExit: () -> Unit) {
Modifier
.height(6.dp)
.fillMaxWidth(frac)
.background(LocalXuneColors.current.accent),
.background(LocalDoradoColors.current.accent),
)
}
}
@@ -297,7 +297,7 @@ private enum class PicturePivot { ALL, ALBUMS, DATE_TAKEN, FAVORITES }
@Composable
fun PicturesScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
val context = LocalContext.current
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
var buckets by remember { mutableStateOf<List<PictureBucket>>(emptyList()) }
var flat by remember { mutableStateOf<List<PictureItem>>(emptyList()) }
@@ -306,7 +306,7 @@ fun PicturesScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
val pagerState = androidx.compose.foundation.pager.rememberPagerState(initialPage = 0, pageCount = { 4 })
val pinned by graph.quickplay.pins().collectAsState(initial = emptyList())
val favoriteItems = remember(pinned, flat) {
pinned.filter { it.kind == com.heretek.xunehd.data.model.PinKind.PICTURE }
pinned.filter { it.kind == com.heretek.dorado_hd.data.model.PinKind.PICTURE }
.mapNotNull { card ->
val uri = card.subLabel.takeIf { it.startsWith("content://") || it.startsWith("file://") }
?: return@mapNotNull null
@@ -357,7 +357,7 @@ fun PicturesScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
val viewing = selectedBucket
if (viewing != null) {
val pagerStateBucket = rememberPagerState(initialPage = viewerIndex.coerceAtMost(viewing.items.lastIndex), pageCount = { viewing.items.size })
Box(Modifier.fillMaxSize().background(LocalXuneColors.current.background)) {
Box(Modifier.fillMaxSize().background(LocalDoradoColors.current.background)) {
HorizontalPager(state = pagerStateBucket, modifier = Modifier.fillMaxSize()) { p ->
val item = viewing.items[p]
AsyncImage(
@@ -374,15 +374,15 @@ fun PicturesScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
.combinedClickable(
onClick = {
val item = viewing.items[pagerStateBucket.currentPage]
scope.launch { graph.quickplay.pin(com.heretek.xunehd.data.model.PinKind.PICTURE, item.id, item.displayName, item.uri.toString(), 0) }
scope.launch { graph.quickplay.pin(com.heretek.dorado_hd.data.model.PinKind.PICTURE, item.id, item.displayName, item.uri.toString(), 0) }
},
onLongClick = {},
),
) {
EdgeCropText(text = "pin", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = "pin", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
Box(Modifier.align(Alignment.TopStart).padding(8.dp).combinedClickable(onClick = { selectedBucket = null }, onLongClick = {})) {
EdgeCropText(text = "<- albums", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = "<- albums", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
}
return
@@ -406,17 +406,17 @@ fun PicturesScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = { selectedBucket = bucket; viewerIndex = 0 },
onLongClick = {},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = bucket.name, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = "${bucket.items.size} photos", fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = bucket.name, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(text = "${bucket.items.size} photos", fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
}
}
@@ -441,7 +441,7 @@ private fun PictureListContent(pictures: List<PictureItem>, onPicture: (PictureI
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(
text = "no pictures here",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
)
}
@@ -455,14 +455,14 @@ private fun PictureListContent(pictures: List<PictureItem>, onPicture: (PictureI
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(onClick = { onPicture(p) }, onLongClick = {})
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = p.displayName, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = "photo", fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = p.displayName, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(text = "photo", fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
}
},
@@ -496,7 +496,7 @@ fun InternetScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
DetailScaffold(title = "internet") {
Column(Modifier.fillMaxSize()) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(XuneTokens.EDGE.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(DoradoTokens.EDGE.dp)) {
// Back / forward in-page navigation (canon §3.6).
Box(
Modifier.combinedClickable(
@@ -507,8 +507,8 @@ fun InternetScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
) {
EdgeCropText(
text = "<",
fontSize = XuneTokens.TYPE_NOW_META.dp,
color = if (canBack) LocalXuneColors.current.accent else LocalXuneColors.current.textInactive,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
color = if (canBack) LocalDoradoColors.current.accent else LocalDoradoColors.current.textInactive,
)
}
Box(
@@ -520,17 +520,17 @@ fun InternetScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
) {
EdgeCropText(
text = ">",
fontSize = XuneTokens.TYPE_NOW_META.dp,
color = if (canForward) LocalXuneColors.current.accent else LocalXuneColors.current.textInactive,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
color = if (canForward) LocalDoradoColors.current.accent else LocalDoradoColors.current.textInactive,
)
}
BasicTextField(
value = input,
onValueChange = { input = it },
singleLine = true,
textStyle = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_LIST.sp, color = LocalXuneColors.current.textPrimary),
cursorBrush = androidx.compose.ui.graphics.SolidColor(LocalXuneColors.current.accent),
modifier = Modifier.weight(1f).background(LocalXuneColors.current.elevated).padding(6.dp),
textStyle = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_LIST.sp, color = LocalDoradoColors.current.textPrimary),
cursorBrush = androidx.compose.ui.graphics.SolidColor(LocalDoradoColors.current.accent),
modifier = Modifier.weight(1f).background(LocalDoradoColors.current.elevated).padding(6.dp),
)
Box(
Modifier
@@ -540,7 +540,7 @@ fun InternetScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
onLongClick = {},
),
) {
EdgeCropText(text = "go", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = "go", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
Box(
Modifier
@@ -550,11 +550,11 @@ fun InternetScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
onLongClick = {},
),
) {
EdgeCropText(text = "*", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = "*", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
}
AndroidView(
modifier = Modifier.weight(1f).background(LocalXuneColors.current.background),
modifier = Modifier.weight(1f).background(LocalDoradoColors.current.background),
factory = { ctx ->
WebView(ctx).apply {
settings.javaScriptEnabled = true
@@ -581,10 +581,10 @@ fun InternetScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
text = "bookmarks: " + bookmarks.joinToString(" · "),
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_CAPTION.sp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_CAPTION.sp,
color = LocalDoradoColors.current.textSecondary,
),
modifier = Modifier.padding(XuneTokens.EDGE.dp),
modifier = Modifier.padding(DoradoTokens.EDGE.dp),
)
}
}
@@ -605,19 +605,19 @@ fun SocialScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
Column(Modifier.fillMaxSize()) {
EdgeCropText(
text = "the social feed is frozen in time.",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.6f,
modifier = Modifier.padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp),
modifier = Modifier.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp),
)
FROZEN_FEED.forEach { post ->
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp),
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp),
) {
EdgeCropText(text = post.author, fontSize = XuneTokens.TYPE_NOW_META.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = post.text, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = post.when_, fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = post.author, fontSize = DoradoTokens.TYPE_NOW_META.dp, color = LocalDoradoColors.current.accent)
EdgeCropText(text = post.text, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(text = post.when_, fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
}
}
@@ -627,7 +627,7 @@ fun SocialScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
@Composable
fun PictureDetailScreen(uri: String) {
DetailScaffold(title = "picture") {
Box(Modifier.fillMaxSize().background(LocalXuneColors.current.background)) {
Box(Modifier.fillMaxSize().background(LocalDoradoColors.current.background)) {
AsyncImage(
model = android.net.Uri.parse(uri),
contentDescription = null,
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.screens
package com.heretek.dorado_hd.ui.screens
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -22,20 +22,20 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.XuneMotion
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.AlbumArt
import com.heretek.xunehd.design.components.CrossbarBar
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.data.model.PinKind
import com.heretek.xunehd.design.components.KineticList
import com.heretek.xunehd.design.components.firstLetterOf
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.LocalContextMenu
import com.heretek.xunehd.ui.components.MenuAction
import com.heretek.xunehd.ui.components.TrackRow
import com.heretek.xunehd.ui.nav.XuneDestination
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.DoradoMotion
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.AlbumArt
import com.heretek.dorado_hd.design.components.CrossbarBar
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.data.model.PinKind
import com.heretek.dorado_hd.design.components.KineticList
import com.heretek.dorado_hd.design.components.firstLetterOf
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.LocalContextMenu
import com.heretek.dorado_hd.ui.components.MenuAction
import com.heretek.dorado_hd.ui.components.TrackRow
import com.heretek.dorado_hd.ui.nav.DoradoDestination
import kotlinx.coroutines.launch
/** Real Zune HD music crossbar order. */
@@ -72,22 +72,22 @@ fun MusicScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun AlbumsTab() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val menus = LocalContextMenu.current
val scope = rememberCoroutineScope()
val albums by graph.library.albums().collectAsState(initial = emptyList())
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = XuneTokens.ALBUM_TILE.dp),
contentPadding = PaddingValues(XuneTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(XuneTokens.GRID_GUTTER.dp),
columns = GridCells.Adaptive(minSize = DoradoTokens.ALBUM_TILE.dp),
contentPadding = PaddingValues(DoradoTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(DoradoTokens.GRID_GUTTER.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxSize(),
) {
items(albums, key = { it.albumId }) { album ->
Column(
Modifier.combinedClickable(
onClick = { graph.nav.push(XuneDestination.Album(album.albumId)) },
onClick = { graph.nav.push(DoradoDestination.Album(album.albumId)) },
onLongClick = {
menus.show(
title = album.title,
@@ -100,7 +100,7 @@ private fun AlbumsTab() {
}
},
MenuAction("view artist") {
graph.nav.push(XuneDestination.Artist(album.artistId))
graph.nav.push(DoradoDestination.Artist(album.artistId))
},
),
)
@@ -114,13 +114,13 @@ private fun AlbumsTab() {
)
EdgeCropText(
text = album.title,
fontSize = XuneTokens.TYPE_CAPTION.dp,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
modifier = Modifier.fillMaxWidth(),
)
EdgeCropText(
text = album.artist,
fontSize = XuneTokens.TYPE_CAPTION.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
color = LocalDoradoColors.current.textSecondary,
modifier = Modifier.fillMaxWidth(),
)
}
@@ -131,7 +131,7 @@ private fun AlbumsTab() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ArtistsTab() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val menus = LocalContextMenu.current
val scope = rememberCoroutineScope()
val artists by graph.library.artists().collectAsState(initial = emptyList())
@@ -144,9 +144,9 @@ private fun ArtistsTab() {
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = { graph.nav.push(XuneDestination.Artist(artist.artistId)) },
onClick = { graph.nav.push(DoradoDestination.Artist(artist.artistId)) },
onLongClick = {
menus.show(
title = artist.name,
@@ -160,15 +160,15 @@ private fun ArtistsTab() {
)
},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = artist.name, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = artist.name, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(
text = "${artist.albumCount} albums — ${artist.trackCount} songs",
fontSize = XuneTokens.TYPE_LIST_SECONDARY.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_LIST_SECONDARY.dp,
color = LocalDoradoColors.current.textSecondary,
)
}
}
@@ -179,13 +179,13 @@ private fun ArtistsTab() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PlaylistsTab() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val menus = LocalContextMenu.current
val scope = rememberCoroutineScope()
val playlists by graph.library.playlists().collectAsState(initial = emptyList())
androidx.compose.foundation.layout.Box(Modifier.fillMaxSize()) {
com.heretek.xunehd.design.components.KineticList(
com.heretek.dorado_hd.design.components.KineticList(
items = playlists,
key = { it.id },
letter = { firstLetterOf(it.name) },
@@ -193,9 +193,9 @@ private fun PlaylistsTab() {
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = { graph.nav.push(XuneDestination.PlaylistDetail(playlist.id)) },
onClick = { graph.nav.push(DoradoDestination.PlaylistDetail(playlist.id)) },
onLongClick = {
menus.show(
title = playlist.name,
@@ -207,15 +207,15 @@ private fun PlaylistsTab() {
)
},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = playlist.name, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = playlist.name, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(
text = "${playlist.trackCount} songs",
fontSize = XuneTokens.TYPE_LIST_SECONDARY.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_LIST_SECONDARY.dp,
color = LocalDoradoColors.current.textSecondary,
)
}
}
@@ -225,7 +225,7 @@ private fun PlaylistsTab() {
// Create playlist affordance, styled as plain text like everything else.
EdgeCropText(
text = "+ new playlist",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
alpha = 0.6f,
modifier = Modifier
.align(Alignment.BottomStart)
@@ -238,7 +238,7 @@ private fun PlaylistsTab() {
},
onLongClick = {},
)
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 12.dp),
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 12.dp),
)
}
}
@@ -246,7 +246,7 @@ private fun PlaylistsTab() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SongsTab() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val menus = LocalContextMenu.current
val scope = rememberCoroutineScope()
val songs by graph.library.tracks().collectAsState(initial = emptyList())
@@ -289,8 +289,8 @@ private fun SongsTab() {
},
)
},
MenuAction("view album") { graph.nav.push(XuneDestination.Album(track.albumId)) },
MenuAction("view artist") { graph.nav.push(XuneDestination.Artist(track.artistId)) },
MenuAction("view album") { graph.nav.push(DoradoDestination.Album(track.albumId)) },
MenuAction("view artist") { graph.nav.push(DoradoDestination.Artist(track.artistId)) },
),
)
},
@@ -302,7 +302,7 @@ private fun SongsTab() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun GenresTab() {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val genres by graph.library.genres().collectAsState(initial = emptyList())
KineticList(
@@ -313,20 +313,20 @@ private fun GenresTab() {
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = { graph.nav.push(XuneDestination.Genre(genre.name)) },
onClick = { graph.nav.push(DoradoDestination.Genre(genre.name)) },
onLongClick = {},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = genre.name, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = genre.name, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(
text = "${genre.trackCount} songs",
fontSize = XuneTokens.TYPE_LIST_SECONDARY.dp,
color = LocalXuneColors.current.textSecondary,
fontSize = DoradoTokens.TYPE_LIST_SECONDARY.dp,
color = LocalDoradoColors.current.textSecondary,
)
}
}
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.screens
package com.heretek.dorado_hd.ui.screens
import android.content.Context
import android.graphics.Bitmap
@@ -55,18 +55,18 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.allowHardware
import com.heretek.xunehd.R
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneMotion
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.AlbumArt
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.data.model.RepeatMode
import com.heretek.xunehd.data.model.Rating
import com.heretek.xunehd.data.model.Track
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.nav.XuneDestination
import com.heretek.dorado_hd.R
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoMotion
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.AlbumArt
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.data.model.RepeatMode
import com.heretek.dorado_hd.data.model.Rating
import com.heretek.dorado_hd.data.model.Track
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.nav.DoradoDestination
import java.io.File
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
@@ -74,7 +74,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private const val SKIP_DRAG_THRESHOLD = XuneTokens.SKIP_DRAG_PX.toFloat()
private const val SKIP_DRAG_THRESHOLD = DoradoTokens.SKIP_DRAG_PX.toFloat()
/**
* The Zune HD Now Playing: a metadata card floating over artist photography.
@@ -84,9 +84,9 @@ private const val SKIP_DRAG_THRESHOLD = XuneTokens.SKIP_DRAG_PX.toFloat()
*/
@Composable
fun NowPlayingScreen(canvasWidth: Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val controller = graph.controller
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val track by controller.nowPlaying.collectAsState()
val isPlaying by controller.isPlaying.collectAsState()
@@ -105,7 +105,7 @@ fun NowPlayingScreen(canvasWidth: Dp) {
// Idle → screensaver, exactly as the device does after a few seconds.
LaunchedEffect(interactionKey, current?.mediaId, isPlaying) {
if (current == null) return@LaunchedEffect
delay(XuneTokens.IDLE_SCREENSAVER_MS)
delay(DoradoTokens.IDLE_SCREENSAVER_MS)
if (!overlay) screensaver = true
}
@@ -118,7 +118,7 @@ fun NowPlayingScreen(canvasWidth: Dp) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(
text = "nothing playing",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
)
}
@@ -165,15 +165,15 @@ fun NowPlayingScreen(canvasWidth: Dp) {
// The floating card.
AnimatedVisibility(
visible = !screensaver,
enter = fadeIn(XuneMotion.pivot()),
exit = fadeOut(XuneMotion.pivot()),
enter = fadeIn(DoradoMotion.pivot()),
exit = fadeOut(DoradoMotion.pivot()),
) {
Column(Modifier.fillMaxSize()) {
// Explicit back arrow, per the canon.
Row(
Modifier
.fillMaxWidth()
.padding(start = XuneTokens.EDGE.dp, top = 8.dp),
.padding(start = DoradoTokens.EDGE.dp, top = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@@ -191,7 +191,7 @@ fun NowPlayingScreen(canvasWidth: Dp) {
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.Top,
) {
AlbumArt(
@@ -202,34 +202,34 @@ fun NowPlayingScreen(canvasWidth: Dp) {
Column(Modifier.padding(start = 14.dp)) {
EdgeCropText(
text = current.title,
fontSize = XuneTokens.TYPE_NOW_TITLE.dp,
fontSize = DoradoTokens.TYPE_NOW_TITLE.dp,
// Canon §4: the song title tappable → the album's track list.
modifier = Modifier.clickable {
poke()
graph.nav.push(XuneDestination.Album(current.albumId))
graph.nav.push(DoradoDestination.Album(current.albumId))
},
)
EdgeCropText(
text = current.artist,
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.85f,
modifier = Modifier.clickable {
poke()
graph.nav.push(XuneDestination.Artist(current.artistId))
graph.nav.push(DoradoDestination.Artist(current.artistId))
},
)
EdgeCropText(
text = current.album,
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.85f,
modifier = Modifier.clickable {
poke()
graph.nav.push(XuneDestination.Album(current.albumId))
graph.nav.push(DoradoDestination.Album(current.albumId))
},
)
EdgeCropText(
text = "${formatTime(positionMs)} / ${formatTime(durationMs)}",
fontSize = XuneTokens.TYPE_CAPTION.dp,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
alpha = 0.6f,
modifier = Modifier.padding(top = 6.dp),
)
@@ -242,7 +242,7 @@ fun NowPlayingScreen(canvasWidth: Dp) {
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 14.dp),
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.spacedBy(18.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -292,8 +292,8 @@ fun NowPlayingScreen(canvasWidth: Dp) {
// The screensaver: slowly drifting metadata over the photography.
AnimatedVisibility(
visible = screensaver,
enter = fadeIn(XuneMotion.pivot()),
exit = fadeOut(XuneMotion.pivot()),
enter = fadeIn(DoradoMotion.pivot()),
exit = fadeOut(DoradoMotion.pivot()),
) {
Box(
Modifier
@@ -314,8 +314,8 @@ fun NowPlayingScreen(canvasWidth: Dp) {
// Transport overlay.
AnimatedVisibility(
visible = overlay,
enter = fadeIn(XuneMotion.pivot()),
exit = fadeOut(XuneMotion.pivot()),
enter = fadeIn(DoradoMotion.pivot()),
exit = fadeOut(DoradoMotion.pivot()),
) {
TransportOverlay(
isPlaying = isPlaying,
@@ -335,7 +335,7 @@ private fun BackdropLayer(
backdropFile: File?,
washColors: Pair<Color, Color>?,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
if (backdropFile != null) {
val transition = rememberInfiniteTransition(label = "kenburns")
val scale by transition.animateFloat(
@@ -391,7 +391,7 @@ private fun BackdropLayer(
@Composable
private fun ScreensaverLayer(track: Track, positionMs: Long, durationMs: Long) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
val transition = rememberInfiniteTransition(label = "saver-drift")
val drift by transition.animateFloat(
initialValue = 30f,
@@ -406,22 +406,22 @@ private fun ScreensaverLayer(track: Track, positionMs: Long, durationMs: Long) {
.align(Alignment.CenterStart)
.fillMaxWidth()
.graphicsLayer { translationY = drift }
.padding(start = XuneTokens.EDGE.dp, end = 80.dp),
.padding(start = DoradoTokens.EDGE.dp, end = 80.dp),
) {
EdgeCropText(text = track.title, fontSize = XuneTokens.TYPE_SAVER_TITLE.dp)
EdgeCropText(text = track.title, fontSize = DoradoTokens.TYPE_SAVER_TITLE.dp)
EdgeCropText(
text = track.artist,
fontSize = XuneTokens.TYPE_SAVER_ARTIST.dp,
fontSize = DoradoTokens.TYPE_SAVER_ARTIST.dp,
alpha = 0.9f,
)
EdgeCropText(
text = track.album,
fontSize = XuneTokens.TYPE_SAVER_ALBUM.dp,
fontSize = DoradoTokens.TYPE_SAVER_ALBUM.dp,
alpha = 0.7f,
)
EdgeCropText(
text = formatTime(durationMs),
fontSize = XuneTokens.TYPE_CAPTION.dp,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
alpha = 0.5f,
modifier = Modifier.padding(top = 8.dp),
)
@@ -431,7 +431,7 @@ private fun ScreensaverLayer(track: Track, positionMs: Long, durationMs: Long) {
contentDescription = null,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(XuneTokens.EDGE.dp)
.padding(DoradoTokens.EDGE.dp)
.size(56.dp)
.graphicsLayer { alpha = 0.92f },
)
@@ -448,8 +448,8 @@ private fun TransportOverlay(
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val controller = LocalXuneGraph.current.controller
val colors = LocalXuneColors.current
val controller = LocalDoradoGraph.current.controller
val colors = LocalDoradoColors.current
val audio = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
var volumePulse by remember { mutableStateOf(0) }
@@ -549,7 +549,7 @@ private fun TransportOverlay(
text = "volume $volumePulse",
style = TextStyle(
fontFamily = Selawik,
fontSize = XuneTokens.TYPE_LIST.sp,
fontSize = DoradoTokens.TYPE_LIST.sp,
color = colors.textSecondary,
textAlign = TextAlign.Center,
),
@@ -563,7 +563,7 @@ private fun TransportOverlay(
@Composable
private fun OverlayGlyph(text: String) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
androidx.compose.foundation.text.BasicText(
text = text,
style = TextStyle(
@@ -578,7 +578,7 @@ private fun OverlayGlyph(text: String) {
/** Cached artist background photography fetch. */
@Composable
private fun rememberArtistBackdropFile(track: Track): androidx.compose.runtime.State<File?> {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val file = androidx.compose.runtime.produceState<File?>(initialValue = null, key1 = track.artist) {
val enabled = graph.artistImages.settingsSnapshot?.artistImagesEnabled ?: true
if (!enabled) {
@@ -1,6 +1,6 @@
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
package com.heretek.xunehd.ui.screens
package com.heretek.dorado_hd.ui.screens
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
@@ -20,18 +20,18 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.heretek.xunehd.data.db.PodcastEpisodeEntity
import com.heretek.xunehd.data.db.PodcastFeedEntity
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.design.components.KineticList
import com.heretek.xunehd.design.components.firstLetterOf
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.xunehd.ui.components.LocalContextMenu
import com.heretek.xunehd.ui.components.MenuAction
import com.heretek.xunehd.ui.nav.XuneDestination
import com.heretek.dorado_hd.data.db.PodcastEpisodeEntity
import com.heretek.dorado_hd.data.db.PodcastFeedEntity
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.design.components.KineticList
import com.heretek.dorado_hd.design.components.firstLetterOf
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.DetailScaffold
import com.heretek.dorado_hd.ui.components.LocalContextMenu
import com.heretek.dorado_hd.ui.components.MenuAction
import com.heretek.dorado_hd.ui.nav.DoradoDestination
import kotlinx.coroutines.launch
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
@@ -130,7 +130,7 @@ object PodcastFetcher {
val conn = URL(feedUrl).openConnection() as HttpURLConnection
conn.connectTimeout = 8000
conn.readTimeout = 15000
conn.setRequestProperty("User-Agent", "XuneHD/0.1")
conn.setRequestProperty("User-Agent", "DoradoHD/0.1")
if (conn.responseCode !in 200..299) null
else PodcastRss.parse(conn.inputStream)
} catch (_: Exception) { null }
@@ -139,7 +139,7 @@ object PodcastFetcher {
@Composable
fun PodcastsScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val menus = LocalContextMenu.current
val scope = rememberCoroutineScope()
val feeds by graph.podcasts.feeds().collectAsState(initial = emptyList())
@@ -151,7 +151,7 @@ fun PodcastsScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
Box(
Modifier
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp)
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp)
.combinedClickable(
onClick = {
menus.showPrompt("add feed", "https://example.com/feed.xml") { url ->
@@ -187,24 +187,24 @@ fun PodcastsScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
onLongClick = {},
),
) {
EdgeCropText(text = "+ add feed by url", fontSize = XuneTokens.TYPE_LIST.dp, color = LocalXuneColors.current.accent)
EdgeCropText(text = "+ add feed by url", fontSize = DoradoTokens.TYPE_LIST.dp, color = LocalDoradoColors.current.accent)
}
com.heretek.xunehd.design.components.CrossbarBar(
com.heretek.dorado_hd.design.components.CrossbarBar(
labels = listOf("audio", "video", "subscriptions", "episodes"),
selected = pagerState.currentPage,
onSelect = { idx -> scope.launch { pagerState.animateScrollToPage(idx) } },
)
androidx.compose.foundation.pager.HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page ->
when (page) {
0 -> PodcastsFeedList(feeds, onOpen = { feed -> graph.nav.push(XuneDestination.PodcastFeed(feed.id)) })
0 -> PodcastsFeedList(feeds, onOpen = { feed -> graph.nav.push(DoradoDestination.PodcastFeed(feed.id)) })
// 'video' is a placeholder on this build — we cannot distinguish
// audio vs video podcasts reliably from RSS alone; surface the
// same feeds as audio until MediaStore tagging exists.
1 -> PodcastsFeedList(feeds, onOpen = { feed -> graph.nav.push(XuneDestination.PodcastFeed(feed.id)) })
2 -> PodcastsFeedList(feeds, onOpen = { feed -> graph.nav.push(XuneDestination.PodcastFeed(feed.id)) })
1 -> PodcastsFeedList(feeds, onOpen = { feed -> graph.nav.push(DoradoDestination.PodcastFeed(feed.id)) })
2 -> PodcastsFeedList(feeds, onOpen = { feed -> graph.nav.push(DoradoDestination.PodcastFeed(feed.id)) })
else -> PodcastEpisodeList(episodes, onPlay = { ep ->
scope.launch {
val track = com.heretek.xunehd.data.model.Track(
val track = com.heretek.dorado_hd.data.model.Track(
mediaId = ep.episodeId,
title = ep.title,
artist = "",
@@ -229,46 +229,46 @@ fun PodcastsScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
@Composable
private fun PodcastsFeedList(
feeds: List<com.heretek.xunehd.data.db.PodcastFeedEntity>,
onOpen: (com.heretek.xunehd.data.db.PodcastFeedEntity) -> Unit,
feeds: List<com.heretek.dorado_hd.data.db.PodcastFeedEntity>,
onOpen: (com.heretek.dorado_hd.data.db.PodcastFeedEntity) -> Unit,
) {
val menus = com.heretek.xunehd.ui.components.LocalContextMenu.current
val menus = com.heretek.dorado_hd.ui.components.LocalContextMenu.current
val scope = rememberCoroutineScope()
val graph = com.heretek.xunehd.ui.LocalXuneGraph.current
val graph = com.heretek.dorado_hd.ui.LocalDoradoGraph.current
if (feeds.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(text = "no feeds yet", fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.4f)
EdgeCropText(text = "no feeds yet", fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.4f)
}
return
}
com.heretek.xunehd.design.components.KineticList(
com.heretek.dorado_hd.design.components.KineticList(
items = feeds,
key = { it.id },
letter = { com.heretek.xunehd.design.components.firstLetterOf(it.title) },
letter = { com.heretek.dorado_hd.design.components.firstLetterOf(it.title) },
rowContent = { feed, _ ->
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = { onOpen(feed) },
onLongClick = {
menus.show(
title = feed.title,
actions = listOf(
com.heretek.xunehd.ui.components.MenuAction("remove") {
com.heretek.dorado_hd.ui.components.MenuAction("remove") {
scope.launch { graph.podcasts.deleteFeed(feed.id) }
},
),
)
},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = feed.title, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = feed.description.take(60), fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = feed.title, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(text = feed.description.take(60), fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
}
},
@@ -277,31 +277,31 @@ private fun PodcastsFeedList(
@Composable
private fun PodcastEpisodeList(
episodes: List<com.heretek.xunehd.data.db.PodcastEpisodeFlat>,
onPlay: (com.heretek.xunehd.data.db.PodcastEpisodeFlat) -> Unit,
episodes: List<com.heretek.dorado_hd.data.db.PodcastEpisodeFlat>,
onPlay: (com.heretek.dorado_hd.data.db.PodcastEpisodeFlat) -> Unit,
) {
if (episodes.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(text = "no episodes", fontSize = XuneTokens.TYPE_NOW_META.dp, alpha = 0.4f)
EdgeCropText(text = "no episodes", fontSize = DoradoTokens.TYPE_NOW_META.dp, alpha = 0.4f)
}
return
}
com.heretek.xunehd.design.components.KineticList(
com.heretek.dorado_hd.design.components.KineticList(
items = episodes,
key = { it.episodeId },
letter = { com.heretek.xunehd.design.components.firstLetterOf(it.title) },
letter = { com.heretek.dorado_hd.design.components.firstLetterOf(it.title) },
rowContent = { ep, _ ->
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(onClick = { onPlay(ep) }, onLongClick = {})
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = ep.title, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = ep.feedTitle, fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = ep.title, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(text = ep.feedTitle, fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
}
},
@@ -310,7 +310,7 @@ private fun PodcastEpisodeList(
@Composable
fun PodcastFeedScreen(feedId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val episodes by graph.podcasts.episodes(feedId).collectAsState(initial = emptyList())
val feedTitle = remember(feedId) { "podcast" }
@@ -324,11 +324,11 @@ fun PodcastFeedScreen(feedId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = {
scope.launch {
val track = com.heretek.xunehd.data.model.Track(
val track = com.heretek.dorado_hd.data.model.Track(
mediaId = ep.id,
title = ep.title,
artist = "",
@@ -347,12 +347,12 @@ fun PodcastFeedScreen(feedId: Long, canvasWidth: androidx.compose.ui.unit.Dp) {
},
onLongClick = {},
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = ep.title, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = formatDuration(ep.durationMs), fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = ep.title, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(text = formatDuration(ep.durationMs), fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
}
},
@@ -1,6 +1,6 @@
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
package com.heretek.xunehd.ui.screens
package com.heretek.dorado_hd.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
@@ -29,17 +29,17 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.heretek.xunehd.data.db.RadioStationEntity
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.Selawik
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.design.components.KineticList
import com.heretek.xunehd.design.components.firstLetterOf
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.xunehd.ui.components.LocalContextMenu
import com.heretek.xunehd.ui.components.MenuAction
import com.heretek.dorado_hd.data.db.RadioStationEntity
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.Selawik
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.design.components.KineticList
import com.heretek.dorado_hd.design.components.firstLetterOf
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.DetailScaffold
import com.heretek.dorado_hd.ui.components.LocalContextMenu
import com.heretek.dorado_hd.ui.components.MenuAction
import kotlinx.coroutines.launch
/** Dial frequency format, e.g. 98700 kHz → "98.7 MHz". */
@@ -52,7 +52,7 @@ fun stepFrequency(freqKhz: Int, deltaPx: Float): Int =
@Composable
fun RadioScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val menus = LocalContextMenu.current
val scope = rememberCoroutineScope()
val stations by graph.radio.stations().collectAsState(initial = emptyList())
@@ -61,13 +61,13 @@ fun RadioScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
DetailScaffold(title = "radio") {
Column(Modifier.fillMaxSize()) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
// Dial sits at the top across all pivots.
Box(
Modifier
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp)
.height(XuneTokens.DIAL_HEIGHT.dp)
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp)
.height(DoradoTokens.DIAL_HEIGHT.dp)
.background(colors.elevated),
) {
androidx.compose.foundation.Canvas(Modifier.fillMaxSize()) {
@@ -79,7 +79,7 @@ fun RadioScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
for (mhz in 87..108) {
val f = (mhz - 87) / (108 - 87).toFloat()
val xx = w * f
val tickLen = if (mhz % 5 == 0) XuneTokens.DIAL_TICK_MAJOR.dp.toPx() else XuneTokens.DIAL_TICK_MINOR.dp.toPx()
val tickLen = if (mhz % 5 == 0) DoradoTokens.DIAL_TICK_MAJOR.dp.toPx() else DoradoTokens.DIAL_TICK_MINOR.dp.toPx()
drawLine(colors.border, Offset(xx, h / 2 - tickLen / 2), Offset(xx, h / 2 + tickLen / 2), 1f)
}
val cy = h / 2
@@ -100,8 +100,8 @@ fun RadioScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
)
BasicText(
text = formatDial(dialKhz),
style = TextStyle(fontFamily = Selawik, fontSize = XuneTokens.TYPE_NOW_META.sp, color = colors.textPrimary),
modifier = Modifier.align(Alignment.BottomStart).padding(XuneTokens.EDGE.dp / 2),
style = TextStyle(fontFamily = Selawik, fontSize = DoradoTokens.TYPE_NOW_META.sp, color = colors.textPrimary),
modifier = Modifier.align(Alignment.BottomStart).padding(DoradoTokens.EDGE.dp / 2),
)
}
val fmStations = stations.filter { !it.isPreset && it.frequencyKhz > 0 }
@@ -114,7 +114,7 @@ fun RadioScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
val pivotBuckets = listOf(fmStations, hdStations, presetStations)
val pivotLabels = listOf("FM", "HD", "presets")
val radioPagerState = androidx.compose.foundation.pager.rememberPagerState(initialPage = 0, pageCount = { 3 })
com.heretek.xunehd.design.components.CrossbarBar(
com.heretek.dorado_hd.design.components.CrossbarBar(
labels = pivotLabels,
selected = radioPagerState.currentPage,
onSelect = { i -> scope.launch { radioPagerState.animateScrollToPage(i) } },
@@ -122,11 +122,11 @@ fun RadioScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
androidx.compose.foundation.pager.HorizontalPager(state = radioPagerState, modifier = Modifier.fillMaxSize()) { page ->
val pivotList = pivotBuckets.getOrNull(page) ?: emptyList()
Column(Modifier.fillMaxSize()) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(horizontal = XuneTokens.EDGE.dp, vertical = 4.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(horizontal = DoradoTokens.EDGE.dp, vertical = 4.dp)) {
EdgeCropText(
text = "tune in",
fontSize = XuneTokens.TYPE_LIST.dp,
color = LocalXuneColors.current.accent,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = LocalDoradoColors.current.accent,
modifier = Modifier.combinedClickable(
onClick = {
val match = stations.firstOrNull { it.frequencyKhz == dialKhz }
@@ -143,8 +143,8 @@ fun RadioScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
Spacer(Modifier.weight(1f))
EdgeCropText(
text = "+ station",
fontSize = XuneTokens.TYPE_LIST.dp,
color = LocalXuneColors.current.accent,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = LocalDoradoColors.current.accent,
modifier = Modifier.combinedClickable(
onClick = {
menus.showPrompt("add station", "name|url") { text ->
@@ -205,7 +205,7 @@ private fun RadioStationList(
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EdgeCropText(
text = "no stations here",
fontSize = XuneTokens.TYPE_NOW_META.dp,
fontSize = DoradoTokens.TYPE_NOW_META.dp,
alpha = 0.4f,
)
}
@@ -219,25 +219,25 @@ private fun RadioStationList(
Row(
Modifier
.fillMaxWidth()
.height(XuneTokens.ROW_HEIGHT.dp)
.height(DoradoTokens.ROW_HEIGHT.dp)
.combinedClickable(
onClick = { onStation(s) },
onLongClick = { onLongPress(s) },
)
.padding(horizontal = XuneTokens.EDGE.dp),
.padding(horizontal = DoradoTokens.EDGE.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = s.name, fontSize = XuneTokens.TYPE_LIST.dp, color = if (s.id == current?.id) LocalXuneColors.current.accent else LocalXuneColors.current.textPrimary)
EdgeCropText(text = formatDial(s.frequencyKhz), fontSize = XuneTokens.TYPE_CAPTION.dp, color = LocalXuneColors.current.textSecondary)
EdgeCropText(text = s.name, fontSize = DoradoTokens.TYPE_LIST.dp, color = if (s.id == current?.id) LocalDoradoColors.current.accent else LocalDoradoColors.current.textPrimary)
EdgeCropText(text = formatDial(s.frequencyKhz), fontSize = DoradoTokens.TYPE_CAPTION.dp, color = LocalDoradoColors.current.textSecondary)
}
}
},
)
}
private fun playStation(graph: com.heretek.xunehd.XuneGraph, station: RadioStationEntity) {
val track = com.heretek.xunehd.data.model.Track(
private fun playStation(graph: com.heretek.dorado_hd.DoradoGraph, station: RadioStationEntity) {
val track = com.heretek.dorado_hd.data.model.Track(
mediaId = station.id,
title = station.name,
artist = "radio",
@@ -1,4 +1,4 @@
package com.heretek.xunehd.ui.screens
package com.heretek.dorado_hd.ui.screens
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -28,29 +28,29 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.heretek.xunehd.BuildConfig
import com.heretek.xunehd.design.LocalXuneColors
import com.heretek.xunehd.design.XuneAccent
import com.heretek.xunehd.design.XuneTokens
import com.heretek.xunehd.design.components.EdgeCropText
import com.heretek.xunehd.ui.LocalXuneGraph
import com.heretek.xunehd.ui.components.DetailScaffold
import com.heretek.xunehd.ui.components.SectionLabel
import com.heretek.dorado_hd.BuildConfig
import com.heretek.dorado_hd.design.LocalDoradoColors
import com.heretek.dorado_hd.design.DoradoAccent
import com.heretek.dorado_hd.design.DoradoTokens
import com.heretek.dorado_hd.design.components.EdgeCropText
import com.heretek.dorado_hd.ui.LocalDoradoGraph
import com.heretek.dorado_hd.ui.components.DetailScaffold
import com.heretek.dorado_hd.ui.components.SectionLabel
import kotlinx.coroutines.launch
@Composable
fun SettingsScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
val graph = LocalXuneGraph.current
val graph = LocalDoradoGraph.current
val scope = rememberCoroutineScope()
val context = LocalContext.current
val settings by graph.settingsFlow.collectAsState(initial = com.heretek.xunehd.data.repo.XuneSettings())
val colors = LocalXuneColors.current
val settings by graph.settingsFlow.collectAsState(initial = com.heretek.dorado_hd.data.repo.DoradoSettings())
val colors = LocalDoradoColors.current
val scanState by graph.library.scanning.collectAsState()
val importState by graph.library.importing.collectAsState()
val lastScanAt by graph.library.lastScanAt.collectAsState()
val lastScanResult by graph.library.lastScanResult.collectAsState()
val lastImportResult by graph.library.lastImportResult.collectAsState()
val menus = com.heretek.xunehd.ui.components.LocalContextMenu.current
val menus = com.heretek.dorado_hd.ui.components.LocalContextMenu.current
var trackCount by remember { mutableStateOf<Int?>(null) }
androidx.compose.runtime.LaunchedEffect(Unit) { trackCount = graph.library.trackCount() }
@@ -75,10 +75,10 @@ fun SettingsScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
) {
SectionLabel("accent")
Row(
Modifier.padding(horizontal = XuneTokens.EDGE.dp),
Modifier.padding(horizontal = DoradoTokens.EDGE.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
XuneAccent.entries.forEach { accent ->
DoradoAccent.entries.forEach { accent ->
val selected = accent == settings.accent
Box(
Modifier
@@ -151,16 +151,16 @@ fun SettingsScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
SectionLabel("about")
SettingsRow(
label = "xune hd ${BuildConfig.VERSION_NAME}",
subLabel = "sister to not-zune — the zune hd, reborn on android",
label = "dorado hd ${BuildConfig.VERSION_NAME}",
subLabel = "sister to dorado — the zune hd, reborn on android",
onClick = {},
)
Spacer(Modifier.height(12.dp))
EdgeCropText(
text = "selawik stands in for zegoe (sil ofl). zune, zegoe and the zune hd are trademarks of microsoft; this is an independent homage.",
fontSize = XuneTokens.TYPE_CAPTION.dp,
fontSize = DoradoTokens.TYPE_CAPTION.dp,
alpha = 0.08f,
modifier = Modifier.padding(horizontal = XuneTokens.EDGE.dp),
modifier = Modifier.padding(horizontal = DoradoTokens.EDGE.dp),
)
}
}
@@ -168,8 +168,8 @@ fun SettingsScreen(canvasWidth: androidx.compose.ui.unit.Dp) {
private fun scanStatusText(
lastScanAt: Long,
lastScanResult: com.heretek.xunehd.data.scan.MediaLibraryScanner.ScanResult?,
lastImportResult: com.heretek.xunehd.data.repo.LibraryRepository.ImportResult?,
lastScanResult: com.heretek.dorado_hd.data.scan.MediaLibraryScanner.ScanResult?,
lastImportResult: com.heretek.dorado_hd.data.repo.LibraryRepository.ImportResult?,
): String {
if (lastScanAt == 0L && lastImportResult == null) return "no scan yet"
val parts = mutableListOf<String>()
@@ -194,17 +194,17 @@ private fun SettingsRow(
subLabel: String,
onClick: () -> Unit,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
Column(
Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp),
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp),
) {
EdgeCropText(text = label, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = label, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(
text = subLabel,
fontSize = XuneTokens.TYPE_LIST_SECONDARY.dp,
fontSize = DoradoTokens.TYPE_LIST_SECONDARY.dp,
color = colors.textSecondary,
)
}
@@ -213,16 +213,16 @@ private fun SettingsRow(
/** Non-tappable read-only row (e.g. "library location" — canonical info, not an action). */
@Composable
private fun SettingsInfoRow(label: String, subLabel: String) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp),
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp),
) {
EdgeCropText(text = label, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = label, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(
text = subLabel,
fontSize = XuneTokens.TYPE_LIST_SECONDARY.dp,
fontSize = DoradoTokens.TYPE_LIST_SECONDARY.dp,
color = colors.textSecondary,
)
}
@@ -235,19 +235,19 @@ private fun SettingsToggle(
value: Boolean,
onChange: (Boolean) -> Unit,
) {
val colors = LocalXuneColors.current
val colors = LocalDoradoColors.current
Row(
Modifier
.fillMaxWidth()
.clickable { onChange(!value) }
.padding(horizontal = XuneTokens.EDGE.dp, vertical = 8.dp),
.padding(horizontal = DoradoTokens.EDGE.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
EdgeCropText(text = label, fontSize = XuneTokens.TYPE_LIST.dp)
EdgeCropText(text = label, fontSize = DoradoTokens.TYPE_LIST.dp)
EdgeCropText(
text = subLabel,
fontSize = XuneTokens.TYPE_LIST_SECONDARY.dp,
fontSize = DoradoTokens.TYPE_LIST_SECONDARY.dp,
color = colors.textSecondary,
)
}
@@ -259,7 +259,7 @@ private fun SettingsToggle(
) {
EdgeCropText(
text = if (value) "on" else "off",
fontSize = XuneTokens.TYPE_LIST.dp,
fontSize = DoradoTokens.TYPE_LIST.dp,
color = if (value) colors.textPrimary else colors.textSecondary,
)
}
@@ -1,8 +0,0 @@
package com.heretek.xunehd.ui
import androidx.compose.runtime.staticCompositionLocalOf
import com.heretek.xunehd.XuneGraph
val LocalXuneGraph = staticCompositionLocalOf<XuneGraph> {
error("XuneGraph not provided")
}
@@ -1,67 +0,0 @@
package com.heretek.xunehd.ui.apps
import androidx.compose.runtime.Composable
import com.heretek.xunehd.ui.LocalXuneGraph
/**
* A single mini-app: id, label, category, and a composable renderer that
* reads the XuneGraph from [LocalXuneGraph] and calls `nav.pop()` itself
* (e.g. via the cropped-header scaffold).
*/
data class XuneMiniApp(
val id: String,
val title: String,
val category: String,
val render: @Composable () -> Unit,
)
/**
* The registry of installed mini-apps (canon §8). Built once at first access;
* `byId` is used by the marketplace apps pivot and the destination router.
*/
object XuneApps {
val all: List<XuneMiniApp> by lazy { buildAppRegistry() }
fun byId(id: String): XuneMiniApp? = all.firstOrNull { it.id == id }
}
/** The master list of installed mini-apps (Phase 3/4 append here). */
fun buildAppRegistry(): List<XuneMiniApp> = listOf(
XuneMiniApp("calculator", "calculator", "utilities", { CalculatorApp() }),
XuneMiniApp("notes", "notes", "utilities", { NotesApp() }),
XuneMiniApp("stopwatch", "stopwatch", "utilities", { StopwatchApp() }),
XuneMiniApp("metronome", "metronome", "music", { MetronomeApp() }),
XuneMiniApp("alarm", "alarm clock", "utilities", { AlarmClockApp() }),
XuneMiniApp("calendar", "calendar", "utilities", { CalendarApp() }),
XuneMiniApp("level", "level", "utilities", { LevelApp() }),
XuneMiniApp("piano", "piano", "music", { PianoApp() }),
XuneMiniApp("drummachine", "drum machine", "music", { DrumMachineApp() }),
XuneMiniApp("chordfinder", "chord finder", "music", { ChordFinderApp() }),
XuneMiniApp("musicquiz", "music quiz", "music", { MusicQuizApp() }),
XuneMiniApp("shufflebyalbum", "shuffle by album", "music", { ShuffleByAlbumApp() }),
XuneMiniApp("solitaire", "solitaire", "games", { com.heretek.xunehd.ui.apps.games.SolitaireApp() }),
XuneMiniApp("sudoku", "sudoku", "games", { com.heretek.xunehd.ui.apps.games.SudokuApp() }),
XuneMiniApp("hexic", "hexic", "games", { com.heretek.xunehd.ui.apps.games.HexicApp() }),
XuneMiniApp("reversi", "reversi", "games", { com.heretek.xunehd.ui.apps.games.ReversiApp() }),
XuneMiniApp("hearts", "hearts", "games", { com.heretek.xunehd.ui.apps.games.HeartsApp() }),
XuneMiniApp("spades", "spades", "games", { com.heretek.xunehd.ui.apps.games.SpadesApp() }),
XuneMiniApp("checkers", "checkers", "games", { com.heretek.xunehd.ui.apps.games.CheckersApp() }),
XuneMiniApp("chess", "chess", "games", { com.heretek.xunehd.ui.apps.games.ChessApp() }),
XuneMiniApp("texasholdem", "texas hold 'em", "games", { com.heretek.xunehd.ui.apps.games.TexasHoldemApp() }),
XuneMiniApp("weather", "weather", "reading", { com.heretek.xunehd.ui.apps.mocks.WeatherMock() }),
XuneMiniApp("twitter", "twitter", "social", { com.heretek.xunehd.ui.apps.mocks.TwitterMock() }),
XuneMiniApp("facebook", "facebook", "social", { com.heretek.xunehd.ui.apps.mocks.FacebookMock() }),
XuneMiniApp("email", "email", "networking", { com.heretek.xunehd.ui.apps.mocks.EmailMock() }),
XuneMiniApp("messenger", "messenger", "networking", { com.heretek.xunehd.ui.apps.mocks.MessengerMock() }),
XuneMiniApp("msnmoney", "msn money", "reading", { com.heretek.xunehd.ui.apps.mocks.MsnMoneyMock() }),
XuneMiniApp("zunereader", "zune reader", "reading", { com.heretek.xunehd.ui.apps.mocks.ZuneReaderMock() }),
XuneMiniApp("zunesocial", "social", "social", { com.heretek.xunehd.ui.apps.mocks.SocialMock() }),
)
/** Mini-app scaffold: cropped-header back, fullscreen content. */
@Composable
fun MiniAppScaffold(title: String, onBack: () -> Unit, content: @Composable () -> Unit) {
com.heretek.xunehd.ui.components.DetailScaffold(title = title) {
content()
}
}
@@ -1,64 +0,0 @@
package com.heretek.xunehd.ui.nav
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.flow.MutableStateFlow
/**
* Lightweight hierarchical navigation. The Zune HD has no hardware back:
* going back is "tap the cut-off header", which calls [pop]. Slides are
* horizontal with the Zune deceleration curve.
*/
sealed interface XuneDestination {
data object Home : XuneDestination
data object Quickplay : XuneDestination
data object Music : XuneDestination
data class Album(val albumId: Long) : XuneDestination
data class Artist(val artistId: Long) : XuneDestination
data class Genre(val genre: String) : XuneDestination
data class PlaylistDetail(val playlistId: Long) : XuneDestination
data object NowPlaying : XuneDestination
data object Settings : XuneDestination
// Media pivots (canon §3.1, Phase 5 surfaces).
data object Videos : XuneDestination
data object Pictures : XuneDestination
data object Radio : XuneDestination
data object Podcasts : XuneDestination
data class PodcastFeed(val feedId: Long) : XuneDestination
data object Marketplace : XuneDestination
data object Social : XuneDestination
data object Internet : XuneDestination
// Mini-app platform (canon §8).
data class MiniApp(val appId: String) : XuneDestination
// A single pinned picture viewer — URI is stored in the pin's subLabel.
data class PictureDetail(val uri: String) : XuneDestination
}
class XuneNav {
private val stack = mutableStateListOf<XuneDestination>(XuneDestination.Home)
val current: XuneDestination
get() = stack.lastOrNull() ?: XuneDestination.Home
val depth: Int
get() = stack.size
fun push(destination: XuneDestination) {
stack.add(destination)
}
fun pop() {
if (stack.size > 1) stack.removeAt(stack.lastIndex)
}
fun toHome() {
stack.clear()
stack.add(XuneDestination.Home)
}
fun replaceTop(destination: XuneDestination) {
if (stack.isNotEmpty()) stack[stack.lastIndex] = destination else stack.add(destination)
}
}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/xune_mark" />
<monochrome android:drawable="@drawable/xune_mark" />
<foreground android:drawable="@drawable/dorado_mark" />
<monochrome android:drawable="@drawable/dorado_mark" />
</adaptive-icon>
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="xune_black">#FF111111</color>
<color name="xune_accent">#FFFA2A55</color>
<color name="dorado_black">#FF111111</color>
<color name="dorado_accent">#FFFA2A55</color>
<color name="ic_launcher_background">#FF111111</color>
</resources>
+1 -1
View File
@@ -1,3 +1,3 @@
<resources>
<string name="app_name">Xune HD</string>
<string name="app_name">Dorado HD</string>
</resources>
+4 -4
View File
@@ -1,8 +1,8 @@
<resources>
<style name="Theme.XuneHd" parent="android:Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/xune_black</item>
<item name="android:statusBarColor">@color/xune_black</item>
<item name="android:navigationBarColor">@color/xune_black</item>
<style name="Theme.DoradoHd" parent="android:Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/dorado_black</item>
<item name="android:statusBarColor">@color/dorado_black</item>
<item name="android:navigationBarColor">@color/dorado_black</item>
<item name="android:windowLightStatusBar">false</item>
</style>
</resources>
@@ -1,4 +1,4 @@
package com.heretek.xunehd
package com.heretek.dorado_hd
import java.io.File
import org.junit.Assert.assertTrue
@@ -7,7 +7,7 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
/**
* Design-invariant audit, ported from Not-Zune's approach. Scans the UI
* Design-invariant audit, ported from Dorado's approach. Scans the UI
* sources for violations of the Zune HD canon (docs/zune-hd-ui-canon.md §7).
*/
@RunWith(RobolectricTestRunner::class)
@@ -20,7 +20,7 @@ class DesignInvariantTest {
get() = appDir.absoluteFile.normalize().parentFile
private fun sources(dir: String): List<File> {
val root = File(appDir, "src/main/java/com/heretek/xunehd/$dir")
val root = File(appDir, "src/main/java/com/heretek/dorado_hd/$dir")
return root.walkTopDown().filter { it.isFile && it.extension == "kt" }.toList()
}
@@ -45,7 +45,7 @@ class DesignInvariantTest {
}
.toList()
assertTrue(
"Screens must use LocalXuneColors tokens, not raw colors: $offenders",
"Screens must use LocalDoradoColors tokens, not raw colors: $offenders",
offenders.isEmpty(),
)
}
@@ -67,7 +67,7 @@ class DesignInvariantTest {
}
.toList()
assertTrue(
"Screens must not reference platform color constants (use LocalXuneColors tokens): $offenders",
"Screens must not reference platform color constants (use LocalDoradoColors tokens): $offenders",
offenders.isEmpty(),
)
}
@@ -78,7 +78,7 @@ class DesignInvariantTest {
.filter { it.readText().contains("spring") }
.map { it.name }
assertTrue(
"Navigation must use XuneMotion deceleration (canon §6, invariant 6): $offenders",
"Navigation must use DoradoMotion deceleration (canon §6, invariant 6): $offenders",
offenders.isEmpty(),
)
}
@@ -86,10 +86,10 @@ class DesignInvariantTest {
@Test
fun `mini-app registry is non-empty`() {
val offenders = sources("ui/apps")
.filter { it.name == "XuneApps.kt" }
.filter { !it.readText().contains("XuneMiniApp(\"calculator\"") }
.filter { it.name == "DoradoApps.kt" }
.filter { !it.readText().contains("DoradoMiniApp(\"calculator\"") }
.map { it.path }
assertTrue("XuneApps registry should ship utilities (canon §8)", offenders.isEmpty())
assertTrue("DoradoApps registry should ship utilities (canon §8)", offenders.isEmpty())
}
@Test
@@ -1,40 +1,40 @@
package com.heretek.xunehd
package com.heretek.dorado_hd
import com.heretek.xunehd.data.model.PinKind
import com.heretek.xunehd.data.model.Rating
import com.heretek.xunehd.data.model.Track
import com.heretek.xunehd.design.XuneAccent
import com.heretek.xunehd.design.components.firstLetterOf
import com.heretek.xunehd.media.AlarmScheduler
import com.heretek.xunehd.media.PlaybackController
import com.heretek.xunehd.ui.apps.CalcEngine
import com.heretek.xunehd.ui.apps.ChordData
import com.heretek.xunehd.ui.apps.QuizEngine
import com.heretek.xunehd.ui.apps.games.CheckersColor
import com.heretek.xunehd.ui.apps.games.CheckersEngine
import com.heretek.xunehd.ui.apps.games.CheckersPiece
import com.heretek.xunehd.ui.apps.games.CheckersState
import com.heretek.xunehd.ui.apps.games.ChessColor
import com.heretek.xunehd.ui.apps.games.ChessEngine
import com.heretek.xunehd.ui.apps.games.ChessPiece
import com.heretek.xunehd.ui.apps.games.ChessPieceType
import com.heretek.xunehd.ui.apps.games.ChessState
import com.heretek.xunehd.ui.apps.games.HeartsEngine
import com.heretek.xunehd.ui.apps.games.HeartsSeat
import com.heretek.xunehd.ui.apps.games.HeartsState
import com.heretek.xunehd.ui.apps.games.HexColor
import com.heretek.xunehd.ui.apps.games.HexicEngine
import com.heretek.xunehd.ui.apps.games.PokerEngine
import com.heretek.xunehd.ui.apps.games.ReversiEngine
import com.heretek.xunehd.ui.apps.games.SolCard
import com.heretek.xunehd.ui.apps.games.SolSuit
import com.heretek.xunehd.ui.apps.games.SolitaireEngine
import com.heretek.xunehd.ui.apps.games.SpadesEngine
import com.heretek.xunehd.ui.apps.games.SpadesSeat
import com.heretek.xunehd.ui.apps.games.SudokuEngine
import com.heretek.xunehd.ui.screens.formatDial
import com.heretek.xunehd.ui.screens.formatTime
import com.heretek.xunehd.ui.screens.stepFrequency
import com.heretek.dorado_hd.data.model.PinKind
import com.heretek.dorado_hd.data.model.Rating
import com.heretek.dorado_hd.data.model.Track
import com.heretek.dorado_hd.design.DoradoAccent
import com.heretek.dorado_hd.design.components.firstLetterOf
import com.heretek.dorado_hd.media.AlarmScheduler
import com.heretek.dorado_hd.media.PlaybackController
import com.heretek.dorado_hd.ui.apps.CalcEngine
import com.heretek.dorado_hd.ui.apps.ChordData
import com.heretek.dorado_hd.ui.apps.QuizEngine
import com.heretek.dorado_hd.ui.apps.games.CheckersColor
import com.heretek.dorado_hd.ui.apps.games.CheckersEngine
import com.heretek.dorado_hd.ui.apps.games.CheckersPiece
import com.heretek.dorado_hd.ui.apps.games.CheckersState
import com.heretek.dorado_hd.ui.apps.games.ChessColor
import com.heretek.dorado_hd.ui.apps.games.ChessEngine
import com.heretek.dorado_hd.ui.apps.games.ChessPiece
import com.heretek.dorado_hd.ui.apps.games.ChessPieceType
import com.heretek.dorado_hd.ui.apps.games.ChessState
import com.heretek.dorado_hd.ui.apps.games.HeartsEngine
import com.heretek.dorado_hd.ui.apps.games.HeartsSeat
import com.heretek.dorado_hd.ui.apps.games.HeartsState
import com.heretek.dorado_hd.ui.apps.games.HexColor
import com.heretek.dorado_hd.ui.apps.games.HexicEngine
import com.heretek.dorado_hd.ui.apps.games.PokerEngine
import com.heretek.dorado_hd.ui.apps.games.ReversiEngine
import com.heretek.dorado_hd.ui.apps.games.SolCard
import com.heretek.dorado_hd.ui.apps.games.SolSuit
import com.heretek.dorado_hd.ui.apps.games.SolitaireEngine
import com.heretek.dorado_hd.ui.apps.games.SpadesEngine
import com.heretek.dorado_hd.ui.apps.games.SpadesSeat
import com.heretek.dorado_hd.ui.apps.games.SudokuEngine
import com.heretek.dorado_hd.ui.screens.formatDial
import com.heretek.dorado_hd.ui.screens.formatTime
import com.heretek.dorado_hd.ui.screens.stepFrequency
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -76,10 +76,10 @@ class LogicTest {
@Test
fun `accent ids are unique and pink is default`() {
val ids = XuneAccent.entries.map { it.id }
val ids = DoradoAccent.entries.map { it.id }
assertEquals(ids.size, ids.toSet().size)
assertEquals(0, XuneAccent.PINK.id)
assertEquals(5, XuneAccent.entries.size)
assertEquals(0, DoradoAccent.PINK.id)
assertEquals(5, DoradoAccent.entries.size)
}
@Test
@@ -333,8 +333,8 @@ class LogicTest {
turn = CheckersColor.RED,
captureChain = null, winner = null,
)
setup.board[2][1] = com.heretek.xunehd.ui.apps.games.checkersPiece(CheckersColor.RED, false)
setup.board[3][2] = com.heretek.xunehd.ui.apps.games.checkersPiece(CheckersColor.BLACK, false)
setup.board[2][1] = com.heretek.dorado_hd.ui.apps.games.checkersPiece(CheckersColor.RED, false)
setup.board[3][2] = com.heretek.dorado_hd.ui.apps.games.checkersPiece(CheckersColor.BLACK, false)
val moves = CheckersEngine.legalMoves(setup, CheckersColor.RED)
assertEquals(1, moves.size)
assertTrue(moves.any { it.second == (4 to 3) })
@@ -378,10 +378,10 @@ class LogicTest {
// Frozen catalog invariant from the gap audit: every installedId must
// point to a real entry in the marketplace apps pivot, and the total
// package count must match docs/zcp-inventory.md.
val all = com.heretek.xunehd.data.official.OfficialCatalog.all
val all = com.heretek.dorado_hd.data.official.OfficialCatalog.all
assertEquals("catalog must match the ZuneRedux 62-entry set", 62, all.size)
val installed = all.mapNotNull { it.installedId }.distinct()
val missing = installed.filter { id -> com.heretek.xunehd.ui.apps.XuneApps.byId(id) == null }
val missing = installed.filter { id -> com.heretek.dorado_hd.ui.apps.DoradoApps.byId(id) == null }
assertTrue(
"installed catalog ids should resolve to a real mini-app: missing=$missing",
missing.isEmpty(),
@@ -395,15 +395,15 @@ class LogicTest {
}
@Test
fun `XuneColors tokens match the spec table`() {
fun `DoradoColors tokens match the spec table`() {
// Single-source-of-truth checks against docs/design-tokens.md. The build
// invariant catches raw color literals; this guards the token values
// themselves (background #111111, watermark alpha 0.08, etc.).
val bg = androidx.compose.ui.graphics.Color(0xFF111111)
val elevated = androidx.compose.ui.graphics.Color(0xFF181818)
val tile = androidx.compose.ui.graphics.Color(0xFF202020)
assertEquals(bg, com.heretek.xunehd.design.XuneColors(accent = XuneAccent.PINK.primary, accentBright = XuneAccent.PINK.bright).background)
assertEquals(elevated, com.heretek.xunehd.design.XuneColors(accent = XuneAccent.PINK.primary, accentBright = XuneAccent.PINK.bright).elevated)
assertEquals(tile, com.heretek.xunehd.design.XuneColors(accent = XuneAccent.PINK.primary, accentBright = XuneAccent.PINK.bright).tile)
assertEquals(bg, com.heretek.dorado_hd.design.DoradoColors(accent = DoradoAccent.PINK.primary, accentBright = DoradoAccent.PINK.bright).background)
assertEquals(elevated, com.heretek.dorado_hd.design.DoradoColors(accent = DoradoAccent.PINK.primary, accentBright = DoradoAccent.PINK.bright).elevated)
assertEquals(tile, com.heretek.dorado_hd.design.DoradoColors(accent = DoradoAccent.PINK.primary, accentBright = DoradoAccent.PINK.bright).tile)
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
# Xune-HD Design Tokens
# Dorado-HD Design Tokens
Ported from the Not-Zune `zune-design-system` skill (MIT) and adapted for the
Ported from the Dorado `zune-design-system` skill (MIT) and adapted for the
Zune HD device UI. All Compose code must consume these tokens; raw hex literals
in screens are an invariant violation.
+1 -1
View File
@@ -3,7 +3,7 @@
Generated by `tools/zcp_inventory.py` from the ZuneRedux `zune-hd-apps`
archive (62 installable marketplace packages). Design reference only —
per canon §8 and NOTICE, no Microsoft binary or asset is bundled or
shipped by Xune-HD.
shipped by Dorado-HD.
## Container format (NX)
+15 -15
View File
@@ -1,10 +1,10 @@
# Zune HD UI Canon
The authoritative description of the Microsoft Zune HD on-device interface that
Xune-HD replicates. Compiled from contemporaneous reviews (CNET, Gizmodo,
Dorado-HD replicates. Compiled from contemporaneous reviews (CNET, Gizmodo,
SlashGear, TechCrunch, Windows Central, ZDNet, PCMag, ITPro Today, Sept 2009),
the ZuneDev/Not-Zune decompilation corpus, and ZuneRedux community resources.
Every screen in Xune-HD must be checkable against this document.
the ZuneDev/Dorado decompilation corpus, and ZuneRedux community resources.
Every screen in Dorado-HD must be checkable against this document.
## 1. Hardware & canvas
@@ -33,7 +33,7 @@ Every screen in Xune-HD must be checkable against this document.
### 3.1 Home menu (default view)
A vertical text list: `music · videos · pictures · radio · marketplace ·
social · podcasts · internet · settings` (Zune HD firmware 4.x). In Xune-HD
social · podcasts · internet · settings` (Zune HD firmware 4.x). In Dorado-HD
the functional entries are `music` and `settings`; future pivots may join.
- Flick vertically to scroll (kinetic).
- Tap an entry to enter. The whole entry is the button — text only.
@@ -47,7 +47,7 @@ the functional entries are `music` and `settings`; future pivots may join.
artist), **Pins** (user-pinned items — long-press anything to pin),
**History** (recently played), **New** (recently added).
- Purpose: bypass collection drilling; reach relevant content instantly.
- On device the left-edge hardware button summons it; in Xune-HD it is the
- On device the left-edge hardware button summons it; in Dorado-HD it is the
home screen's left page.
### 3.3 Crossbar ("sub-menus arrayed left to right across the top")
@@ -62,8 +62,8 @@ the functional entries are `music` and `settings`; future pivots may join.
(e.g. the bottom of "SETT" on the Settings screen) is the back button
(ZDNet Quick Start Guide discovery; Gizmodo confirms).
- Now Playing is the device's documented exception: it shows an explicit
left-arrow back button (Gizmodo). Xune-HD preserves this.
- Xune-HD additionally places a faint back arrow at the right edge of the
left-arrow back button (Gizmodo). Dorado-HD preserves this.
- Dorado-HD additionally places a faint back arrow at the right edge of the
cropped header on every detail screen, so the affordance is also visible
to users who never realize the cropped text is tappable. The arrow and the
cropped text are equivalent back actions; the cropped text remains the
@@ -83,7 +83,7 @@ the functional entries are `music` and `settings`; future pivots may join.
- Official apps and games were delivered exclusively through the **Apps
section of Zune Marketplace** (Wikipedia, *List of Zune applications*).
There was no home-menu `apps` entry.
- In Xune-HD the `marketplace` pivot's `apps` section lists installed
- In Dorado-HD the `marketplace` pivot's `apps` section lists installed
mini-apps plus the frozen official catalog (unavailable entries dim).
- Mini-apps open fullscreen and return via the cropped header / system back
(the device used its physical home button). See §8.
@@ -97,7 +97,7 @@ Layout (from ITPro Today's walkthrough):
- **Album art** prominent.
- Bottom row: **shuffle**, **repeat**, **rating** (heart / broken heart).
- The card **floats over artist photography** fetched from zune.net keyed by
MusicBrainz ID (see `net/` in Xune-HD; recreated by ZuneArtistImages).
MusicBrainz ID (see `net/` in Dorado-HD; recreated by ZuneArtistImages).
Interactions:
- **Idle for a few seconds → screensaver**: metadata (artist, track, album,
@@ -113,12 +113,12 @@ Ratings (tri-state heart, from the Zune desktop/HD family):
- Broken heart = dislike (skipped in shuffle).
- Unrated = neutral.
- Smart DJ honored ratings: hearts were prioritized and broken hearts
skipped when shuffling (Microsoft press release, Sept 15 2009). Xune-HD's
skipped when shuffling (Microsoft press release, Sept 15 2009). Dorado-HD's
shuffle mirrors this on-device behavior.
## 5. Lock/wake behavior
- Wake shows the user wallpaper behind a "software shade"; **slide the shade
up** to reveal the home screen (ITPro Today). Xune-HD v1 treats the Android
up** to reveal the home screen (ITPro Today). Dorado-HD v1 treats the Android
lock screen as the wake surface; the in-app shade is a stretch goal.
## 6. Motion rules
@@ -128,19 +128,19 @@ Ratings (tri-state heart, from the Zune desktop/HD family):
- Kinetic lists: fling, then long deceleration to rest.
- Everything fast: Metro is "designed to feel fast and responsive".
## 7. Banned in Xune-HD (invariant list, mirrors Not-Zune)
## 7. Banned in Dorado-HD (invariant list, mirrors Dorado)
- `RoundedCornerShape` / any nonzero corner radius on UI chrome.
- Drop shadows on text/buttons; gradient chrome; skeuomorphic textures.
- Icon-only navigation in the main hierarchy (text is navigation).
- Material default colors/typography leaking into screens.
- Star ratings (Zune is heart-based, tri-state).
## 8. Mini-app platform (Xune-HD)
## 8. Mini-app platform (Dorado-HD)
- Apps open fullscreen: no MiniPlayer; no crossbar unless the app defines one.
- The cropped header is the back affordance; system back also returns to
marketplace/apps (canon §3.6).
- Apps must render correctly in device mode (480x272) and adaptive mode.
- Xune-HD ships behavioral re-implementations only — no Microsoft code,
- Dorado-HD ships behavioral re-implementations only — no Microsoft code,
binaries or assets: utilities (calculator, notes, stopwatch, metronome,
alarm clock, calendar, level, piano, drum machine, chord finder, music
quiz, shuffle by album), games (solitaire, sudoku, hexic, reversi), and
@@ -148,7 +148,7 @@ Ratings (tri-state heart, from the Zune desktop/HD family):
messenger, msn money, zune reader).
## 9. Reference library
- not-zune (desktop) design-system skill + extracted Zune assets (MIT).
- dorado (desktop) design-system skill + extracted Zune assets (MIT).
- ZuneRedux/zune-hd-apps — original HD app archive (design reference only).
The corpus's NX container, Authenticode signature, manifest records and
AES-ECB-encrypted payload are documented in `docs/zcp-inventory.md`
+1 -1
View File
@@ -20,5 +20,5 @@ dependencyResolutionManagement {
}
}
rootProject.name = "xune-hd"
rootProject.name = "dorado-hd"
include(":app")
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Xune-HD .zcp corpus inventory generator.
"""Dorado-HD .zcp corpus inventory generator.
Reads the ZuneRedux `zune-hd-apps` archive (review/Zune HD Apps), parses the
NX container headers and plaintext manifests, and writes docs/zcp-inventory.md
@@ -83,7 +83,7 @@ def main() -> int:
"Generated by `tools/zcp_inventory.py` from the ZuneRedux `zune-hd-apps`",
"archive (62 installable marketplace packages). Design reference only —",
"per canon §8 and NOTICE, no Microsoft binary or asset is bundled or",
"shipped by Xune-HD.",
"shipped by Dorado-HD.",
"",
"## Container format (NX)",
"",