mirror of
https://github.com/project-dorado/dorado-hd.git
synced 2026-10-11 21:10:01 +00:00
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:
@@ -0,0 +1,2 @@
|
||||
# Default owners for everything in this repository.
|
||||
* @BillyOutlast
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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**
|
||||
|
||||
[]()
|
||||
[]()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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.**
|
||||
|
||||
@@ -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>
|
||||
|
||||
+32
-32
@@ -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
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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?
|
||||
+5
-5
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package com.heretek.xunehd.data.model
|
||||
package com.heretek.dorado_hd.data.model
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
+2
-2
@@ -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,
|
||||
+14
-14
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
+16
-16
@@ -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(
|
||||
+8
-8
@@ -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() }
|
||||
}
|
||||
+9
-9
@@ -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 }
|
||||
+6
-6
@@ -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,
|
||||
+3
-3
@@ -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),
|
||||
+2
-2
@@ -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
|
||||
+8
-8
@@ -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()
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
+6
-6
@@ -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(
|
||||
+8
-8
@@ -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) })
|
||||
+8
-8
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -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),
|
||||
)
|
||||
}
|
||||
+14
-14
@@ -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,
|
||||
),
|
||||
+17
-17
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package com.heretek.xunehd.media
|
||||
package com.heretek.dorado_hd.media
|
||||
|
||||
import java.util.Calendar
|
||||
|
||||
+6
-6
@@ -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
|
||||
+8
-8
@@ -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,
|
||||
+2
-2
@@ -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)"
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -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)"
|
||||
}
|
||||
}
|
||||
+67
-67
@@ -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
-1
@@ -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
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
+93
-93
@@ -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,
|
||||
),
|
||||
)
|
||||
+15
-15
@@ -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 ->
|
||||
+2
-2
@@ -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
-1
@@ -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
|
||||
+34
-34
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
-45
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-36
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-36
@@ -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)
|
||||
}
|
||||
}
|
||||
+75
-75
@@ -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,
|
||||
)
|
||||
}
|
||||
+63
-63
@@ -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)
|
||||
}
|
||||
}
|
||||
+51
-51
@@ -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,
|
||||
)
|
||||
}
|
||||
+62
-62
@@ -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,
|
||||
+51
-51
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+47
-47
@@ -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) {
|
||||
+49
-49
@@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
+32
-32
@@ -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",
|
||||
+34
-34
@@ -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>
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Xune HD</string>
|
||||
<string name="app_name">Dorado HD</string>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+9
-9
@@ -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
|
||||
+47
-47
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -20,5 +20,5 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "xune-hd"
|
||||
rootProject.name = "dorado-hd"
|
||||
include(":app")
|
||||
|
||||
@@ -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)",
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user