Changed the visualizer bg color to pure black and will be releasing Android next.
This commit is contained in:
parent
464aad571a
commit
b26d60b735
|
|
@ -66,6 +66,7 @@ ndk = "0.9"
|
|||
ndk-context = "0.1"
|
||||
android_logger = "0.14"
|
||||
log = "0.4"
|
||||
libc = "0.2"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.media.MediaMetadataRetriever
|
|||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
|
|
@ -13,6 +14,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
|
@ -83,7 +85,7 @@ class LibraryController(private val activity: ComponentActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/// extracts metadata, seeds the sidebar, copies bytes to cache, and pushes paths and art to rust.
|
||||
/// extracts metadata, seeds the sidebar, copies bytes to cache, and pushes paths to rust for lofty-side art extraction.
|
||||
private fun handlePickedAudio(uris: List<Uri>) {
|
||||
val handle = view?.viewportHandle ?: run {
|
||||
Log.w(TAG, "handlePickedAudio: no viewportHandle")
|
||||
|
|
@ -95,32 +97,35 @@ class LibraryController(private val activity: ComponentActivity) {
|
|||
val titles = arrayOfNulls<String>(total)
|
||||
val trackNumbers = IntArray(total)
|
||||
|
||||
main.post {
|
||||
NativeBridge.viewportSetCoordinatingMessage(handle, "Reading tags from $total track${if (total == 1) "" else "s"}...")
|
||||
}
|
||||
|
||||
io.execute {
|
||||
val metas = uris.map { extractMeta(it) }
|
||||
for (i in 0 until total) {
|
||||
titles[i] = metas[i].title.ifEmpty { displayName(uris[i]) ?: "Track ${i + 1}" }
|
||||
trackNumbers[i] = metas[i].track
|
||||
val meta = extractMeta(uris[i])
|
||||
titles[i] = meta.title.ifEmpty { displayName(uris[i]) ?: "Track ${i + 1}" }
|
||||
trackNumbers[i] = meta.track
|
||||
}
|
||||
main.post {
|
||||
NativeBridge.viewportSetPendingTitles(handle, titles.map { it ?: "" }.toTypedArray(), trackNumbers)
|
||||
NativeBridge.viewportSetLibraryProgress(handle, 0, total)
|
||||
}
|
||||
for (i in 0 until total) {
|
||||
val art = metas[i].art
|
||||
if (art != null && art.isNotEmpty()) {
|
||||
main.post { NativeBridge.viewportSetTrackArt(handle, i, art) }
|
||||
}
|
||||
NativeBridge.viewportSetCoordinatingMessage(handle, "Caching 1 of $total...")
|
||||
}
|
||||
var done = 0
|
||||
for (i in 0 until total) {
|
||||
val cachePath = copyToCache(uris[i])
|
||||
done += 1
|
||||
val current = done
|
||||
main.post {
|
||||
if (cachePath != null) {
|
||||
NativeBridge.viewportSetTrackPath(handle, i, cachePath)
|
||||
}
|
||||
NativeBridge.viewportSetLibraryProgress(handle, done, total)
|
||||
if (done == total) {
|
||||
NativeBridge.viewportSetLibraryProgress(handle, current, total)
|
||||
if (current < total) {
|
||||
NativeBridge.viewportSetCoordinatingMessage(handle, "Caching ${current + 1} of $total...")
|
||||
} else {
|
||||
NativeBridge.viewportSetCoordinatingMessage(handle, null)
|
||||
NativeBridge.viewportSetLibraryProgress(handle, 0, 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -128,8 +133,9 @@ class LibraryController(private val activity: ComponentActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
private data class TrackMeta(val title: String, val track: Int, val art: ByteArray?)
|
||||
private data class TrackMeta(val title: String, val track: Int)
|
||||
|
||||
/// pulls title and track-number via MediaMetadataRetriever, catching OOM so a single huge ID3v2 frame can't kill the worker.
|
||||
private fun extractMeta(uri: Uri): TrackMeta {
|
||||
val r = MediaMetadataRetriever()
|
||||
return try {
|
||||
|
|
@ -137,13 +143,12 @@ class LibraryController(private val activity: ComponentActivity) {
|
|||
val title = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE).orEmpty()
|
||||
val trackStr = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER)
|
||||
val track = trackStr?.substringBefore('/')?.toIntOrNull() ?: 0
|
||||
val art = r.embeddedPicture
|
||||
TrackMeta(title, track, art)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "extractMeta failed for $uri: ${e.message}")
|
||||
TrackMeta("", 0, null)
|
||||
TrackMeta(title, track)
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "extractMeta failed for $uri: ${t.javaClass.simpleName}: ${t.message}")
|
||||
TrackMeta("", 0)
|
||||
} finally {
|
||||
try { r.release() } catch (_: Exception) {}
|
||||
try { r.release() } catch (_: Throwable) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -157,17 +162,47 @@ class LibraryController(private val activity: ComponentActivity) {
|
|||
}
|
||||
|
||||
private fun copyToCache(uri: Uri): String? {
|
||||
val ext = displayName(uri)?.substringAfterLast('.', "")?.takeIf { it.isNotEmpty() } ?: "audio"
|
||||
val mime = activity.contentResolver.getType(uri)
|
||||
val ext = displayName(uri)?.substringAfterLast('.', "")?.takeIf { it.isNotEmpty() }
|
||||
?: extFromMime(mime)
|
||||
?: "audio"
|
||||
val out = File(activity.cacheDir, "${UUID.randomUUID()}.$ext")
|
||||
return try {
|
||||
activity.contentResolver.openInputStream(uri)?.use { input ->
|
||||
val written = openContentStream(uri).use { input ->
|
||||
out.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
Log.i(TAG, "copyToCache wrote ${out.length()} bytes to ${out.absolutePath}")
|
||||
if (written <= 0L || out.length() <= 0L) {
|
||||
Log.w(TAG, "copyToCache: copied $written bytes for $uri (mime=$mime ext=$ext)")
|
||||
out.delete()
|
||||
return null
|
||||
}
|
||||
Log.i(TAG, "copyToCache wrote $written bytes (mime=$mime) to ${out.absolutePath}")
|
||||
out.absolutePath
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "copyToCache failed for $uri: ${e.message}")
|
||||
Log.e(TAG, "copyToCache failed for $uri (mime=$mime): ${e.message}")
|
||||
out.delete()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/// opens the URI as a stream, falling back to a ParcelFileDescriptor when openInputStream returns null.
|
||||
private fun openContentStream(uri: Uri): InputStream {
|
||||
val resolver = activity.contentResolver
|
||||
resolver.openInputStream(uri)?.let { return it }
|
||||
val pfd = resolver.openFileDescriptor(uri, "r")
|
||||
?: throw java.io.IOException("openFileDescriptor returned null for $uri")
|
||||
return ParcelFileDescriptor.AutoCloseInputStream(pfd)
|
||||
}
|
||||
|
||||
/// maps an audio MIME type to the canonical file extension symphonia and lofty expect.
|
||||
private fun extFromMime(mime: String?): String? = when (mime?.lowercase()) {
|
||||
"audio/mpeg", "audio/mp3" -> "mp3"
|
||||
"audio/mp4", "audio/m4a", "audio/x-m4a", "audio/aac" -> "m4a"
|
||||
"audio/flac", "audio/x-flac" -> "flac"
|
||||
"audio/ogg", "audio/vorbis", "application/ogg" -> "ogg"
|
||||
"audio/opus" -> "opus"
|
||||
"audio/wav", "audio/x-wav", "audio/wave", "audio/vnd.wave" -> "wav"
|
||||
"audio/aiff", "audio/x-aiff" -> "aiff"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ object NativeBridge {
|
|||
external fun viewportApplyPickedFiles(handle: Long, paths: Array<String>)
|
||||
|
||||
external fun viewportSetLibraryProgress(handle: Long, current: Int, total: Int)
|
||||
external fun viewportSetCoordinatingMessage(handle: Long, msg: String?)
|
||||
external fun viewportSetPendingTitles(handle: Long, titles: Array<String>, trackNumbers: IntArray)
|
||||
external fun viewportSetTrackPath(handle: Long, idx: Int, path: String)
|
||||
external fun viewportSetTrackArt(handle: Long, idx: Int, bytes: ByteArray)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
# installs the apk, launches the activity, and tails logcat filtered to YrXtals and native crash channels.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/_env.sh"
|
||||
|
||||
# _env.sh enables `set -e`; turn it off so a single non-zero adb call doesn't kill the script.
|
||||
set +e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
"$SCRIPT_DIR/install.sh" release-debug
|
||||
install_rc=$?
|
||||
if [[ $install_rc -ne 0 ]]; then
|
||||
echo "install failed with rc=$install_rc" >&2
|
||||
exit $install_rc
|
||||
fi
|
||||
|
||||
if [[ -z "${ANDROID_TARGET:-}" ]]; then
|
||||
echo "no device selected." >&2
|
||||
|
|
@ -17,14 +25,31 @@ fi
|
|||
|
||||
PKG="org.elseif.yrxtals"
|
||||
|
||||
adb -s "$ANDROID_TARGET" logcat -c || true
|
||||
adb -s "$ANDROID_TARGET" shell am start -n "$PKG/.MainActivity" >/dev/null
|
||||
echo "→ adb logcat -c (clear)"
|
||||
adb -s "$ANDROID_TARGET" logcat -c >/dev/null 2>&1
|
||||
echo "→ adb shell am force-stop $PKG"
|
||||
adb -s "$ANDROID_TARGET" shell am force-stop "$PKG" >/dev/null 2>&1
|
||||
echo "→ adb shell am start -n $PKG/.MainActivity"
|
||||
adb -s "$ANDROID_TARGET" shell am start -n "$PKG/.MainActivity"
|
||||
start_rc=$?
|
||||
echo "am start rc=$start_rc"
|
||||
|
||||
PID="$(adb -s "$ANDROID_TARGET" shell pidof "$PKG" | tr -d '\r')"
|
||||
echo "running pid=$PID on $ANDROID_TARGET — tailing logcat (Ctrl+C to stop)"
|
||||
# poll up to 5s for the pid; pidof races the activity start.
|
||||
PID=""
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||
PID="$(adb -s "$ANDROID_TARGET" shell pidof "$PKG" 2>/dev/null | tr -d '\r')"
|
||||
[[ -n "$PID" ]] && break
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
echo "running pid='$PID' on $ANDROID_TARGET — tailing logcat (Ctrl+C to stop)"
|
||||
|
||||
# tags: YrXtals (kotlin), yr_crystals + yr_crystals.io (rust + stdio redirect), crash channels.
|
||||
FILTER=( YrXtals:V yr_crystals:V yr_crystals.io:V AndroidRuntime:E libc:F DEBUG:F '*:S' )
|
||||
|
||||
if [[ -n "$PID" ]]; then
|
||||
exec adb -s "$ANDROID_TARGET" logcat --pid="$PID" YrXtals:V yr_crystals:V AndroidRuntime:E libc:F DEBUG:F '*:S'
|
||||
exec adb -s "$ANDROID_TARGET" logcat --pid="$PID" "${FILTER[@]}"
|
||||
else
|
||||
exec adb -s "$ANDROID_TARGET" logcat YrXtals:V yr_crystals:V AndroidRuntime:E libc:F DEBUG:F '*:S'
|
||||
echo "pidof returned empty; tailing unfiltered by pid" >&2
|
||||
exec adb -s "$ANDROID_TARGET" logcat "${FILTER[@]}"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_initContext<'a>(
|
|||
.with_max_level(log::LevelFilter::Debug)
|
||||
.with_tag("yr_crystals"),
|
||||
);
|
||||
redirect_stdio_to_logcat();
|
||||
let vm = match env.get_java_vm() {
|
||||
Ok(vm) => vm,
|
||||
Err(_) => return,
|
||||
|
|
@ -242,6 +243,23 @@ pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetLibraryPr
|
|||
av.inner.set_library_progress(current.max(0) as u32, total.max(0) as u32);
|
||||
}
|
||||
|
||||
/// shows or clears the modal coordinator-wait overlay; null/empty message hides the overlay.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetCoordinatingMessage<'a>(
|
||||
mut env: JNIEnv<'a>,
|
||||
_class: JClass<'a>,
|
||||
handle: jlong,
|
||||
msg: JString<'a>,
|
||||
) {
|
||||
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
||||
let s = if msg.is_null() {
|
||||
None
|
||||
} else {
|
||||
env.get_string(&msg).ok().map(String::from).filter(|s| !s.is_empty())
|
||||
};
|
||||
av.inner.set_coordinating_message(s);
|
||||
}
|
||||
|
||||
/// seeds the sidebar with placeholder rows of titles plus track-number tags ahead of export completion.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetPendingTitles<'a>(
|
||||
|
|
@ -327,6 +345,33 @@ pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportTakePendingP
|
|||
av.inner.take_pending_pick() as jint
|
||||
}
|
||||
|
||||
/// dup2s stdout and stderr onto a pipe and pumps each line into logcat under the `yr_crystals.io` tag.
|
||||
fn redirect_stdio_to_logcat() {
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::os::fd::FromRawFd;
|
||||
let mut fds: [libc::c_int; 2] = [0, 0];
|
||||
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
|
||||
if rc != 0 {
|
||||
return;
|
||||
}
|
||||
let (rfd, wfd) = (fds[0], fds[1]);
|
||||
unsafe {
|
||||
libc::dup2(wfd, libc::STDOUT_FILENO);
|
||||
libc::dup2(wfd, libc::STDERR_FILENO);
|
||||
libc::close(wfd);
|
||||
}
|
||||
std::thread::Builder::new()
|
||||
.name("yr_crystals.stdio_to_logcat".into())
|
||||
.spawn(move || {
|
||||
let file = unsafe { std::fs::File::from_raw_fd(rfd) };
|
||||
let reader = BufReader::new(file);
|
||||
for line in reader.lines().flatten() {
|
||||
log::info!(target: "yr_crystals.io", "{}", line);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// nullability check on a JObject through its raw pointer.
|
||||
trait JObjectExt {
|
||||
fn is_null(&self) -> bool;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,13 @@ pub fn decode_file(path: &Path) -> Result<TrackData, DecodeError> {
|
|||
#[cfg(debug_assertions)]
|
||||
let _t0 = std::time::Instant::now();
|
||||
|
||||
let file_size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
|
||||
eprintln!(
|
||||
"yr_crystals[decode] open {:?} ({} bytes)",
|
||||
path.file_name().unwrap_or_default(),
|
||||
file_size,
|
||||
);
|
||||
|
||||
let file = File::open(path)?;
|
||||
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
||||
|
||||
|
|
@ -71,8 +78,13 @@ pub fn decode_file(path: &Path) -> Result<TrackData, DecodeError> {
|
|||
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||
.ok_or(DecodeError::NoTrack)?;
|
||||
let track_id = track.id;
|
||||
let codec_short = track.codec_params.codec;
|
||||
let probed_rate = track.codec_params.sample_rate;
|
||||
let probed_channels = track.codec_params.channels.map(|c| c.count());
|
||||
eprintln!(
|
||||
"yr_crystals[decode] probed codec={:?} rate={:?} ch={:?}",
|
||||
codec_short, probed_rate, probed_channels,
|
||||
);
|
||||
|
||||
let mut decoder =
|
||||
symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default())?;
|
||||
|
|
@ -82,23 +94,38 @@ pub fn decode_file(path: &Path) -> Result<TrackData, DecodeError> {
|
|||
|
||||
let mut actual_sample_rate = probed_rate.unwrap_or(48_000);
|
||||
let mut actual_channels = probed_channels.unwrap_or(2).max(1);
|
||||
let mut packet_count: u64 = 0;
|
||||
let mut decode_errors: u64 = 0;
|
||||
|
||||
loop {
|
||||
let packet = match format.next_packet() {
|
||||
Ok(p) => p,
|
||||
Err(SymError::IoError(e)) if e.kind() == ErrorKind::UnexpectedEof => break,
|
||||
Err(SymError::ResetRequired) => break,
|
||||
Err(e) => return Err(e.into()),
|
||||
Err(e) => {
|
||||
eprintln!("yr_crystals[decode] next_packet err: {e}");
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
if packet.track_id() != track_id {
|
||||
continue;
|
||||
}
|
||||
packet_count += 1;
|
||||
|
||||
let decoded = match decoder.decode(&packet) {
|
||||
Ok(d) => d,
|
||||
|
||||
Err(SymError::DecodeError(_)) => continue,
|
||||
Err(e) => return Err(e.into()),
|
||||
Err(SymError::DecodeError(msg)) => {
|
||||
decode_errors += 1;
|
||||
if decode_errors <= 3 {
|
||||
eprintln!("yr_crystals[decode] frame decode err #{decode_errors}: {msg}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("yr_crystals[decode] fatal decode err: {e}");
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
let spec = *decoded.spec();
|
||||
|
|
@ -121,6 +148,14 @@ pub fn decode_file(path: &Path) -> Result<TrackData, DecodeError> {
|
|||
append_stereo(&mut pcm, buf.samples(), actual_channels);
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"yr_crystals[decode] done: {} packets, {} decode-errs, {} stereo frames, sr={}",
|
||||
packet_count,
|
||||
decode_errors,
|
||||
pcm.len() / 2,
|
||||
actual_sample_rate,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"yr_crystals[dbg] decode_file({:?}) took {:?} — {} stereo frames @ {} Hz",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ pub struct App {
|
|||
/// modal status message shown over the UI while waiting on iOS file-coordinator caching.
|
||||
pub coordinating_message: Option<String>,
|
||||
|
||||
/// running count of decode failures since the last successful track load.
|
||||
pub consecutive_failures: usize,
|
||||
|
||||
/// settings panel scroll offset mirrored from the on_scroll callback.
|
||||
pub settings_scroll: AbsoluteOffset,
|
||||
|
||||
|
|
@ -174,6 +177,7 @@ impl App {
|
|||
track_loading: false,
|
||||
library_progress: None,
|
||||
coordinating_message: None,
|
||||
consecutive_failures: 0,
|
||||
settings_scroll: AbsoluteOffset::default(),
|
||||
restore_sidebar_scroll: false,
|
||||
restore_settings_scroll: false,
|
||||
|
|
@ -428,6 +432,9 @@ impl App {
|
|||
self.track_loading = false;
|
||||
match result {
|
||||
Ok(td) => {
|
||||
if td.is_valid() {
|
||||
self.consecutive_failures = 0;
|
||||
}
|
||||
if let Some(eng) = &self.engine {
|
||||
eng.load(td.clone(), self.current_decode_id);
|
||||
if self.playing {
|
||||
|
|
@ -438,7 +445,10 @@ impl App {
|
|||
}
|
||||
self.worker.set_track(td);
|
||||
}
|
||||
Err(e) => eprintln!("yr_crystals: decode failed: {e}"),
|
||||
Err(e) => {
|
||||
eprintln!("yr_crystals: decode failed: {e}");
|
||||
self.note_failure_and_skip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -473,6 +483,17 @@ impl App {
|
|||
self.load_index(next);
|
||||
}
|
||||
|
||||
/// counts a decode failure and skips past the failed track, halting once the whole library has cycled.
|
||||
fn note_failure_and_skip(&mut self) {
|
||||
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
|
||||
if self.library.tracks.len() <= 1 || self.consecutive_failures >= self.library.tracks.len() {
|
||||
self.playing = false;
|
||||
if let Some(eng) = &self.engine { eng.pause(); }
|
||||
return;
|
||||
}
|
||||
self.advance_to_next();
|
||||
}
|
||||
|
||||
/// re-sorts the library on arrival of track-number tags and preserves the current selection.
|
||||
#[allow(dead_code)]
|
||||
fn resort_library(&mut self) {
|
||||
|
|
@ -533,6 +554,7 @@ impl App {
|
|||
if idx < self.library.tracks.len() {
|
||||
self.selected_track = Some(idx);
|
||||
self.playing = true;
|
||||
self.consecutive_failures = 0;
|
||||
self.load_index(idx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ use iced_wgpu::core::Color;
|
|||
pub mod palette {
|
||||
use super::Color;
|
||||
|
||||
const BG_C: Color = Color::from_rgb(0.06, 0.06, 0.08);
|
||||
const SIDEBAR_C: Color = Color::from_rgb(0.09, 0.09, 0.12);
|
||||
const BORDER_C: Color = Color::from_rgb(0.18, 0.18, 0.22);
|
||||
const BG_C: Color = Color::from_rgb(0.0, 0.0, 0.0);
|
||||
const SIDEBAR_C: Color = Color::from_rgb(0.04, 0.04, 0.05);
|
||||
const BORDER_C: Color = Color::from_rgb(0.10, 0.10, 0.12);
|
||||
const TEXT_C: Color = Color::from_rgb(0.92, 0.92, 0.95);
|
||||
const TEXT_DIM_C: Color = Color::from_rgb(0.55, 0.55, 0.62);
|
||||
const ACCENT_C: Color = Color::from_rgb(0.78, 0.62, 0.95);
|
||||
|
|
|
|||
Loading…
Reference in New Issue